Quellcode durchsuchen

added sentry and alerter interface, for internal server alerts

Alexander Belanger vor 4 Jahren
Ursprung
Commit
84c85c64b5
86 geänderte Dateien mit 533 neuen und 461 gelöschten Zeilen
  1. 5 5
      api/server/authn/handler.go
  2. 3 3
      api/server/authn/handler_test.go
  3. 3 3
      api/server/authn/session_helpers.go
  4. 8 8
      api/server/authz/cluster.go
  5. 6 6
      api/server/authz/git_installation.go
  6. 6 6
      api/server/authz/helm_repo.go
  7. 6 6
      api/server/authz/infra.go
  8. 6 6
      api/server/authz/invite.go
  9. 8 7
      api/server/authz/policy.go
  10. 2 2
      api/server/authz/policy_test.go
  11. 5 5
      api/server/authz/project.go
  12. 2 2
      api/server/authz/project_test.go
  13. 6 6
      api/server/authz/registry.go
  14. 6 6
      api/server/authz/release.go
  15. 0 25
      api/server/handlers/capabilities/get.go
  16. 5 4
      api/server/handlers/cluster/create_namespace.go
  17. 4 3
      api/server/handlers/cluster/delete_namespace.go
  18. 4 4
      api/server/handlers/cluster/detect_prometheus_installed.go
  19. 4 3
      api/server/handlers/cluster/get.go
  20. 5 4
      api/server/handlers/cluster/get_kubeconfig.go
  21. 6 5
      api/server/handlers/cluster/get_pod_metrics.go
  22. 4 3
      api/server/handlers/cluster/list.go
  23. 5 4
      api/server/handlers/cluster/list_namespaces.go
  24. 5 4
      api/server/handlers/cluster/list_nginx_ingresses.go
  25. 3 2
      api/server/handlers/gitinstallation/get.go
  26. 14 13
      api/server/handlers/handler.go
  27. 3 2
      api/server/handlers/helmrepo/get.go
  28. 3 2
      api/server/handlers/infra/get.go
  29. 3 2
      api/server/handlers/invite/get.go
  30. 26 0
      api/server/handlers/metadata/get.go
  31. 4 3
      api/server/handlers/project/create.go
  32. 3 2
      api/server/handlers/project/get.go
  33. 6 4
      api/server/handlers/project/get_policy.go
  34. 4 3
      api/server/handlers/project/list.go
  35. 4 3
      api/server/handlers/project/list_infra.go
  36. 3 2
      api/server/handlers/registry/get.go
  37. 4 3
      api/server/handlers/release/get.go
  38. 8 7
      api/server/handlers/user/cli_login.go
  39. 7 6
      api/server/handlers/user/create.go
  40. 3 2
      api/server/handlers/user/current.go
  41. 5 4
      api/server/handlers/user/delete.go
  42. 14 6
      api/server/handlers/user/email_verify.go
  43. 2 0
      api/server/handlers/user/email_verify_test.go
  44. 7 6
      api/server/handlers/user/login.go
  45. 3 3
      api/server/handlers/user/logout.go
  46. 34 22
      api/server/handlers/user/pw_reset.go
  47. 8 7
      api/server/router/base.go
  48. 3 2
      api/server/router/cluster.go
  49. 3 2
      api/server/router/git_installation.go
  50. 3 2
      api/server/router/helm_repo.go
  51. 3 2
      api/server/router/infra.go
  52. 3 2
      api/server/router/invite.go
  53. 3 2
      api/server/router/namespace.go
  54. 3 2
      api/server/router/project.go
  55. 3 2
      api/server/router/registry.go
  56. 3 2
      api/server/router/release.go
  57. 4 3
      api/server/router/router.go
  58. 3 2
      api/server/router/user.go
  59. 9 0
      api/server/shared/apierrors/alerter/alerter.go
  60. 7 0
      api/server/shared/apierrors/alerter/noop.go
  61. 29 0
      api/server/shared/apierrors/alerter/sentry.go
  62. 11 4
      api/server/shared/apierrors/errors.go
  63. 3 3
      api/server/shared/apitest/authn.go
  64. 7 7
      api/server/shared/apitest/config.go
  65. 4 3
      api/server/shared/apitest/request.go
  66. 2 2
      api/server/shared/apitest/user.go
  67. 10 3
      api/server/shared/config/config.go
  68. 8 8
      api/server/shared/config/loader/envloader.go
  69. 17 10
      api/server/shared/config/loader/loader.go
  70. 4 4
      api/server/shared/config/metadata.go
  71. 4 3
      api/server/shared/endpoints.go
  72. 6 5
      api/server/shared/reader.go
  73. 8 6
      api/server/shared/writer.go
  74. 2 2
      cmd/app/main.go
  75. 2 2
      docs/developing/backend-refactor.md
  76. 1 0
      go.mod
  77. 72 0
      go.sum
  78. 2 2
      internal/adapter/gorm.go
  79. 1 1
      internal/adapter/redis.go
  80. 1 2
      internal/auth/sessionstore/sessionstore.go
  81. 0 117
      internal/config/config.go
  82. 0 13
      internal/config/redis.go
  83. 1 2
      internal/kubernetes/agent.go
  84. 1 2
      internal/kubernetes/provisioner/provisioner.go
  85. 2 2
      internal/repository/gorm/helpers_test.go
  86. 0 1
      server/api/api.go

+ 5 - 5
api/server/authn/handler.go

@@ -7,21 +7,21 @@ import (
 	"strings"
 
 	"github.com/gorilla/sessions"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
 )
 
 // AuthNFactory generates a middleware handler `AuthN`
 type AuthNFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 // NewAuthNFactory returns an `AuthNFactory` that uses the passed-in server
 // config
 func NewAuthNFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *AuthNFactory {
 	return &AuthNFactory{config}
 }
@@ -42,7 +42,7 @@ func (f *AuthNFactory) NewAuthenticatedWithRedirect(next http.Handler) http.Hand
 // AuthN implements the authentication middleware
 type AuthN struct {
 	next     http.Handler
-	config   *shared.Config
+	config   *config.Config
 	redirect bool
 }
 
@@ -153,7 +153,7 @@ func (authn *AuthN) nextWithUserID(w http.ResponseWriter, r *http.Request, userI
 func (authn *AuthN) sendForbiddenError(err error, w http.ResponseWriter) {
 	reqErr := apierrors.NewErrForbidden(err)
 
-	apierrors.HandleAPIError(w, authn.config.Logger, reqErr)
+	apierrors.HandleAPIError(context.Background(), authn.config, w, reqErr)
 }
 
 var errInvalidToken = fmt.Errorf("authorization header exists, but token is not valid")

+ 3 - 3
api/server/authn/handler_test.go

@@ -7,8 +7,8 @@ import (
 	"testing"
 
 	"github.com/porter-dev/porter/api/server/authn"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/stretchr/testify/assert"
@@ -211,7 +211,7 @@ func (t *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	t.User = user
 }
 
-func loadHandlers(t *testing.T) (*shared.Config, http.Handler, *testHandler) {
+func loadHandlers(t *testing.T) (*config.Config, http.Handler, *testHandler) {
 	config := apitest.LoadConfig(t)
 
 	factory := authn.NewAuthNFactory(config)
@@ -222,7 +222,7 @@ func loadHandlers(t *testing.T) (*shared.Config, http.Handler, *testHandler) {
 	return config, handler, next
 }
 
-func loadHandlersWithRedirect(t *testing.T) (*shared.Config, http.Handler, *testHandler) {
+func loadHandlersWithRedirect(t *testing.T) (*config.Config, http.Handler, *testHandler) {
 	config := apitest.LoadConfig(t)
 
 	factory := authn.NewAuthNFactory(config)

+ 3 - 3
api/server/authn/session_helpers.go

@@ -3,14 +3,14 @@ package authn
 import (
 	"net/http"
 
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 func SaveUserAuthenticated(
 	w http.ResponseWriter,
 	r *http.Request,
-	config *shared.Config,
+	config *config.Config,
 	user *models.User,
 ) error {
 	session, err := config.Store.Get(r, config.ServerConf.CookieName)
@@ -36,7 +36,7 @@ func SaveUserAuthenticated(
 func SaveUserUnauthenticated(
 	w http.ResponseWriter,
 	r *http.Request,
-	config *shared.Config,
+	config *config.Config,
 ) error {
 	session, err := config.Store.Get(r, config.ServerConf.CookieName)
 

+ 8 - 8
api/server/authz/cluster.go

@@ -6,8 +6,8 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/helm"
 	"github.com/porter-dev/porter/internal/kubernetes"
@@ -21,11 +21,11 @@ const KubernetesDynamicClientCtxKey string = "k8s-dyn-client"
 const HelmAgentCtxKey string = "helm-agent"
 
 type ClusterScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewClusterScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *ClusterScopedFactory {
 	return &ClusterScopedFactory{config}
 }
@@ -36,7 +36,7 @@ func (p *ClusterScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type ClusterScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *ClusterScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -50,11 +50,11 @@ func (p *ClusterScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("cluster with id %d not found in project %d", clusterID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return
@@ -77,10 +77,10 @@ type KubernetesAgentGetter interface {
 }
 
 type OutOfClusterAgentGetter struct {
-	config *shared.Config
+	config *config.Config
 }
 
-func NewOutOfClusterAgentGetter(config *shared.Config) KubernetesAgentGetter {
+func NewOutOfClusterAgentGetter(config *config.Config) KubernetesAgentGetter {
 	return &OutOfClusterAgentGetter{config}
 }
 

+ 6 - 6
api/server/authz/git_installation.go

@@ -6,8 +6,8 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
@@ -15,11 +15,11 @@ import (
 )
 
 type GitInstallationScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewGitInstallationScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *GitInstallationScopedFactory {
 	return &GitInstallationScopedFactory{config}
 }
@@ -30,7 +30,7 @@ func (p *GitInstallationScopedFactory) Middleware(next http.Handler) http.Handle
 
 type GitInstallationScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -45,11 +45,11 @@ func (p *GitInstallationScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *ht
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("github app installation with id %d not found in project %d", gitInstallationID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return

+ 6 - 6
api/server/authz/helm_repo.go

@@ -6,19 +6,19 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 )
 
 type HelmRepoScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewHelmRepoScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *HelmRepoScopedFactory {
 	return &HelmRepoScopedFactory{config}
 }
@@ -29,7 +29,7 @@ func (p *HelmRepoScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type HelmRepoScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *HelmRepoScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -44,11 +44,11 @@ func (p *HelmRepoScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("helm repo with id %d not found in project %d", helmRepoID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return

+ 6 - 6
api/server/authz/infra.go

@@ -6,19 +6,19 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 )
 
 type InfraScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewInfraScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *InfraScopedFactory {
 	return &InfraScopedFactory{config}
 }
@@ -29,7 +29,7 @@ func (p *InfraScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type InfraScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *InfraScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -44,11 +44,11 @@ func (p *InfraScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("infra with id %d not found in project %d", infraID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return

+ 6 - 6
api/server/authz/invite.go

@@ -6,19 +6,19 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 )
 
 type InviteScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewInviteScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *InviteScopedFactory {
 	return &InviteScopedFactory{config}
 }
@@ -29,7 +29,7 @@ func (p *InviteScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type InviteScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *InviteScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -44,11 +44,11 @@ func (p *InviteScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("invite with id %d not found in project %d", inviteID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return

+ 8 - 7
api/server/authz/policy.go

@@ -6,20 +6,20 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 type PolicyMiddleware struct {
-	config       *shared.Config
+	config       *config.Config
 	endpointMeta types.APIRequestMetadata
 	loader       policy.PolicyDocumentLoader
 }
 
 func NewPolicyMiddleware(
-	config *shared.Config,
+	config *config.Config,
 	endpointMeta types.APIRequestMetadata,
 	loader policy.PolicyDocumentLoader,
 ) *PolicyMiddleware {
@@ -32,7 +32,7 @@ func (p *PolicyMiddleware) Middleware(next http.Handler) http.Handler {
 
 type PolicyHandler struct {
 	next         http.Handler
-	config       *shared.Config
+	config       *config.Config
 	endpointMeta types.APIRequestMetadata
 	loader       policy.PolicyDocumentLoader
 }
@@ -42,7 +42,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	reqScopes, reqErr := getRequestActionForEndpoint(r, h.endpointMeta)
 
 	if reqErr != nil {
-		apierrors.HandleAPIError(w, h.config.Logger, reqErr)
+		apierrors.HandleAPIError(r.Context(), h.config, w, reqErr)
 		return
 	}
 
@@ -53,7 +53,7 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	policyDocs, reqErr := h.loader.LoadPolicyDocuments(user.ID, projID)
 
 	if reqErr != nil {
-		apierrors.HandleAPIError(w, h.config.Logger, reqErr)
+		apierrors.HandleAPIError(r.Context(), h.config, w, reqErr)
 		return
 	}
 
@@ -62,8 +62,9 @@ func (h *PolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if !hasAccess {
 		apierrors.HandleAPIError(
+			r.Context(),
+			h.config,
 			w,
-			h.config.Logger,
 			apierrors.NewErrForbidden(fmt.Errorf("policy forbids action for user %d in project %d", user.ID, projID)),
 		)
 

+ 2 - 2
api/server/authz/policy_test.go

@@ -9,9 +9,9 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/handlers/project"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/stretchr/testify/assert"
@@ -238,7 +238,7 @@ func loadHandlers(
 	endpointMeta types.APIRequestMetadata,
 	shouldLoaderFail bool,
 	shouldLoaderLoadViewer bool,
-) (*shared.Config, http.Handler, *testHandler) {
+) (*config.Config, http.Handler, *testHandler) {
 	config := apitest.LoadConfig(t)
 	var loader policy.PolicyDocumentLoader = policy.NewBasicPolicyDocumentLoader(config.Repo.Project())
 

+ 5 - 5
api/server/authz/project.go

@@ -5,18 +5,18 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
 
 type ProjectScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewProjectScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *ProjectScopedFactory {
 	return &ProjectScopedFactory{config}
 }
@@ -27,7 +27,7 @@ func (p *ProjectScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type ProjectScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *ProjectScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -39,7 +39,7 @@ func (p *ProjectScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	project, err := p.config.Repo.Project().ReadProject(projID)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		return
 	}
 

+ 2 - 2
api/server/authz/project_test.go

@@ -7,8 +7,8 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/handlers/project"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apitest"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository/test"
@@ -76,7 +76,7 @@ func TestProjectMiddlewareFailedRead(t *testing.T) {
 func loadProjectHandlers(
 	t *testing.T,
 	failingRepoMethods ...string,
-) (*shared.Config, http.Handler, *testProjectHandler) {
+) (*config.Config, http.Handler, *testProjectHandler) {
 	config := apitest.LoadConfig(t, failingRepoMethods...)
 	mwFactory := authz.NewProjectScopedFactory(config)
 

+ 6 - 6
api/server/authz/registry.go

@@ -6,19 +6,19 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"gorm.io/gorm"
 )
 
 type RegistryScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewRegistryScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *RegistryScopedFactory {
 	return &RegistryScopedFactory{config}
 }
@@ -29,7 +29,7 @@ func (p *RegistryScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type RegistryScopedMiddleware struct {
 	next   http.Handler
-	config *shared.Config
+	config *config.Config
 }
 
 func (p *RegistryScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -44,11 +44,11 @@ func (p *RegistryScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrForbidden(
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrForbidden(
 				fmt.Errorf("registry with id %d not found in project %d", registryID, proj.ID),
 			))
 		} else {
-			apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+			apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		}
 
 		return

+ 6 - 6
api/server/authz/release.go

@@ -5,19 +5,19 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/authz/policy"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"helm.sh/helm/v3/pkg/release"
 )
 
 type ReleaseScopedFactory struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func NewReleaseScopedFactory(
-	config *shared.Config,
+	config *config.Config,
 ) *ReleaseScopedFactory {
 	return &ReleaseScopedFactory{config}
 }
@@ -28,7 +28,7 @@ func (p *ReleaseScopedFactory) Middleware(next http.Handler) http.Handler {
 
 type ReleaseScopedMiddleware struct {
 	next        http.Handler
-	config      *shared.Config
+	config      *config.Config
 	agentGetter KubernetesAgentGetter
 }
 
@@ -38,7 +38,7 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	helmAgent, err := p.agentGetter.GetHelmAgent(r, cluster)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -52,7 +52,7 @@ func (p *ReleaseScopedMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	release, err := helmAgent.GetRelease(name, int(version))
 
 	if err != nil {
-		apierrors.HandleAPIError(w, p.config.Logger, apierrors.NewErrInternal(err))
+		apierrors.HandleAPIError(r.Context(), p.config, w, apierrors.NewErrInternal(err))
 		return
 	}
 

+ 0 - 25
api/server/handlers/capabilities/get.go

@@ -1,25 +0,0 @@
-package capabilities
-
-import (
-	"net/http"
-
-	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
-)
-
-type CapabilitiesGetHandler struct {
-	handlers.PorterHandlerWriter
-}
-
-func NewCapabilitiesGetHandler(
-	config *shared.Config,
-	writer shared.ResultWriter,
-) *CapabilitiesGetHandler {
-	return &CapabilitiesGetHandler{
-		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
-	}
-}
-
-func (v *CapabilitiesGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	v.WriteResult(w, v.Config().Capabilities)
-}

+ 5 - 4
api/server/handlers/cluster/create_namespace.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -17,7 +18,7 @@ type CreateNamespaceHandler struct {
 }
 
 func NewCreateNamespaceHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *CreateNamespaceHandler {
@@ -39,14 +40,14 @@ func (c *CreateNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	namespace, err := agent.CreateNamespace(request.Name)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -54,5 +55,5 @@ func (c *CreateNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		Namespace: namespace,
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 4 - 3
api/server/handlers/cluster/delete_namespace.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -17,7 +18,7 @@ type DeleteNamespaceHandler struct {
 }
 
 func NewDeleteNamespaceHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 ) *DeleteNamespaceHandler {
 	return &DeleteNamespaceHandler{
@@ -38,12 +39,12 @@ func (c *DeleteNamespaceHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	if err := agent.DeleteNamespace(request.Name); err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 

+ 4 - 4
api/server/handlers/cluster/detect_prometheus_installed.go

@@ -5,8 +5,8 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	"github.com/porter-dev/porter/internal/models"
@@ -18,7 +18,7 @@ type DetectPrometheusInstalledHandler struct {
 }
 
 func NewDetectPrometheusInstalledHandler(
-	config *shared.Config,
+	config *config.Config,
 ) *DetectPrometheusInstalledHandler {
 	return &DetectPrometheusInstalledHandler{
 		PorterHandler:         handlers.NewDefaultPorterHandler(config, nil, nil),
@@ -32,12 +32,12 @@ func (c *DetectPrometheusInstalledHandler) ServeHTTP(w http.ResponseWriter, r *h
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	if _, found, err := prometheus.GetPrometheusService(agent.Clientset); err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	} else if !found {
 		http.NotFound(w, r)

+ 4 - 3
api/server/handlers/cluster/get.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/domain"
@@ -19,7 +20,7 @@ type ClusterGetHandler struct {
 }
 
 func NewClusterGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ClusterGetHandler {
 	return &ClusterGetHandler{
@@ -38,7 +39,7 @@ func (c *ClusterGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -52,5 +53,5 @@ func (c *ClusterGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		res.IngressError = kubernetes.CatchK8sConnectionError(ingressErr).Externalize()
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 5 - 4
api/server/handlers/cluster/get_kubeconfig.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"k8s.io/client-go/tools/clientcmd"
@@ -18,7 +19,7 @@ type GetTemporaryKubeconfigHandler struct {
 }
 
 func NewGetTemporaryKubeconfigHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *GetTemporaryKubeconfigHandler {
 	return &GetTemporaryKubeconfigHandler{
@@ -35,14 +36,14 @@ func (c *GetTemporaryKubeconfigHandler) ServeHTTP(w http.ResponseWriter, r *http
 	kubeconfig, err := outOfClusterConfig.CreateRawConfigFromCluster()
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	kubeconfigBytes, err := clientcmd.Write(*kubeconfig)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -50,5 +51,5 @@ func (c *GetTemporaryKubeconfigHandler) ServeHTTP(w http.ResponseWriter, r *http
 		Kubeconfig: kubeconfigBytes,
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 6 - 5
api/server/handlers/cluster/get_pod_metrics.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -19,7 +20,7 @@ type GetPodMetricsHandler struct {
 }
 
 func NewGetPodMetricsHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *GetPodMetricsHandler {
@@ -41,7 +42,7 @@ func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -49,14 +50,14 @@ func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	promSvc, found, err := prometheus.GetPrometheusService(agent.Clientset)
 
 	if err != nil || !found {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	rawQuery, err := prometheus.QueryPrometheus(agent.Clientset, promSvc, &request.QueryOpts)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -64,5 +65,5 @@ func (c *GetPodMetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 	var res types.GetPodMetricsResponse = &s
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 4 - 3
api/server/handlers/cluster/list.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -15,7 +16,7 @@ type ClusterListHandler struct {
 }
 
 func NewClusterListHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ClusterListHandler {
 	return &ClusterListHandler{
@@ -31,7 +32,7 @@ func (p *ClusterListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	clusters, err := p.Repo().Cluster().ListClustersByProjectID(proj.ID)
 
 	if err != nil {
-		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		p.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -41,5 +42,5 @@ func (p *ClusterListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		res[i] = cluster.ToClusterType()
 	}
 
-	p.WriteResult(w, res)
+	p.WriteResult(r.Context(), w, res)
 }

+ 5 - 4
api/server/handlers/cluster/list_namespaces.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -17,7 +18,7 @@ type ListNamespacesHandler struct {
 }
 
 func NewListNamespacesHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ListNamespacesHandler {
 	return &ListNamespacesHandler{
@@ -32,14 +33,14 @@ func (c *ListNamespacesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	namespaceList, err := agent.ListNamespaces()
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -47,5 +48,5 @@ func (c *ListNamespacesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
 		NamespaceList: namespaceList,
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 5 - 4
api/server/handlers/cluster/list_nginx_ingresses.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/kubernetes/prometheus"
 	"github.com/porter-dev/porter/internal/models"
@@ -18,7 +19,7 @@ type ListNGINXIngressesHandler struct {
 }
 
 func NewListNGINXIngressesHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ListNGINXIngressesHandler {
 	return &ListNGINXIngressesHandler{
@@ -33,18 +34,18 @@ func (c *ListNGINXIngressesHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 	agent, err := c.GetAgent(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	ingresses, err := prometheus.GetIngressesWithNGINXAnnotation(agent.Clientset)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	var res types.ListNGINXIngressesResponse = ingresses
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 3 - 2
api/server/handlers/gitinstallation/get.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models/integrations"
 )
@@ -16,7 +17,7 @@ type GitInstallationGetHandler struct {
 }
 
 func NewGitInstallationGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *GitInstallationGetHandler {
 	return &GitInstallationGetHandler{
@@ -28,5 +29,5 @@ func NewGitInstallationGetHandler(
 func (c *GitInstallationGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
 
-	c.WriteResult(w, ga.ToGitInstallationType())
+	c.WriteResult(r.Context(), w, ga.ToGitInstallationType())
 }

+ 14 - 13
api/server/handlers/handler.go

@@ -1,28 +1,29 @@
 package handlers
 
 import (
+	"context"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/repository"
 )
 
 type PorterHandler interface {
-	Config() *shared.Config
+	Config() *config.Config
 	Repo() repository.Repository
-	HandleAPIError(w http.ResponseWriter, err apierrors.RequestError)
+	HandleAPIError(ctx context.Context, w http.ResponseWriter, err apierrors.RequestError)
 }
 
 type PorterHandlerWriter interface {
 	PorterHandler
-	WriteResult(w http.ResponseWriter, v interface{})
+	shared.ResultWriter
 }
 
 type PorterHandlerReader interface {
 	PorterHandler
-	DecodeAndValidate(w http.ResponseWriter, r *http.Request, v interface{}) bool
-	DecodeAndValidateNoWrite(r *http.Request, v interface{}) error
+	shared.RequestDecoderValidator
 }
 
 type PorterHandlerReadWriter interface {
@@ -31,20 +32,20 @@ type PorterHandlerReadWriter interface {
 }
 
 type DefaultPorterHandler struct {
-	config           *shared.Config
+	config           *config.Config
 	decoderValidator shared.RequestDecoderValidator
 	writer           shared.ResultWriter
 }
 
 func NewDefaultPorterHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) PorterHandlerReadWriter {
 	return &DefaultPorterHandler{config, decoderValidator, writer}
 }
 
-func (d *DefaultPorterHandler) Config() *shared.Config {
+func (d *DefaultPorterHandler) Config() *config.Config {
 	return d.config
 }
 
@@ -52,12 +53,12 @@ func (d *DefaultPorterHandler) Repo() repository.Repository {
 	return d.config.Repo
 }
 
-func (d *DefaultPorterHandler) HandleAPIError(w http.ResponseWriter, err apierrors.RequestError) {
-	apierrors.HandleAPIError(w, d.Config().Logger, err)
+func (d *DefaultPorterHandler) HandleAPIError(ctx context.Context, w http.ResponseWriter, err apierrors.RequestError) {
+	apierrors.HandleAPIError(ctx, d.Config(), w, err)
 }
 
-func (d *DefaultPorterHandler) WriteResult(w http.ResponseWriter, v interface{}) {
-	d.writer.WriteResult(w, v)
+func (d *DefaultPorterHandler) WriteResult(ctx context.Context, w http.ResponseWriter, v interface{}) {
+	d.writer.WriteResult(ctx, w, v)
 }
 
 func (d *DefaultPorterHandler) DecodeAndValidate(w http.ResponseWriter, r *http.Request, v interface{}) bool {
@@ -68,6 +69,6 @@ func (d *DefaultPorterHandler) DecodeAndValidateNoWrite(r *http.Request, v inter
 	return d.decoderValidator.DecodeAndValidateNoWrite(r, v)
 }
 
-func IgnoreAPIError(w http.ResponseWriter, err apierrors.RequestError) {
+func IgnoreAPIError(ctx context.Context, w http.ResponseWriter, err apierrors.RequestError) {
 	return
 }

+ 3 - 2
api/server/handlers/helmrepo/get.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -16,7 +17,7 @@ type HelmRepoGetHandler struct {
 }
 
 func NewHelmRepoGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *HelmRepoGetHandler {
 	return &HelmRepoGetHandler{
@@ -28,5 +29,5 @@ func NewHelmRepoGetHandler(
 func (c *HelmRepoGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	helmRepo, _ := r.Context().Value(types.HelmRepoScope).(*models.HelmRepo)
 
-	c.WriteResult(w, helmRepo.ToHelmRepoType())
+	c.WriteResult(r.Context(), w, helmRepo.ToHelmRepoType())
 }

+ 3 - 2
api/server/handlers/infra/get.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -16,7 +17,7 @@ type InfraGetHandler struct {
 }
 
 func NewInfraGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *InfraGetHandler {
 	return &InfraGetHandler{
@@ -28,5 +29,5 @@ func NewInfraGetHandler(
 func (c *InfraGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	infra, _ := r.Context().Value(types.InfraScope).(*models.Infra)
 
-	c.WriteResult(w, infra.ToInfraType())
+	c.WriteResult(r.Context(), w, infra.ToInfraType())
 }

+ 3 - 2
api/server/handlers/invite/get.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -16,7 +17,7 @@ type InviteGetHandler struct {
 }
 
 func NewInviteGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *InviteGetHandler {
 	return &InviteGetHandler{
@@ -28,5 +29,5 @@ func NewInviteGetHandler(
 func (c *InviteGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	invite, _ := r.Context().Value(types.InviteScope).(*models.Invite)
 
-	c.WriteResult(w, invite.ToInviteType())
+	c.WriteResult(r.Context(), w, invite.ToInviteType())
 }

+ 26 - 0
api/server/handlers/metadata/get.go

@@ -0,0 +1,26 @@
+package metadata
+
+import (
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
+)
+
+type MetadataGetHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewMetadataGetHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *MetadataGetHandler {
+	return &MetadataGetHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (v *MetadataGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	v.WriteResult(r.Context(), w, v.Config().Metadata)
+}

+ 4 - 3
api/server/handlers/project/create.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -16,7 +17,7 @@ type ProjectCreateHandler struct {
 }
 
 func NewProjectCreateHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *ProjectCreateHandler {
@@ -45,11 +46,11 @@ func (p *ProjectCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	proj, err = CreateProjectWithUser(p.Repo().Project(), proj, user)
 
 	if err != nil {
-		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		p.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	p.WriteResult(w, proj.ToProjectType())
+	p.WriteResult(r.Context(), w, proj.ToProjectType())
 }
 
 func CreateProjectWithUser(

+ 3 - 2
api/server/handlers/project/get.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -14,7 +15,7 @@ type ProjectGetHandler struct {
 }
 
 func NewProjectGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ProjectGetHandler {
 	return &ProjectGetHandler{
@@ -25,5 +26,5 @@ func NewProjectGetHandler(
 func (p *ProjectGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 
-	p.WriteResult(w, proj.ToProjectType())
+	p.WriteResult(r.Context(), w, proj.ToProjectType())
 }

+ 6 - 4
api/server/handlers/project/get_policy.go

@@ -1,9 +1,11 @@
 package project
 
 import (
+	"net/http"
+
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"net/http"
+	"github.com/porter-dev/porter/api/server/shared/config"
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -16,7 +18,7 @@ type ProjectGetPolicyHandler struct {
 }
 
 func NewProjectGetPolicyHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ProjectGetPolicyHandler {
 	return &ProjectGetPolicyHandler{
@@ -33,10 +35,10 @@ func (p *ProjectGetPolicyHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	policyDocs, err := policyDocLoader.LoadPolicyDocuments(user.ID, proj.ID)
 
 	if err != nil {
-		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		p.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 	}
 
 	var res types.GetProjectPolicyResponse = policyDocs
 
-	p.WriteResult(w, res)
+	p.WriteResult(r.Context(), w, res)
 }

+ 4 - 3
api/server/handlers/project/list.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -15,7 +16,7 @@ type ProjectListHandler struct {
 }
 
 func NewProjectListHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ProjectListHandler {
 	return &ProjectListHandler{
@@ -31,7 +32,7 @@ func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	projects, err := p.Config().Repo.Project().ListProjectsByUserID(user.ID)
 
 	if err != nil {
-		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		p.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -41,5 +42,5 @@ func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		res[i] = proj.ToProjectType()
 	}
 
-	p.WriteResult(w, res)
+	p.WriteResult(r.Context(), w, res)
 }

+ 4 - 3
api/server/handlers/project/list_infra.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -15,7 +16,7 @@ type ProjectListInfraHandler struct {
 }
 
 func NewProjectListInfraHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ProjectListInfraHandler {
 	return &ProjectListInfraHandler{
@@ -29,7 +30,7 @@ func (p *ProjectListInfraHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	infras, err := p.Repo().Infra().ListInfrasByProjectID(proj.ID)
 
 	if err != nil {
-		p.HandleAPIError(w, apierrors.NewErrInternal(err))
+		p.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 	}
 
 	infraList := make([]*types.Infra, 0)
@@ -40,5 +41,5 @@ func (p *ProjectListInfraHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 
 	var res types.ListProjectInfraResponse = infraList
 
-	p.WriteResult(w, res)
+	p.WriteResult(r.Context(), w, res)
 }

+ 3 - 2
api/server/handlers/registry/get.go

@@ -6,6 +6,7 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -16,7 +17,7 @@ type RegistryGetHandler struct {
 }
 
 func NewRegistryGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *RegistryGetHandler {
 	return &RegistryGetHandler{
@@ -28,5 +29,5 @@ func NewRegistryGetHandler(
 func (c *RegistryGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	registry, _ := r.Context().Value(types.RegistryScope).(*models.Registry)
 
-	c.WriteResult(w, registry.ToRegistryType())
+	c.WriteResult(r.Context(), w, registry.ToRegistryType())
 }

+ 4 - 3
api/server/handlers/release/get.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/templater/parser"
@@ -19,7 +20,7 @@ type ReleaseGetHandler struct {
 }
 
 func NewReleaseGetHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *ReleaseGetHandler {
 	return &ReleaseGetHandler{
@@ -52,7 +53,7 @@ func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	dynClient, err := c.GetDynamicClient(r, cluster)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 	}
 
 	parserDef := &parser.ClientConfigDefault{
@@ -69,5 +70,5 @@ func (c *ReleaseGetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		res.Form = form
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 8 - 7
api/server/handlers/user/cli_login.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/models"
@@ -20,7 +21,7 @@ type CLILoginHandler struct {
 }
 
 func NewCLILoginHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *CLILoginHandler {
@@ -45,7 +46,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		err = fmt.Errorf("CLI token creation failed: %s", err.Error())
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -53,7 +54,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		err = fmt.Errorf("CLI token encoding failed: %s", err.Error())
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -62,7 +63,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if err != nil {
 		err = fmt.Errorf("CLI random code generation failed: %s", err.Error())
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -78,7 +79,7 @@ func (c *CLILoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	authCode, err = c.Repo().AuthCode().CreateAuthCode(authCode)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -90,7 +91,7 @@ type CLILoginExchangeHandler struct {
 }
 
 func NewCLILoginExchangeHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *CLILoginExchangeHandler {
@@ -120,5 +121,5 @@ func (c *CLILoginExchangeHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 		Token: authCode.Token,
 	}
 
-	c.WriteResult(w, res)
+	c.WriteResult(r.Context(), w, res)
 }

+ 7 - 6
api/server/handlers/user/create.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
@@ -19,7 +20,7 @@ type UserCreateHandler struct {
 }
 
 func NewUserCreateHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserCreateHandler {
@@ -47,7 +48,7 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 
 	if doesExist {
 		err := fmt.Errorf("email already taken")
-		u.HandleAPIError(w, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
 
@@ -55,7 +56,7 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	hashedPw, err := bcrypt.GenerateFromPassword([]byte(user.Password), 8)
 
 	if err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -65,17 +66,17 @@ func (u *UserCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, err = u.Repo().User().CreateUser(user)
 
 	if err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// save the user as authenticated in the session
 	if err := authn.SaveUserAuthenticated(w, r, u.Config(), user); err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	u.WriteResult(w, user.ToUserType())
+	u.WriteResult(r.Context(), w, user.ToUserType())
 }
 
 func doesUserExist(userRepo repository.UserRepository, user *models.User) bool {

+ 3 - 2
api/server/handlers/user/current.go

@@ -5,6 +5,7 @@ import (
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -14,7 +15,7 @@ type UserGetCurrentHandler struct {
 }
 
 func NewUserGetCurrentHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *UserGetCurrentHandler {
 	return &UserGetCurrentHandler{
@@ -25,5 +26,5 @@ func NewUserGetCurrentHandler(
 func (a *UserGetCurrentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
-	a.WriteResult(w, user.ToUserType())
+	a.WriteResult(r.Context(), w, user.ToUserType())
 }

+ 5 - 4
api/server/handlers/user/delete.go

@@ -7,6 +7,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 )
@@ -16,7 +17,7 @@ type UserDeleteHandler struct {
 }
 
 func NewUserDeleteHandler(
-	config *shared.Config,
+	config *config.Config,
 	writer shared.ResultWriter,
 ) *UserDeleteHandler {
 	return &UserDeleteHandler{
@@ -30,15 +31,15 @@ func (u *UserDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, err := u.Repo().User().DeleteUser(user)
 
 	if err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
 	// set the user as unauthenticated in the session
 	if err := authn.SaveUserUnauthenticated(w, r, u.Config()); err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	u.WriteResult(w, user.ToUserType())
+	u.WriteResult(r.Context(), w, user.ToUserType())
 }

+ 14 - 6
api/server/handlers/user/email_verify.go

@@ -8,6 +8,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -18,7 +19,7 @@ type VerifyEmailInitiateHandler struct {
 }
 
 func NewVerifyEmailInitiateHandler(
-	config *shared.Config,
+	config *config.Config,
 ) *VerifyEmailInitiateHandler {
 	return &VerifyEmailInitiateHandler{
 		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
@@ -28,9 +29,15 @@ func NewVerifyEmailInitiateHandler(
 func (v *VerifyEmailInitiateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
-	pwReset, rawToken, err := CreatePWResetTokenForEmail(v.Repo().PWResetToken(), v.HandleAPIError, w, &types.InitiateResetUserPasswordRequest{
-		Email: user.Email,
-	})
+	pwReset, rawToken, err := CreatePWResetTokenForEmail(
+		r.Context(),
+		v.Repo().PWResetToken(),
+		v.HandleAPIError,
+		w,
+		&types.InitiateResetUserPasswordRequest{
+			Email: user.Email,
+		},
+	)
 
 	if err != nil {
 		return
@@ -49,7 +56,7 @@ func (v *VerifyEmailInitiateHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	)
 
 	if err != nil {
-		v.HandleAPIError(w, apierrors.NewErrInternal(err))
+		v.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 }
@@ -59,7 +66,7 @@ type VerifyEmailFinalizeHandler struct {
 }
 
 func NewVerifyEmailFinalizeHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 ) *VerifyEmailFinalizeHandler {
 	return &VerifyEmailFinalizeHandler{
@@ -78,6 +85,7 @@ func (v *VerifyEmailFinalizeHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 
 	token, err := VerifyToken(
+		r.Context(),
 		v.Repo().PWResetToken(),
 		handlers.IgnoreAPIError,
 		w,

+ 2 - 0
api/server/handlers/user/email_verify_test.go

@@ -1,6 +1,7 @@
 package user_test
 
 import (
+	"context"
 	"fmt"
 	"net/url"
 	"testing"
@@ -62,6 +63,7 @@ func TestEmailVerifyFinalizeSuccessful(t *testing.T) {
 
 	// create a token in the DB to use for testing
 	pwReset, rawToken, err := user.CreatePWResetTokenForEmail(
+		context.Background(),
 		config.Repo.PWResetToken(),
 		handlers.IgnoreAPIError,
 		nil,

+ 7 - 6
api/server/handlers/user/login.go

@@ -9,6 +9,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"golang.org/x/crypto/bcrypt"
 	"gorm.io/gorm"
@@ -19,7 +20,7 @@ type UserLoginHandler struct {
 }
 
 func NewUserLoginHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserLoginHandler {
@@ -43,25 +44,25 @@ func (u *UserLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	// case on user not existing, send forbidden error if not exist
 	if err != nil {
 		if targetErr := gorm.ErrRecordNotFound; errors.Is(err, targetErr) {
-			u.HandleAPIError(w, apierrors.NewErrForbidden(err))
+			u.HandleAPIError(r.Context(), w, apierrors.NewErrForbidden(err))
 			return
 		} else {
-			u.HandleAPIError(w, apierrors.NewErrInternal(err))
+			u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 			return
 		}
 	}
 
 	if err := bcrypt.CompareHashAndPassword([]byte(storedUser.Password), []byte(request.Password)); err != nil {
 		reqErr := apierrors.NewErrPassThroughToClient(fmt.Errorf("incorrect password"), http.StatusUnauthorized)
-		u.HandleAPIError(w, reqErr)
+		u.HandleAPIError(r.Context(), w, reqErr)
 		return
 	}
 
 	// save the user as authenticated in the session
 	if err := authn.SaveUserAuthenticated(w, r, u.Config(), storedUser); err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
-	u.WriteResult(w, storedUser.ToUserType())
+	u.WriteResult(r.Context(), w, storedUser.ToUserType())
 }

+ 3 - 3
api/server/handlers/user/logout.go

@@ -5,8 +5,8 @@ import (
 
 	"github.com/porter-dev/porter/api/server/authn"
 	"github.com/porter-dev/porter/api/server/handlers"
-	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
 type UserLogoutHandler struct {
@@ -14,7 +14,7 @@ type UserLogoutHandler struct {
 }
 
 func NewUserLogoutHandler(
-	config *shared.Config,
+	config *config.Config,
 ) *UserLogoutHandler {
 	return &UserLogoutHandler{
 		PorterHandler: handlers.NewDefaultPorterHandler(config, nil, nil),
@@ -23,7 +23,7 @@ func NewUserLogoutHandler(
 
 func (u *UserLogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if err := authn.SaveUserUnauthenticated(w, r, u.Config()); err != nil {
-		u.HandleAPIError(w, apierrors.NewErrInternal(err))
+		u.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 	}
 
 	return

+ 34 - 22
api/server/handlers/user/pw_reset.go

@@ -1,6 +1,7 @@
 package user
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -9,6 +10,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -23,7 +25,7 @@ type UserPasswordInitiateResetHandler struct {
 }
 
 func NewUserPasswordInitiateResetHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordInitiateResetHandler {
@@ -48,7 +50,7 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 		w.WriteHeader(http.StatusOK)
 		return
 	} else if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -62,7 +64,7 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 		)
 
 		if err != nil {
-			c.HandleAPIError(w, apierrors.NewErrInternal(err))
+			c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 			return
 		}
 
@@ -70,7 +72,13 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 		return
 	}
 
-	pwReset, rawToken, err := CreatePWResetTokenForEmail(c.Repo().PWResetToken(), c.HandleAPIError, w, request)
+	pwReset, rawToken, err := CreatePWResetTokenForEmail(
+		r.Context(),
+		c.Repo().PWResetToken(),
+		c.HandleAPIError,
+		w,
+		request,
+	)
 
 	if err != nil {
 		return
@@ -90,7 +98,7 @@ func (c *UserPasswordInitiateResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -103,7 +111,7 @@ type UserPasswordVerifyResetHandler struct {
 }
 
 func NewUserPasswordVerifyResetHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordVerifyResetHandler {
@@ -122,6 +130,7 @@ func (c *UserPasswordVerifyResetHandler) ServeHTTP(w http.ResponseWriter, r *htt
 	}
 
 	VerifyToken(
+		r.Context(),
 		c.Repo().PWResetToken(),
 		c.HandleAPIError,
 		w,
@@ -135,7 +144,7 @@ type UserPasswordFinalizeResetHandler struct {
 }
 
 func NewUserPasswordFinalizeResetHandler(
-	config *shared.Config,
+	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	writer shared.ResultWriter,
 ) *UserPasswordFinalizeResetHandler {
@@ -154,6 +163,7 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	}
 
 	token, err := VerifyToken(
+		r.Context(),
 		c.Repo().PWResetToken(),
 		c.HandleAPIError,
 		w,
@@ -171,9 +181,9 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
 			err = fmt.Errorf("finalize password reset failed: email does not exist")
-			c.HandleAPIError(w, apierrors.NewErrForbidden(err))
+			c.HandleAPIError(r.Context(), w, apierrors.NewErrForbidden(err))
 		} else {
-			c.HandleAPIError(w, apierrors.NewErrInternal(err))
+			c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		}
 
 		return
@@ -182,7 +192,7 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	hashedPW, err := bcrypt.GenerateFromPassword([]byte(request.NewPassword), 8)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -191,7 +201,7 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	user, err = c.Repo().User().UpdateUser(user)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -201,7 +211,7 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 	_, err = c.Repo().PWResetToken().UpdatePWResetToken(token)
 
 	if err != nil {
-		c.HandleAPIError(w, apierrors.NewErrInternal(err))
+		c.HandleAPIError(r.Context(), w, apierrors.NewErrInternal(err))
 		return
 	}
 
@@ -210,8 +220,9 @@ func (c *UserPasswordFinalizeResetHandler) ServeHTTP(w http.ResponseWriter, r *h
 }
 
 func VerifyToken(
+	ctx context.Context,
 	pwResetRepo repository.PWResetTokenRepository,
-	handleErr func(w http.ResponseWriter, apiErr apierrors.RequestError),
+	handleErr func(ctx context.Context, w http.ResponseWriter, apiErr apierrors.RequestError),
 	w http.ResponseWriter,
 	request *types.VerifyTokenFinalizeRequest,
 	email string,
@@ -221,10 +232,10 @@ func VerifyToken(
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
 			err = fmt.Errorf("verify token failed: token does not exist")
-			handleErr(w, apierrors.NewErrForbidden(err))
+			handleErr(ctx, w, apierrors.NewErrForbidden(err))
 			return nil, err
 		} else {
-			handleErr(w, apierrors.NewErrInternal(err))
+			handleErr(ctx, w, apierrors.NewErrInternal(err))
 		}
 
 		return nil, err
@@ -233,7 +244,7 @@ func VerifyToken(
 	// make sure the token is still valid and has not expired
 	if !token.IsValid || token.IsExpired() {
 		err = fmt.Errorf("verify token failed: expired %t, valid %t", token.IsExpired(), token.IsValid)
-		handleErr(w, apierrors.NewErrForbidden(err))
+		handleErr(ctx, w, apierrors.NewErrForbidden(err))
 
 		return nil, err
 	}
@@ -241,7 +252,7 @@ func VerifyToken(
 	// check that the email matches
 	if token.Email != email {
 		err = fmt.Errorf("verify token failed: token email does not match request email")
-		handleErr(w, apierrors.NewErrForbidden(err))
+		handleErr(ctx, w, apierrors.NewErrForbidden(err))
 
 		return nil, err
 	}
@@ -249,7 +260,7 @@ func VerifyToken(
 	// make sure the token is correct
 	if err := bcrypt.CompareHashAndPassword([]byte(token.Token), []byte(request.Token)); err != nil {
 		err = fmt.Errorf("verify token failed: %s", err)
-		handleErr(w, apierrors.NewErrForbidden(err))
+		handleErr(ctx, w, apierrors.NewErrForbidden(err))
 
 		return nil, err
 	}
@@ -258,8 +269,9 @@ func VerifyToken(
 }
 
 func CreatePWResetTokenForEmail(
+	ctx context.Context,
 	pwResetRepo repository.PWResetTokenRepository,
-	handleErr func(w http.ResponseWriter, apiErr apierrors.RequestError),
+	handleErr func(ctx context.Context, w http.ResponseWriter, apiErr apierrors.RequestError),
 	w http.ResponseWriter,
 	request *types.InitiateResetUserPasswordRequest,
 ) (*models.PWResetToken, string, error) {
@@ -269,14 +281,14 @@ func CreatePWResetTokenForEmail(
 	rawToken, err := random.StringWithCharset(32, "")
 
 	if err != nil {
-		handleErr(w, apierrors.NewErrInternal(err))
+		handleErr(ctx, w, apierrors.NewErrInternal(err))
 		return nil, "", err
 	}
 
 	hashedToken, err := bcrypt.GenerateFromPassword([]byte(rawToken), 8)
 
 	if err != nil {
-		handleErr(w, apierrors.NewErrInternal(err))
+		handleErr(ctx, w, apierrors.NewErrInternal(err))
 		return nil, "", err
 	}
 
@@ -291,7 +303,7 @@ func CreatePWResetTokenForEmail(
 	pwReset, err = pwResetRepo.CreatePWResetToken(pwReset)
 
 	if err != nil {
-		handleErr(w, apierrors.NewErrInternal(err))
+		handleErr(ctx, w, apierrors.NewErrInternal(err))
 		return nil, "", err
 	}
 

+ 8 - 7
api/server/router/base.go

@@ -2,9 +2,10 @@ package router
 
 import (
 	"github.com/go-chi/chi"
-	"github.com/porter-dev/porter/api/server/handlers/capabilities"
+	"github.com/porter-dev/porter/api/server/handlers/metadata"
 	"github.com/porter-dev/porter/api/server/handlers/user"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -17,7 +18,7 @@ func NewBaseRegisterer(children ...*Registerer) *Registerer {
 
 func GetBaseRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -25,25 +26,25 @@ func GetBaseRoutes(
 	routes := make([]*Route, 0)
 
 	// GET /api/capabilities -> user.NewUserCreateHandler
-	getCapabilitiesEndpoint := factory.NewAPIEndpoint(
+	getMetadataEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent:       basePath,
-				RelativePath: "/capabilities",
+				RelativePath: "/metadata",
 			},
 		},
 	)
 
-	getCapabilitiesHandler := capabilities.NewCapabilitiesGetHandler(
+	getMetadataHandler := metadata.NewMetadataGetHandler(
 		config,
 		factory.GetResultWriter(),
 	)
 
 	routes = append(routes, &Route{
-		Endpoint: getCapabilitiesEndpoint,
-		Handler:  getCapabilitiesHandler,
+		Endpoint: getMetadataEndpoint,
+		Handler:  getMetadataHandler,
 		Router:   r,
 	})
 

+ 3 - 2
api/server/router/cluster.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewClusterScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetClusterScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetClusterScopedRoutes(
 
 func getClusterRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/git_installation.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/gitinstallation"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewGitInstallationScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetGitInstallationScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetGitInstallationScopedRoutes(
 
 func getGitInstallationRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/helm_repo.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/helmrepo"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewHelmRepoScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetHelmRepoScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetHelmRepoScopedRoutes(
 
 func getHelmRepoRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/infra.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/infra"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewInfraScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetInfraScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetInfraScopedRoutes(
 
 func getInfraRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/invite.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/invite"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewInviteScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetInviteScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetInviteScopedRoutes(
 
 func getInviteRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/namespace.go

@@ -3,6 +3,7 @@ package router
 import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -15,7 +16,7 @@ func NewNamespaceScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetNamespaceScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -37,7 +38,7 @@ func GetNamespaceScopedRoutes(
 
 func getNamespaceRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/project.go

@@ -5,6 +5,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/cluster"
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -17,7 +18,7 @@ func NewProjectScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetProjectScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -39,7 +40,7 @@ func GetProjectScopedRoutes(
 
 func getProjectRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/registry.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/registry"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewRegistryScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetRegistryScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetRegistryScopedRoutes(
 
 func getRegistryRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 3 - 2
api/server/router/release.go

@@ -4,6 +4,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/handlers/release"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -16,7 +17,7 @@ func NewReleaseScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetReleaseScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -38,7 +39,7 @@ func GetReleaseScopedRoutes(
 
 func getReleaseRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) ([]*Route, *types.Path) {

+ 4 - 3
api/server/router/router.go

@@ -8,10 +8,11 @@ import (
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz/policy"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
-func NewAPIRouter(config *shared.Config) *chi.Mux {
+func NewAPIRouter(config *config.Config) *chi.Mux {
 	r := chi.NewRouter()
 
 	// set the content type for all API endpoints
@@ -75,7 +76,7 @@ type Route struct {
 type Registerer struct {
 	GetRoutes func(
 		r chi.Router,
-		config *shared.Config,
+		config *config.Config,
 		basePath *types.Path,
 		factory shared.APIEndpointFactory,
 		children ...*Registerer,
@@ -84,7 +85,7 @@ type Registerer struct {
 	Children []*Registerer
 }
 
-func registerRoutes(config *shared.Config, routes []*Route) {
+func registerRoutes(config *config.Config, routes []*Route) {
 	// Create a new "user-scoped" factory which will create a new user-scoped request
 	// after authentication. Each subsequent http.Handler can lookup the user in context.
 	authNFactory := authn.NewAuthNFactory(config)

+ 3 - 2
api/server/router/user.go

@@ -5,6 +5,7 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers/project"
 	"github.com/porter-dev/porter/api/server/handlers/user"
 	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -17,7 +18,7 @@ func NewUserScopedRegisterer(children ...*Registerer) *Registerer {
 
 func GetUserScopedRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 	children ...*Registerer,
@@ -37,7 +38,7 @@ func GetUserScopedRoutes(
 
 func getUserRoutes(
 	r chi.Router,
-	config *shared.Config,
+	config *config.Config,
 	basePath *types.Path,
 	factory shared.APIEndpointFactory,
 ) []*Route {

+ 9 - 0
api/server/shared/apierrors/alerter/alerter.go

@@ -0,0 +1,9 @@
+package alerter
+
+import (
+	"context"
+)
+
+type Alerter interface {
+	SendAlert(ctx context.Context, err error)
+}

+ 7 - 0
api/server/shared/apierrors/alerter/noop.go

@@ -0,0 +1,7 @@
+package alerter
+
+import "context"
+
+type NoOpAlerter struct{}
+
+func (s NoOpAlerter) SendAlert(ctx context.Context, err error) {}

+ 29 - 0
api/server/shared/apierrors/alerter/sentry.go

@@ -0,0 +1,29 @@
+package alerter
+
+import (
+	"context"
+
+	"github.com/getsentry/sentry-go"
+)
+
+type SentryAlerter struct {
+	client *sentry.Client
+}
+
+func NewSentryAlerter(sentryDSN string) (*SentryAlerter, error) {
+	sentryClient, err := sentry.NewClient(sentry.ClientOptions{
+		Dsn: sentryDSN,
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &SentryAlerter{
+		client: sentryClient,
+	}, nil
+}
+
+func (s *SentryAlerter) SendAlert(ctx context.Context, err error) {
+	s.client.CaptureException(err, &sentry.EventHint{}, nil)
+}

+ 11 - 4
api/server/shared/apierrors/errors.go

@@ -1,11 +1,12 @@
 package apierrors
 
 import (
+	"context"
 	"encoding/json"
 	"net/http"
 
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
-	"github.com/porter-dev/porter/internal/logger"
 )
 
 type RequestError interface {
@@ -90,14 +91,20 @@ func (e *ErrPassThroughToClient) GetStatusCode() int {
 }
 
 func HandleAPIError(
+	ctx context.Context,
+	config *config.Config,
 	w http.ResponseWriter,
-	logger *logger.Logger,
 	err RequestError,
 ) {
+	// if the status code is internal server error, use alerter
+	if err.GetStatusCode() == http.StatusInternalServerError && config.Alerter != nil {
+		config.Alerter.SendAlert(ctx, err)
+	}
+
 	extErrorStr := err.ExternalError()
 
 	// log the internal error
-	logger.Warn().
+	config.Logger.Warn().
 		Str("internal_error", err.InternalError()).
 		Str("external_error", extErrorStr).
 		Msg("")
@@ -113,7 +120,7 @@ func HandleAPIError(
 	writerErr := json.NewEncoder(w).Encode(resp)
 
 	if writerErr != nil {
-		logger.Error().
+		config.Logger.Error().
 			Err(writerErr).
 			Msg("")
 	}

+ 3 - 3
api/server/shared/apitest/authn.go

@@ -7,7 +7,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/models"
@@ -16,7 +16,7 @@ import (
 // AuthenticateUserWithCookie uses the session store to create a cookie for a user
 func AuthenticateUserWithCookie(
 	t *testing.T,
-	config *shared.Config,
+	config *config.Config,
 	user *models.User,
 	badUserIDType bool,
 ) *http.Cookie {
@@ -58,7 +58,7 @@ func AuthenticateUserWithCookie(
 }
 
 // AuthenticateUserWithToken uses the JWT token generator to create a token for a user
-func AuthenticateUserWithToken(t *testing.T, config *shared.Config, userID uint) string {
+func AuthenticateUserWithToken(t *testing.T, config *config.Config, userID uint) string {
 	issToken, err := token.GetTokenForUser(userID)
 
 	if err != nil {

+ 7 - 7
api/server/shared/apitest/config.go

@@ -4,8 +4,8 @@ import (
 	"os"
 	"testing"
 
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/envloader"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/server/shared/config/loader"
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/logger"
@@ -17,15 +17,15 @@ type TestConfigLoader struct {
 	failingRepoMethods []string
 }
 
-func NewTestConfigLoader(canQuery bool, failingRepoMethods ...string) shared.ConfigLoader {
+func NewTestConfigLoader(canQuery bool, failingRepoMethods ...string) config.ConfigLoader {
 	return &TestConfigLoader{canQuery, failingRepoMethods}
 }
 
-func (t *TestConfigLoader) LoadConfig() (*shared.Config, error) {
+func (t *TestConfigLoader) LoadConfig() (*config.Config, error) {
 	l := logger.New(true, os.Stdout)
 	repo := test.NewRepository(t.canQuery, t.failingRepoMethods...)
 
-	envConf, err := envloader.FromEnv()
+	envConf, err := loader.FromEnv()
 
 	if err != nil {
 		return nil, err
@@ -48,7 +48,7 @@ func (t *TestConfigLoader) LoadConfig() (*shared.Config, error) {
 
 	notifier := NewFakeUserNotifier()
 
-	return &shared.Config{
+	return &config.Config{
 		Logger:       l,
 		Repo:         repo,
 		Store:        store,
@@ -58,7 +58,7 @@ func (t *TestConfigLoader) LoadConfig() (*shared.Config, error) {
 	}, nil
 }
 
-func LoadConfig(t *testing.T, failingRepoMethods ...string) *shared.Config {
+func LoadConfig(t *testing.T, failingRepoMethods ...string) *config.Config {
 	configLoader := NewTestConfigLoader(true, failingRepoMethods...)
 
 	config, err := configLoader.LoadConfig()

+ 4 - 3
api/server/shared/apitest/request.go

@@ -13,6 +13,7 @@ import (
 	"github.com/go-chi/chi"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
 func GetRequestAndRecorder(t *testing.T, method, route string, requestObj interface{}) (*http.Request, *httptest.ResponseRecorder) {
@@ -56,7 +57,7 @@ func WithURLParams(t *testing.T, req *http.Request, params map[string]string) *h
 }
 
 type failingDecoderValidator struct {
-	config *shared.Config
+	config *config.Config
 }
 
 func (f *failingDecoderValidator) DecodeAndValidate(
@@ -64,7 +65,7 @@ func (f *failingDecoderValidator) DecodeAndValidate(
 	r *http.Request,
 	v interface{},
 ) (ok bool) {
-	apierrors.HandleAPIError(w, f.config.Logger, apierrors.NewErrInternal(fmt.Errorf("fake error")))
+	apierrors.HandleAPIError(r.Context(), f.config, w, apierrors.NewErrInternal(fmt.Errorf("fake error")))
 	return false
 }
 
@@ -75,6 +76,6 @@ func (f *failingDecoderValidator) DecodeAndValidateNoWrite(
 	return fmt.Errorf("fake error")
 }
 
-func NewFailingDecoderValidator(config *shared.Config) shared.RequestDecoderValidator {
+func NewFailingDecoderValidator(config *config.Config) shared.RequestDecoderValidator {
 	return &failingDecoderValidator{config}
 }

+ 2 - 2
api/server/shared/apitest/user.go

@@ -3,12 +3,12 @@ package apitest
 import (
 	"testing"
 
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/models"
 	"golang.org/x/crypto/bcrypt"
 )
 
-func CreateTestUser(t *testing.T, config *shared.Config, verified bool) *models.User {
+func CreateTestUser(t *testing.T, config *config.Config, verified bool) *models.User {
 	hashedPw, _ := bcrypt.GenerateFromPassword([]byte("hello"), 8)
 
 	user, err := config.Repo.User().CreateUser(&models.User{

+ 10 - 3
api/server/shared/config.go → api/server/shared/config/config.go

@@ -1,9 +1,10 @@
-package shared
+package config
 
 import (
 	"time"
 
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/internal/auth/token"
 	"github.com/porter-dev/porter/internal/logger"
 	"github.com/porter-dev/porter/internal/notifier"
@@ -18,9 +19,13 @@ type Config struct {
 	// Repo implements a query repository
 	Repo repository.Repository
 
-	// Capabilities is a description object for the server capabilities, used
+	// Metadata is a description object for the server metadata, used
 	// to determine which endpoints to register
-	Capabilities *Capabilities
+	Metadata *Metadata
+
+	// Alerter sends messages to alert aggregators (like Sentry) if the
+	// error is fatal
+	Alerter alerter.Alerter
 
 	// Store implements a session store for session-based cookies
 	Store sessions.Store
@@ -96,6 +101,8 @@ type ServerConf struct {
 	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
 	SegmentClientKey           string `env:"SEGMENT_CLIENT_KEY"`
 
+	SentryDSN string `env:"SENTRY_DSN"`
+
 	ProvisionerCluster string `env:"PROVISIONER_CLUSTER"`
 	IngressCluster     string `env:"INGRESS_CLUSTER"`
 	SelfKubeconfig     string `env:"SELF_KUBECONFIG"`

+ 8 - 8
api/server/shared/envloader/envloader.go → api/server/shared/config/loader/envloader.go

@@ -1,22 +1,22 @@
-package envloader
+package loader
 
 import (
 	"fmt"
 
 	"github.com/joeshaw/envdecode"
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
 type EnvDecoderConf struct {
-	ServerConf shared.ServerConf
-	RedisConf  shared.RedisConf
-	DBConf     shared.DBConf
+	ServerConf config.ServerConf
+	RedisConf  config.RedisConf
+	DBConf     config.DBConf
 }
 
 type EnvConf struct {
-	ServerConf *shared.ServerConf
-	RedisConf  *shared.RedisConf
-	DBConf     *shared.DBConf
+	ServerConf *config.ServerConf
+	RedisConf  *config.RedisConf
+	DBConf     *config.DBConf
 }
 
 // FromEnv generates a configuration from environment variables

+ 17 - 10
api/server/shared/configloader/configloader.go → api/server/shared/config/loader/loader.go

@@ -1,8 +1,8 @@
-package configloader
+package loader
 
 import (
-	"github.com/porter-dev/porter/api/server/shared"
-	"github.com/porter-dev/porter/api/server/shared/envloader"
+	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/auth/sessionstore"
 	"github.com/porter-dev/porter/internal/auth/token"
@@ -15,18 +15,18 @@ import (
 
 type EnvConfigLoader struct{}
 
-func NewEnvLoader() shared.ConfigLoader {
+func NewEnvLoader() config.ConfigLoader {
 	return &EnvConfigLoader{}
 }
 
-func (e *EnvConfigLoader) LoadConfig() (*shared.Config, error) {
-	envConf, err := envloader.FromEnv()
+func (e *EnvConfigLoader) LoadConfig() (*config.Config, error) {
+	envConf, err := FromEnv()
 
 	if err != nil {
 		return nil, err
 	}
 
-	capabilities := shared.CapabilitiesFromConf(envConf.ServerConf)
+	metadata := config.MetadataFromConf(envConf.ServerConf)
 
 	db, err := adapter.New(envConf.DBConf)
 
@@ -66,7 +66,7 @@ func (e *EnvConfigLoader) LoadConfig() (*shared.Config, error) {
 
 	var notif notifier.UserNotifier = &notifier.EmptyUserNotifier{}
 
-	if capabilities.Email {
+	if metadata.Email {
 		notif = sendgrid.NewUserNotifier(&sendgrid.Client{
 			APIKey:                  envConf.ServerConf.SendgridAPIKey,
 			PWResetTemplateID:       envConf.ServerConf.SendgridPWResetTemplateID,
@@ -77,10 +77,17 @@ func (e *EnvConfigLoader) LoadConfig() (*shared.Config, error) {
 		})
 	}
 
-	return &shared.Config{
+	var errAlerter alerter.Alerter = alerter.NoOpAlerter{}
+
+	if envConf.ServerConf.SentryDSN != "" {
+		errAlerter, err = alerter.NewSentryAlerter(envConf.ServerConf.SentryDSN)
+	}
+
+	return &config.Config{
+		Alerter:      errAlerter,
 		Logger:       lr.NewConsole(envConf.ServerConf.Debug),
 		Repo:         repo,
-		Capabilities: capabilities,
+		Metadata:     metadata,
 		Store:        store,
 		ServerConf:   envConf.ServerConf,
 		TokenConf:    tokenConf,

+ 4 - 4
api/server/shared/capabilities.go → api/server/shared/config/metadata.go

@@ -1,6 +1,6 @@
-package shared
+package config
 
-type Capabilities struct {
+type Metadata struct {
 	Provisioning       bool `json:"provisioner"`
 	Github             bool `json:"github"`
 	BasicLogin         bool `json:"basic_login"`
@@ -11,8 +11,8 @@ type Capabilities struct {
 	Analytics          bool `json:"analytics"`
 }
 
-func CapabilitiesFromConf(sc *ServerConf) *Capabilities {
-	return &Capabilities{
+func MetadataFromConf(sc *ServerConf) *Metadata {
+	return &Metadata{
 		// TODO: case provisioning on env variables
 		Provisioning:       false,
 		Github:             hasGithubAppVars(sc),

+ 4 - 3
api/server/shared/endpoints.go

@@ -1,6 +1,7 @@
 package shared
 
 import (
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 )
 
@@ -21,9 +22,9 @@ type APIObjectEndpointFactory struct {
 	ResultWriter     ResultWriter
 }
 
-func NewAPIObjectEndpointFactory(config *Config) APIEndpointFactory {
-	decoderValidator := NewDefaultRequestDecoderValidator(config)
-	resultWriter := NewDefaultResultWriter(config)
+func NewAPIObjectEndpointFactory(conf *config.Config) APIEndpointFactory {
+	decoderValidator := NewDefaultRequestDecoderValidator(conf)
+	resultWriter := NewDefaultResultWriter(conf)
 
 	return &APIObjectEndpointFactory{
 		DecoderValidator: decoderValidator,

+ 6 - 5
api/server/shared/reader.go

@@ -5,6 +5,7 @@ import (
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/requestutils"
 )
 
@@ -14,18 +15,18 @@ type RequestDecoderValidator interface {
 }
 
 type DefaultRequestDecoderValidator struct {
-	config    *Config
+	config    *config.Config
 	validator requestutils.Validator
 	decoder   requestutils.Decoder
 }
 
 func NewDefaultRequestDecoderValidator(
-	config *Config,
+	conf *config.Config,
 ) RequestDecoderValidator {
 	validator := requestutils.NewDefaultValidator()
 	decoder := requestutils.NewDefaultDecoder()
 
-	return &DefaultRequestDecoderValidator{config, validator, decoder}
+	return &DefaultRequestDecoderValidator{conf, validator, decoder}
 }
 
 func (j *DefaultRequestDecoderValidator) DecodeAndValidate(
@@ -37,13 +38,13 @@ func (j *DefaultRequestDecoderValidator) DecodeAndValidate(
 
 	// decode the request parameters (body and query)
 	if requestErr = j.decoder.Decode(v, r); requestErr != nil {
-		apierrors.HandleAPIError(w, j.config.Logger, requestErr)
+		apierrors.HandleAPIError(r.Context(), j.config, w, requestErr)
 		return false
 	}
 
 	// validate the request object
 	if requestErr = j.validator.Validate(v); requestErr != nil {
-		apierrors.HandleAPIError(w, j.config.Logger, requestErr)
+		apierrors.HandleAPIError(r.Context(), j.config, w, requestErr)
 		return false
 	}
 

+ 8 - 6
api/server/shared/writer.go

@@ -1,30 +1,32 @@
 package shared
 
 import (
+	"context"
 	"encoding/json"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
 type ResultWriter interface {
-	WriteResult(w http.ResponseWriter, v interface{})
+	WriteResult(ctx context.Context, w http.ResponseWriter, v interface{})
 }
 
 // default generalizes response codes for common operations
 // (http.StatusOK, http.StatusCreated, etc)
 type DefaultResultWriter struct {
-	config *Config
+	config *config.Config
 }
 
-func NewDefaultResultWriter(config *Config) ResultWriter {
-	return &DefaultResultWriter{config}
+func NewDefaultResultWriter(conf *config.Config) ResultWriter {
+	return &DefaultResultWriter{conf}
 }
 
-func (j *DefaultResultWriter) WriteResult(w http.ResponseWriter, v interface{}) {
+func (j *DefaultResultWriter) WriteResult(ctx context.Context, w http.ResponseWriter, v interface{}) {
 	err := json.NewEncoder(w).Encode(v)
 
 	if err != nil {
-		apierrors.HandleAPIError(w, j.config.Logger, apierrors.NewErrInternal(err))
+		apierrors.HandleAPIError(ctx, j.config, w, apierrors.NewErrInternal(err))
 	}
 }

+ 2 - 2
cmd/app/main.go

@@ -8,7 +8,7 @@ import (
 	"os"
 
 	"github.com/porter-dev/porter/api/server/router"
-	"github.com/porter-dev/porter/api/server/shared/configloader"
+	"github.com/porter-dev/porter/api/server/shared/config/loader"
 )
 
 // Version will be linked by an ldflag during build
@@ -25,7 +25,7 @@ func main() {
 		os.Exit(0)
 	}
 
-	cl := configloader.NewEnvLoader()
+	cl := loader.NewEnvLoader()
 
 	config, err := cl.LoadConfig()
 

+ 2 - 2
docs/developing/backend-refactor.md

@@ -73,7 +73,7 @@ type [Resource][Verb]Handler struct {
 }
 
 func New[Resource][Verb]Handler(
-	config *shared.Config,
+	config *config.Config,
     // TODO: additional arguments
 ) *[Resource][Verb]Handler {
 	return &[Resource][Verb]Handler{
@@ -93,7 +93,7 @@ func (c *[Resource][Verb]Handler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 - Use helper methods attached to the `handlers.PorterHandler` interface, which implements the following:
 
 ```go
-Config() *shared.Config
+Config() *config.Config
 Repo() repository.Repository
 HandleAPIError(w http.ResponseWriter, err apierrors.RequestError)
 ```

+ 1 - 0
go.mod

@@ -19,6 +19,7 @@ require (
 	github.com/docker/docker-credential-helpers v0.6.3
 	github.com/docker/go-connections v0.4.0
 	github.com/fatih/color v1.9.0
+	github.com/getsentry/sentry-go v0.11.0
 	github.com/go-chi/chi v4.1.2+incompatible
 	github.com/go-playground/locales v0.13.0
 	github.com/go-playground/universal-translator v0.17.0

+ 72 - 0
go.sum

@@ -36,6 +36,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/AlecAivazis/survey/v2 v2.2.9 h1:LWvJtUswz/W9/zVVXELrmlvdwWcKE60ZAw0FWV9vssk=
 github.com/AlecAivazis/survey/v2 v2.2.9/go.mod h1:9DYvHgXtiXm6nCn+jXnOXLKbH+Yo9u8fAS/SduGdoPk=
+github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
@@ -64,8 +65,11 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
+github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
 github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
 github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
+github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -103,6 +107,7 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
 github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
+github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
 github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
@@ -110,6 +115,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
 github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
+github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
 github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
@@ -153,6 +159,7 @@ github.com/aws/aws-sdk-go v1.35.4/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
 github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
 github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 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=
@@ -211,6 +218,7 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I
 github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
 github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
 github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59 h1:qWj4qVYZ95vLWwqyNJCQg7rDsG5wPdze0UaPolH7DUk=
 github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
 github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
@@ -265,9 +273,11 @@ github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xb
 github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM=
 github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
+github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4=
 github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
@@ -307,6 +317,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
 github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
@@ -322,17 +333,20 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
 github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
+github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
 github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
 github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
+github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
 github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
 github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@@ -345,13 +359,19 @@ github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui72
 github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7 h1:LofdAjjjqCSXMwLGgOgnE+rdPuvX9DxCqaHwKy7i/ko=
 github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
+github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
+github.com/getsentry/sentry-go v0.11.0 h1:qro8uttJGvNAMr5CLcFI9CHR0aDzXl0Vs3Pmw/oTPg8=
+github.com/getsentry/sentry-go v0.11.0/go.mod h1:KBQIxiZAetw62Cj8Ri964vAEWVdgfaUCn30Q3bCvANo=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
 github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
 github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
 github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
@@ -370,6 +390,7 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc=
 github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
+github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
 github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
 github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
 github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -449,6 +470,9 @@ github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYi
 github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
+github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
+github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
 github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
 github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
@@ -502,6 +526,7 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk=
+github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
 github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
 github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
 github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -577,6 +602,7 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
 github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
@@ -628,9 +654,15 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
 github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
 github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
+github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
+github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
+github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
+github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
 github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3 h1:l7vogWrq+zj8v5t/G69/eT13nAGs2H7cq+CI2nlnKdk=
 github.com/itchyny/astgen-go v0.0.0-20210113000433-0da0671862a3/go.mod h1:296z3W7Xsrp2mlIY88ruDKscuvrkL6zXCNRtaYVshzw=
 github.com/itchyny/go-flags v1.5.0/go.mod h1:lenkYuCobuxLBAd/HGFE4LRoW8D3B6iXRQfWYJ+MNbA=
@@ -723,6 +755,7 @@ github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBv
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
 github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -730,6 +763,12 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
+github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
+github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
+github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
+github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
+github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
@@ -738,6 +777,9 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -757,6 +799,8 @@ github.com/kris-nova/logger v0.0.0-20181127235838-fd0d87064b06 h1:vN4d3jSss3ExzU
 github.com/kris-nova/logger v0.0.0-20181127235838-fd0d87064b06/go.mod h1:++9BgZujZd4v0ZTZCb5iPsaomXdZWyxotIAh1IiDm44=
 github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b h1:xYEM2oBUhBEhQjrV+KJ9lEWDWYZoNVZUaBF++Wyljq4=
 github.com/kris-nova/lolgopher v0.0.0-20180921204813-313b3abb0d9b/go.mod h1:V0HF/ZBlN86HqewcDC/cVxMmYDiRukWjSrgKLUAn9Js=
+github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
 github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
 github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -818,13 +862,16 @@ github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71
 github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
 github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
 github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
+github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
 github.com/microcosm-cc/bluemonday v1.0.6/go.mod h1:HOT/6NaBlR0f9XlxD3zolN6Z3N8Lp4pvhp+jLS5ihnI=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@@ -871,6 +918,7 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/
 github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
 github.com/muesli/reflow v0.2.0/go.mod h1:qT22vjVmM9MIUeLgsVYe/Ye7eZlbv9dZjL3dVhUqLX8=
 github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
 github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
@@ -903,6 +951,7 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -956,6 +1005,7 @@ github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoU
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
 github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -1031,10 +1081,12 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sabhiram/go-gitignore v0.0.0-20201211074657-223ce5d391b0 h1:4Q/TASkyjpqyR5DL5+6c2FGSDpHM5bTMSspcXr7J6R8=
 github.com/sabhiram/go-gitignore v0.0.0-20201211074657-223ce5d391b0/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
 github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
 github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8=
 github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM=
@@ -1131,10 +1183,17 @@ github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKw
 github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
 github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
@@ -1154,6 +1213,10 @@ github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6Ut
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
+github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
+github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
+github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1222,6 +1285,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -1284,6 +1348,7 @@ golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73r
 golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -1451,6 +1516,7 @@ golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181221001348-537d06c36207/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -1458,6 +1524,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -1638,14 +1705,18 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
 gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
 gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
 gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
 gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
 gopkg.in/segmentio/analytics-go.v3 v3.1.0 h1:UzxH1uaGZRpMKDhJyBz0pexz6yUoBU3x8bJsRk/HV6U=
@@ -1671,6 +1742,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 2 - 2
internal/adapter/gorm.go

@@ -4,14 +4,14 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"gorm.io/driver/postgres"
 	"gorm.io/driver/sqlite"
 	"gorm.io/gorm"
 )
 
 // New returns a new gorm database instance
-func New(conf *shared.DBConf) (*gorm.DB, error) {
+func New(conf *config.DBConf) (*gorm.DB, error) {
 	if conf.SQLLite {
 		// we add DisableForeignKeyConstraintWhenMigrating since our sqlite does
 		// not support foreign key constraints

+ 1 - 1
internal/adapter/redis.go

@@ -5,7 +5,7 @@ import (
 	"fmt"
 
 	redis "github.com/go-redis/redis/v8"
-	"github.com/porter-dev/porter/internal/config"
+	"github.com/porter-dev/porter/api/server/shared/config"
 )
 
 // NewRedisClient returns a new redis client instance

+ 1 - 2
internal/auth/sessionstore/sessionstore.go

@@ -8,11 +8,10 @@ import (
 	"strings"
 	"time"
 
-	"github.com/porter-dev/porter/internal/config"
-
 	"github.com/gorilla/securecookie"
 	"github.com/gorilla/sessions"
 
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/repository"
 

+ 0 - 117
internal/config/config.go

@@ -1,117 +0,0 @@
-package config
-
-import (
-	"log"
-	"time"
-
-	"github.com/joeshaw/envdecode"
-)
-
-// Conf is the configuration for the Go server
-type Conf struct {
-	Debug        bool `env:"DEBUG,default=false"`
-	Server       ServerConf
-	Db           DBConf
-	K8s          K8sConf
-	Redis        RedisConf
-	Capabilities CapConf
-}
-
-// ServerConf is the server configuration
-type ServerConf struct {
-	ServerURL            string        `env:"SERVER_URL,default=http://localhost:8080"`
-	Port                 int           `env:"SERVER_PORT,default=8080"`
-	StaticFilePath       string        `env:"STATIC_FILE_PATH,default=/porter/static"`
-	CookieName           string        `env:"COOKIE_NAME,default=porter"`
-	CookieSecrets        []string      `env:"COOKIE_SECRETS,default=random_hash_key_;random_block_key"`
-	TokenGeneratorSecret string        `env:"TOKEN_GENERATOR_SECRET,default=secret"`
-	TimeoutRead          time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
-	TimeoutWrite         time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
-	TimeoutIdle          time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
-	IsLocal              bool          `env:"IS_LOCAL,default=false"`
-	IsTesting            bool          `env:"IS_TESTING,default=false"`
-	AppRootDomain        string        `env:"APP_ROOT_DOMAIN,default=porter.run"`
-
-	DefaultApplicationHelmRepoURL string `env:"HELM_APP_REPO_URL,default=https://charts.dev.getporter.dev"`
-	DefaultAddonHelmRepoURL       string `env:"HELM_ADD_ON_REPO_URL,default=https://chart-addons.dev.getporter.dev"`
-
-	BasicLoginEnabled bool `env:"BASIC_LOGIN_ENABLED,default=true"`
-
-	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
-	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
-	GithubLoginEnabled bool   `env:"GITHUB_LOGIN_ENABLED,default=true"`
-
-	GithubAppClientID      string `env:"GITHUB_APP_CLIENT_ID"`
-	GithubAppClientSecret  string `env:"GITHUB_APP_CLIENT_SECRET"`
-	GithubAppName          string `env:"GITHUB_APP_NAME"`
-	GithubAppWebhookSecret string `env:"GITHUB_APP_WEBHOOK_SECRET"`
-	GithubAppID            string `env:"GITHUB_APP_ID"`
-	GithubAppSecretPath    string `env:"GITHUB_APP_SECRET_PATH"`
-
-	GoogleClientID         string `env:"GOOGLE_CLIENT_ID"`
-	GoogleClientSecret     string `env:"GOOGLE_CLIENT_SECRET"`
-	GoogleRestrictedDomain string `env:"GOOGLE_RESTRICTED_DOMAIN"`
-
-	SendgridAPIKey                  string `env:"SENDGRID_API_KEY"`
-	SendgridPWResetTemplateID       string `env:"SENDGRID_PW_RESET_TEMPLATE_ID"`
-	SendgridPWGHTemplateID          string `env:"SENDGRID_PW_GH_TEMPLATE_ID"`
-	SendgridVerifyEmailTemplateID   string `env:"SENDGRID_VERIFY_EMAIL_TEMPLATE_ID"`
-	SendgridProjectInviteTemplateID string `env:"SENDGRID_INVITE_TEMPLATE_ID"`
-	SendgridSenderEmail             string `env:"SENDGRID_SENDER_EMAIL"`
-
-	SlackClientID     string `env:"SLACK_CLIENT_ID"`
-	SlackClientSecret string `env:"SLACK_CLIENT_SECRET"`
-
-	DOClientID                 string `env:"DO_CLIENT_ID"`
-	DOClientSecret             string `env:"DO_CLIENT_SECRET"`
-	ProvisionerImageTag        string `env:"PROV_IMAGE_TAG,default=latest"`
-	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
-	SegmentClientKey           string `env:"SEGMENT_CLIENT_KEY"`
-
-	ProvisionerCluster string `env:"PROVISIONER_CLUSTER"`
-	IngressCluster     string `env:"INGRESS_CLUSTER"`
-	SelfKubeconfig     string `env:"SELF_KUBECONFIG"`
-}
-
-// DBConf is the database configuration: if generated from environment variables,
-// it assumes the default docker-compose configuration is used
-type DBConf struct {
-	// EncryptionKey is the key to use for sensitive values that are encrypted at rest
-	EncryptionKey string `env:"ENCRYPTION_KEY,default=__random_strong_encryption_key__"`
-
-	Host     string `env:"DB_HOST,default=postgres"`
-	Port     int    `env:"DB_PORT,default=5432"`
-	Username string `env:"DB_USER,default=porter"`
-	Password string `env:"DB_PASS,default=porter"`
-	DbName   string `env:"DB_NAME,default=porter"`
-	ForceSSL bool   `env:"DB_FORCE_SSL,default=false"`
-
-	SQLLite     bool   `env:"SQL_LITE,default=false"`
-	SQLLitePath string `env:"SQL_LITE_PATH,default=/porter/porter.db"`
-}
-
-// K8sConf is the global configuration for the k8s agents
-type K8sConf struct {
-	IsTesting bool `env:"K8S_IS_TESTING,default=false"`
-}
-
-type CapConf struct {
-	Provisioner bool `env:"PROVISIONER_ENABLED,default=true"`
-	Github      bool `env:"GITHUB_ENABLED,default=true"`
-	Google      bool
-}
-
-// FromEnv generates a configuration from environment variables
-func FromEnv() *Conf {
-	var c Conf
-
-	if err := envdecode.StrictDecode(&c); err != nil {
-		log.Fatalf("Failed to decode server conf: %s", err)
-	}
-
-	if c.Server.GoogleClientID != "" && c.Server.GoogleClientSecret != "" {
-		c.Capabilities.Google = true
-	}
-
-	return &c
-}

+ 0 - 13
internal/config/redis.go

@@ -1,13 +0,0 @@
-package config
-
-// RedisConf is the redis config required for the provisioner container
-type RedisConf struct {
-	// if redis should be used
-	Enabled bool `env:"REDIS_ENABLED,default=true"`
-
-	Host     string `env:"REDIS_HOST,default=redis"`
-	Port     string `env:"REDIS_PORT,default=6379"`
-	Username string `env:"REDIS_USER"`
-	Password string `env:"REDIS_PASS"`
-	DB       int    `env:"REDIS_DB,default=0"`
-}

+ 1 - 2
internal/kubernetes/agent.go

@@ -12,6 +12,7 @@ import (
 	"io/ioutil"
 	"strings"
 
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
@@ -47,8 +48,6 @@ import (
 	"k8s.io/client-go/tools/cache"
 	"k8s.io/client-go/tools/remotecommand"
 
-	"github.com/porter-dev/porter/internal/config"
-
 	rspb "helm.sh/helm/v3/pkg/release"
 )
 

+ 1 - 2
internal/kubernetes/provisioner/provisioner.go

@@ -7,6 +7,7 @@ import (
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/eks"
@@ -17,8 +18,6 @@ import (
 
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/gcp/gke"
-
-	"github.com/porter-dev/porter/internal/config"
 )
 
 // InfraOption is a type of infrastructure that can be provisioned

+ 2 - 2
internal/repository/gorm/helpers_test.go

@@ -5,7 +5,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/adapter"
 	"github.com/porter-dev/porter/internal/models"
@@ -39,7 +39,7 @@ type tester struct {
 func setupTestEnv(tester *tester, t *testing.T) {
 	t.Helper()
 
-	db, err := adapter.New(&shared.DBConf{
+	db, err := adapter.New(&config.DBConf{
 		EncryptionKey: "__random_strong_encryption_key__",
 		SQLLite:       true,
 		SQLLitePath:   tester.dbFileName,

+ 0 - 1
server/api/api.go

@@ -28,7 +28,6 @@ import (
 	"helm.sh/helm/v3/pkg/storage"
 
 	"github.com/porter-dev/porter/internal/analytics"
-	"github.com/porter-dev/porter/internal/config"
 )
 
 // TestAgents are the k8s agents used for testing