feature(api,release): prepare for next major release and switch to requiring content-type set to JSON for all incoming requests and expose more CORS environment variables
All checks were successful
/ build (push) Successful in 5m3s
All checks were successful
/ build (push) Successful in 5m3s
- Switched to enforce JSON as Content-Type for all incoming requests - Switched to properly respond with JSON on page not found or method not allowed - Renamed CORS_ALLOW_ORIGIN to CORS_ALLOW_ORIGINS - Added CORS_ALLOW_CREDENTIALS which defaults to true - Added CORS_EXPOSE_HEADERS which defaults to * - Overhauled package visibility for server module
This commit is contained in:
parent
1d79258670
commit
1fc3818d3c
24 changed files with 151 additions and 101 deletions
|
@ -3,9 +3,9 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
env:
|
env:
|
||||||
VERSION_MAJOR: 2
|
VERSION_MAJOR: 3
|
||||||
VERSION_MINOR: 0
|
VERSION_MINOR: 0
|
||||||
VERSION_PATCH: 2
|
VERSION_PATCH: 0
|
||||||
IMAGE_TAG: varakh/upda
|
IMAGE_TAG: varakh/upda
|
||||||
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda
|
IMAGE_TAG_PRIVATE: git.myservermanager.com/varakh/upda
|
||||||
FORGEJO_URL: https://git.myservermanager.com
|
FORGEJO_URL: https://git.myservermanager.com
|
||||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -2,8 +2,17 @@
|
||||||
|
|
||||||
Changes adhere to [semantic versioning](https://semver.org).
|
Changes adhere to [semantic versioning](https://semver.org).
|
||||||
|
|
||||||
## [2.0.2] - UNRELEASED
|
## [3.0.0] - UNRELEASED
|
||||||
|
|
||||||
|
> This is a major version upgrade. Other versions are incompatible with this release.
|
||||||
|
|
||||||
|
* Added automatic detection of `GOMAXPROCS`
|
||||||
|
* Switched to enforce JSON as `Content-Type` for all incoming requests
|
||||||
|
* Switched to properly respond with JSON on page not found or method not allowed
|
||||||
|
* Renamed `CORS_ALLOW_ORIGIN` to `CORS_ALLOW_ORIGINS`
|
||||||
|
* Added `CORS_ALLOW_CREDENTIALS` which defaults to `true`
|
||||||
|
* Added `CORS_EXPOSE_HEADERS` which defaults to `*`
|
||||||
|
* Overhauled package visibility for server module
|
||||||
* Updated dependencies
|
* Updated dependencies
|
||||||
* Updated OCI image base to alpine `3.20` with Go `1.22`
|
* Updated OCI image base to alpine `3.20` with Go `1.22`
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ platform, uploads produced artifacts to that release and publishes docker images
|
||||||
built successfully:
|
built successfully:
|
||||||
|
|
||||||
* Adapt `constants_app.go` and change `Version` to the correct version number
|
* Adapt `constants_app.go` and change `Version` to the correct version number
|
||||||
|
* Adapt `app.go` and change `version` to the correct version number (CLI)
|
||||||
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
* Adapt `CHANGELOG.md` to reflect changes and ensure a date is properly set in the header, also add a reference link
|
||||||
in footer (link to scm git tag source)
|
in footer (link to scm git tag source)
|
||||||
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
* Adapt `api.yaml`: `version` attribute must reflect the to be released version
|
||||||
|
@ -118,6 +119,7 @@ built successfully:
|
||||||
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
After the release has been created, ensure to change the following settings for the _next development cycle_:
|
||||||
|
|
||||||
* Adapt `constants_app.go` and change `Version` to the _next_ version number
|
* Adapt `constants_app.go` and change `Version` to the _next_ version number
|
||||||
|
* Adapt `app.go` and change `version` to the _next_ version number (CLI)
|
||||||
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
* Adapt `CHANGELOG.md` and add an _UNRELEASED_ section
|
||||||
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
* Adapt `api.yaml`: `version` attribute must reflect the _next_ version number
|
||||||
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
* Adapt `env: VERSION_*` in `.forgejo/workflows/release.yaml` to _next_ version number
|
||||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: upda
|
title: upda
|
||||||
description: API specification
|
description: API specification
|
||||||
version: 2.0.2
|
version: 3.0.0
|
||||||
externalDocs:
|
externalDocs:
|
||||||
description: Find out more about the project
|
description: Find out more about the project
|
||||||
url: https://git.myservermanager.com/varakh/upda
|
url: https://git.myservermanager.com/varakh/upda
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -201,8 +201,6 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne
|
||||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||||
github.com/redis/go-redis/v9 v9.5.2 h1:L0L3fcSNReTRGyZ6AqAEN0K56wYeYAwapBIhkvh0f3E=
|
|
||||||
github.com/redis/go-redis/v9 v9.5.2/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
|
||||||
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU=
|
||||||
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||||
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
|
||||||
|
|
|
@ -19,7 +19,7 @@ func errAbortWithValidatorPayload(c *gin.Context, err error) {
|
||||||
errorMap[key] = txt
|
errorMap[key] = txt
|
||||||
}
|
}
|
||||||
|
|
||||||
resErr := newServiceError(IllegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
resErr := newServiceError(illegalArgument, fmt.Errorf("validation error: %v (%w)", util.ValuesString(errorMap), err))
|
||||||
c.Header(headerContentType, headerContentTypeApplicationJson)
|
c.Header(headerContentType, headerContentTypeApplicationJson)
|
||||||
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
_ = c.AbortWithError(http.StatusBadRequest, resErr)
|
||||||
return
|
return
|
||||||
|
@ -29,17 +29,19 @@ func errToHttpStatus(err error) int {
|
||||||
var e *serviceError
|
var e *serviceError
|
||||||
switch {
|
switch {
|
||||||
case errors.As(err, &e):
|
case errors.As(err, &e):
|
||||||
if e.Status == IllegalArgument {
|
if e.Status == illegalArgument {
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
} else if e.Status == Unauthorized {
|
} else if e.Status == unauthorized {
|
||||||
return http.StatusUnauthorized
|
return http.StatusUnauthorized
|
||||||
} else if e.Status == Forbidden {
|
} else if e.Status == forbidden {
|
||||||
return http.StatusForbidden
|
return http.StatusForbidden
|
||||||
} else if e.Status == NotFound {
|
} else if e.Status == notFound {
|
||||||
return http.StatusNotFound
|
return http.StatusNotFound
|
||||||
} else if e.Status == Conflict {
|
} else if e.Status == methodNotAllowed {
|
||||||
|
return http.StatusMethodNotAllowed
|
||||||
|
} else if e.Status == conflict {
|
||||||
return http.StatusConflict
|
return http.StatusConflict
|
||||||
} else if e.Status == General {
|
} else if e.Status == general {
|
||||||
return http.StatusInternalServerError
|
return http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
@ -57,7 +59,7 @@ func errCodeToStr(err error) string {
|
||||||
return string(e.Status)
|
return string(e.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(General)
|
return string(general)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
func validatorErrorToText(e *validator.FieldError) (string, string) {
|
||||||
|
|
|
@ -16,8 +16,8 @@ func newInfoHandler(a *appConfig) *infoHandler {
|
||||||
|
|
||||||
func (h *infoHandler) show(c *gin.Context) {
|
func (h *infoHandler) show(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
c.JSON(http.StatusOK, api.DataResponse{Data: gin.H{
|
||||||
"name": Name,
|
"name": name,
|
||||||
"version": Version,
|
"version": version,
|
||||||
"timeZone": h.appConfig.timeZone,
|
"timeZone": h.appConfig.timeZone,
|
||||||
}})
|
}})
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ func newWebhookInvocationHandler(i *webhookInvocationService, w *webhookService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
||||||
tokenHeader := c.GetHeader(HeaderWebhookToken)
|
tokenHeader := c.GetHeader(headerWebhookToken)
|
||||||
webhookId := c.Param("id")
|
webhookId := c.Param("id")
|
||||||
|
|
||||||
var w *Webhook
|
var w *Webhook
|
||||||
|
@ -52,7 +52,7 @@ func (h *webhookInvocationHandler) execute(c *gin.Context) {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
err = newServiceError(IllegalArgument, errors.New("no default handler for webhook type found"))
|
err = newServiceError(illegalArgument, errors.New("no default handler for webhook type found"))
|
||||||
_ = c.AbortWithError(errToHttpStatus(err), err)
|
_ = c.AbortWithError(errToHttpStatus(err), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,43 @@ import (
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func middlewareAppName() gin.HandlerFunc {
|
func middlewareAppName() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header(HeaderAppName, Name)
|
c.Header(headerAppName, name)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareGlobalNotFound() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusNotFound, api.NewErrorResponseWithStatusAndMessage(string(notFound), "page not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareGlobalMethodNotAllowed() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusMethodNotAllowed, api.NewErrorResponseWithStatusAndMessage(string(methodNotAllowed), "method not allowed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareEnforceJsonContentType() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if c.Request.Method != http.MethodOptions && !strings.HasPrefix(c.GetHeader(headerContentType), headerContentTypeApplicationJson) {
|
||||||
|
c.AbortWithStatusJSON(http.StatusBadRequest, api.NewErrorResponseWithStatusAndMessage(string(illegalArgument), "content-type must be application/json"))
|
||||||
|
return
|
||||||
|
}
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func middlewareAppVersion() gin.HandlerFunc {
|
func middlewareAppVersion() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header(HeaderAppVersion, Version)
|
c.Header(headerAppVersion, version)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +72,7 @@ func middlewareAppErrorRecoveryHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(General), fmt.Sprintf("%s", err)))
|
c.AbortWithStatusJSON(http.StatusInternalServerError, api.NewErrorResponseWithStatusAndMessage(string(general), fmt.Sprintf("%s", err)))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
|
@ -100,17 +100,21 @@ func Start() {
|
||||||
hh := newHealthHandler()
|
hh := newHealthHandler()
|
||||||
authH := newAuthHandler()
|
authH := newAuthHandler()
|
||||||
|
|
||||||
|
router.Use(middlewareEnforceJsonContentType())
|
||||||
router.Use(middlewareAppName())
|
router.Use(middlewareAppName())
|
||||||
router.Use(middlewareAppVersion())
|
router.Use(middlewareAppVersion())
|
||||||
router.Use(middlewareAppContentType())
|
router.Use(middlewareAppContentType())
|
||||||
router.Use(middlewareErrorHandler())
|
router.Use(middlewareErrorHandler())
|
||||||
router.Use(middlewareAppErrorRecoveryHandler())
|
router.Use(middlewareAppErrorRecoveryHandler())
|
||||||
|
router.NoRoute(middlewareGlobalNotFound())
|
||||||
|
router.NoMethod(middlewareGlobalMethodNotAllowed())
|
||||||
|
|
||||||
router.Use(cors.New(cors.Config{
|
router.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: env.serverConfig.corsAllowOrigin,
|
AllowOrigins: env.serverConfig.corsAllowOrigins,
|
||||||
AllowMethods: env.serverConfig.corsAllowMethods,
|
AllowMethods: env.serverConfig.corsAllowMethods,
|
||||||
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
AllowHeaders: env.serverConfig.corsAllowHeaders,
|
||||||
AllowCredentials: true,
|
AllowCredentials: env.serverConfig.corsAllowCredentials,
|
||||||
|
ExposeHeaders: env.serverConfig.corsExposeHeaders,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
apiPublicGroup := router.Group("/api/v1")
|
apiPublicGroup := router.Group("/api/v1")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HeaderAppName = "X-App-Name"
|
headerAppName = "X-App-Name"
|
||||||
HeaderAppVersion = "X-App-Version"
|
headerAppVersion = "X-App-Version"
|
||||||
|
|
||||||
HeaderWebhookToken = "X-Webhook-Token"
|
headerWebhookToken = "X-Webhook-Token"
|
||||||
|
|
||||||
headerContentType = "Content-Type"
|
headerContentType = "Content-Type"
|
||||||
headerContentTypeApplicationJson = "application/json"
|
headerContentTypeApplicationJson = "application/json"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Name = "upda"
|
name = "upda"
|
||||||
Version = "2.0.2"
|
version = "3.0.0"
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,12 +35,16 @@ const (
|
||||||
serverTlsEnabledDefault = "false"
|
serverTlsEnabledDefault = "false"
|
||||||
serverTimeoutDefault = "1s"
|
serverTimeoutDefault = "1s"
|
||||||
|
|
||||||
envCorsAllowOrigin = "CORS_ALLOW_ORIGIN"
|
envCorsAllowOrigins = "CORS_ALLOW_ORIGINS"
|
||||||
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
envCorsAllowMethods = "CORS_ALLOW_METHODS"
|
||||||
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
envCorsAllowHeaders = "CORS_ALLOW_HEADERS"
|
||||||
corsAllowOriginDefault = "*"
|
envCorsAllowCredentials = "CORS_ALLOW_CREDENTIALS"
|
||||||
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
envCorsExposeHeaders = "CORS_EXPOSE_HEADERS"
|
||||||
corsAllowHeadersDefault = "Authorization, Content-Type"
|
corsAllowOriginsDefault = "*"
|
||||||
|
corsAllowMethodsDefault = "HEAD, GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||||
|
corsAllowHeadersDefault = "Authorization, Content-Type"
|
||||||
|
corsAllowCredentialsDefault = "true"
|
||||||
|
corsExposeHeadersDefault = "*"
|
||||||
|
|
||||||
dbTypeSqlite = "sqlite"
|
dbTypeSqlite = "sqlite"
|
||||||
dbTypePostgres = "postgres"
|
dbTypePostgres = "postgres"
|
||||||
|
|
|
@ -27,15 +27,17 @@ type appConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type serverConfig struct {
|
type serverConfig struct {
|
||||||
port int
|
port int
|
||||||
listen string
|
listen string
|
||||||
tlsEnabled bool
|
tlsEnabled bool
|
||||||
tlsCertPath string
|
tlsCertPath string
|
||||||
tlsKeyPath string
|
tlsKeyPath string
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
corsAllowOrigin []string
|
corsAllowCredentials bool
|
||||||
corsAllowMethods []string
|
corsAllowOrigins []string
|
||||||
corsAllowHeaders []string
|
corsAllowMethods []string
|
||||||
|
corsAllowHeaders []string
|
||||||
|
corsExposeHeaders []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type authConfig struct {
|
type authConfig struct {
|
||||||
|
@ -203,15 +205,17 @@ func bootstrapEnvironment() *Environment {
|
||||||
}
|
}
|
||||||
|
|
||||||
sc = &serverConfig{
|
sc = &serverConfig{
|
||||||
port: serverPort,
|
port: serverPort,
|
||||||
timeout: serverTimeout,
|
timeout: serverTimeout,
|
||||||
listen: os.Getenv(envServerListen),
|
listen: os.Getenv(envServerListen),
|
||||||
tlsEnabled: serverTlsEnabled,
|
tlsEnabled: serverTlsEnabled,
|
||||||
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
tlsCertPath: os.Getenv(envServerTlsCertPath),
|
||||||
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
tlsKeyPath: os.Getenv(envServerTlsKeyPath),
|
||||||
corsAllowOrigin: []string{os.Getenv(envCorsAllowOrigin)},
|
corsAllowCredentials: os.Getenv(envCorsAllowCredentials) == "true",
|
||||||
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
corsExposeHeaders: []string{os.Getenv(envCorsExposeHeaders)},
|
||||||
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
corsAllowOrigins: []string{os.Getenv(envCorsAllowOrigins)},
|
||||||
|
corsAllowMethods: []string{os.Getenv(envCorsAllowMethods)},
|
||||||
|
corsAllowHeaders: []string{os.Getenv(envCorsAllowHeaders)},
|
||||||
}
|
}
|
||||||
|
|
||||||
authMode := os.Getenv(envAuthMode)
|
authMode := os.Getenv(envAuthMode)
|
||||||
|
@ -338,7 +342,7 @@ func bootstrapEnvironment() *Environment {
|
||||||
if os.Getenv(envDbType) == dbTypeSqlite {
|
if os.Getenv(envDbType) == dbTypeSqlite {
|
||||||
if os.Getenv(envDbSqliteFile) == "" {
|
if os.Getenv(envDbSqliteFile) == "" {
|
||||||
var defaultDbFile string
|
var defaultDbFile string
|
||||||
if defaultDbFile, err = xdg.DataFile(Name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
if defaultDbFile, err = xdg.DataFile(name + "/" + dbTypeSqliteDbNameDefault); err != nil {
|
||||||
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
zap.L().Sugar().Fatalf("Database file '%s' could not be created. Reason: %v", defaultDbFile, err)
|
||||||
}
|
}
|
||||||
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
setEnvKeyDefault(envDbSqliteFile, defaultDbFile)
|
||||||
|
@ -464,9 +468,11 @@ func bootstrapFromEnvironmentAndValidate() {
|
||||||
setEnvKeyDefault(envServerPort, serverPortDefault)
|
setEnvKeyDefault(envServerPort, serverPortDefault)
|
||||||
setEnvKeyDefault(envServerListen, serverListenDefault)
|
setEnvKeyDefault(envServerListen, serverListenDefault)
|
||||||
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
setEnvKeyDefault(envServerTlsEnabled, serverTlsEnabledDefault)
|
||||||
setEnvKeyDefault(envCorsAllowOrigin, corsAllowOriginDefault)
|
setEnvKeyDefault(envCorsAllowOrigins, corsAllowOriginsDefault)
|
||||||
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
setEnvKeyDefault(envCorsAllowMethods, corsAllowMethodsDefault)
|
||||||
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
setEnvKeyDefault(envCorsAllowHeaders, corsAllowHeadersDefault)
|
||||||
|
setEnvKeyDefault(envCorsAllowCredentials, corsAllowCredentialsDefault)
|
||||||
|
setEnvKeyDefault(envCorsExposeHeaders, corsExposeHeadersDefault)
|
||||||
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
setEnvKeyDefault(envServerTimeout, serverTimeoutDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,43 +6,44 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errorValidationNotEmpty = newServiceError(IllegalArgument, errors.New("assert: empty values are not allowed"))
|
errorValidationNotEmpty = newServiceError(illegalArgument, errors.New("assert: empty values are not allowed"))
|
||||||
errorValidationNotBlank = newServiceError(IllegalArgument, errors.New("assert: blank values are not allowed"))
|
errorValidationNotBlank = newServiceError(illegalArgument, errors.New("assert: blank values are not allowed"))
|
||||||
errorValidationPageGreaterZero = newServiceError(IllegalArgument, errors.New("assert: page has to be greater 0"))
|
errorValidationPageGreaterZero = newServiceError(illegalArgument, errors.New("assert: page has to be greater 0"))
|
||||||
errorValidationPageSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
errorValidationPageSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: pageSize has to be greater 0"))
|
||||||
errorValidationLimitGreaterZero = newServiceError(IllegalArgument, errors.New("assert: limit has to be greater 0"))
|
errorValidationLimitGreaterZero = newServiceError(illegalArgument, errors.New("assert: limit has to be greater 0"))
|
||||||
errorValidationSizeGreaterZero = newServiceError(IllegalArgument, errors.New("assert: size has to be greater 0"))
|
errorValidationSizeGreaterZero = newServiceError(illegalArgument, errors.New("assert: size has to be greater 0"))
|
||||||
errorValidationMaxRetriesGreaterZero = newServiceError(IllegalArgument, errors.New("assert: max retries has to be greater 0"))
|
errorValidationMaxRetriesGreaterZero = newServiceError(illegalArgument, errors.New("assert: max retries has to be greater 0"))
|
||||||
|
|
||||||
errorResourceNotFound = newServiceError(NotFound, errors.New("resource not found"))
|
errorResourceNotFound = newServiceError(notFound, errors.New("resource not found"))
|
||||||
errorResourceAccessDenied = newServiceError(Forbidden, errors.New("resource access denied"))
|
errorResourceAccessDenied = newServiceError(forbidden, errors.New("resource access denied"))
|
||||||
|
|
||||||
errorDatabaseRowsExpected = newServiceDatabaseError(errors.New("action failed, expected affected rows, but got none"))
|
errorDatabaseRowsExpected = newServiceDatabaseError(errors.New("action failed, expected affected rows, but got none"))
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrorCode string
|
type errorCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
IllegalArgument ErrorCode = "IllegalArgument"
|
illegalArgument errorCode = "IllegalArgument"
|
||||||
Unauthorized ErrorCode = "Unauthorized"
|
unauthorized errorCode = "Unauthorized"
|
||||||
Forbidden ErrorCode = "Forbidden"
|
forbidden errorCode = "Forbidden"
|
||||||
NotFound ErrorCode = "NotFound"
|
notFound errorCode = "NotFound"
|
||||||
Conflict ErrorCode = "Conflict"
|
methodNotAllowed errorCode = "MethodNotAllowed"
|
||||||
General ErrorCode = "General"
|
conflict errorCode = "Conflict"
|
||||||
|
general errorCode = "General"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newServiceError returns an error that formats as the given text and aligns with builtin error
|
// newServiceError returns an error that formats as the given text and aligns with builtin error
|
||||||
func newServiceError(status ErrorCode, err error) error {
|
func newServiceError(status errorCode, err error) error {
|
||||||
return &serviceError{status, fmt.Errorf("service error (%v): %w", status, err)}
|
return &serviceError{status, fmt.Errorf("service error (%v): %w", status, err)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newServiceDatabaseError returns an error that formats as the given text and aligns with builtin error
|
// newServiceDatabaseError returns an error that formats as the given text and aligns with builtin error
|
||||||
func newServiceDatabaseError(error error) error {
|
func newServiceDatabaseError(error error) error {
|
||||||
return newServiceError(General, fmt.Errorf("database error: %w", error))
|
return newServiceError(general, fmt.Errorf("database error: %w", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceError struct {
|
type serviceError struct {
|
||||||
Status ErrorCode
|
Status errorCode
|
||||||
Cause error
|
Cause error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebhookRepository interface {
|
type webhookRepository interface {
|
||||||
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
paginate(page int, pageSize int, orderBy string, order string) ([]*Webhook, error)
|
||||||
count() (int64, error)
|
count() (int64, error)
|
||||||
find(id string) (*Webhook, error)
|
find(id string) (*Webhook, error)
|
||||||
|
|
|
@ -39,7 +39,7 @@ func (s *actionService) create(label string, t api.ActionType, matchEvent *strin
|
||||||
}
|
}
|
||||||
|
|
||||||
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||||
return nil, newServiceError(IllegalArgument, validationErr)
|
return nil, newServiceError(illegalArgument, validationErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -197,7 +197,7 @@ func (s *actionService) updateTypeAndPayload(id string, t api.ActionType, payloa
|
||||||
}
|
}
|
||||||
|
|
||||||
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
if isValid, validationErr := s.isValidPayload(t, payload); !isValid {
|
||||||
return nil, newServiceError(IllegalArgument, validationErr)
|
return nil, newServiceError(illegalArgument, validationErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e, err = s.repo.updateTypeAndPayload(id, t, payload); err != nil {
|
if e, err = s.repo.updateTypeAndPayload(id, t, payload); err != nil {
|
||||||
|
|
|
@ -28,7 +28,7 @@ func newActionInvocationService(r ActionInvocationRepository, a *actionService,
|
||||||
|
|
||||||
func (s *actionInvocationService) enqueue(batchSize int) error {
|
func (s *actionInvocationService) enqueue(batchSize int) error {
|
||||||
if batchSize <= 0 {
|
if batchSize <= 0 {
|
||||||
return newServiceError(General, errors.New("cannot enqueue actions from events with invalid configured batch size"))
|
return newServiceError(general, errors.New("cannot enqueue actions from events with invalid configured batch size"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var events []*Event
|
var events []*Event
|
||||||
|
@ -54,7 +54,7 @@ func (s *actionInvocationService) enqueue(batchSize int) error {
|
||||||
|
|
||||||
func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Action) error {
|
func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Action) error {
|
||||||
if event == nil || actions == nil {
|
if event == nil || actions == nil {
|
||||||
return newServiceError(IllegalArgument, errorValidationNotEmpty)
|
return newServiceError(illegalArgument, errorValidationNotEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -99,10 +99,10 @@ func (s *actionInvocationService) enqueueFromEvent(event *Event, actions []*Acti
|
||||||
|
|
||||||
func (s *actionInvocationService) invoke(batchSize int, maxRetries int) error {
|
func (s *actionInvocationService) invoke(batchSize int, maxRetries int) error {
|
||||||
if batchSize <= 0 {
|
if batchSize <= 0 {
|
||||||
return newServiceError(General, errors.New("cannot invoke actions with invalid configured batch size"))
|
return newServiceError(general, errors.New("cannot invoke actions with invalid configured batch size"))
|
||||||
}
|
}
|
||||||
if maxRetries <= 0 {
|
if maxRetries <= 0 {
|
||||||
return newServiceError(General, errors.New("cannot invoke actions with invalid configured max retries"))
|
return newServiceError(general, errors.New("cannot invoke actions with invalid configured max retries"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
@ -198,14 +198,14 @@ func (s *actionInvocationService) execute(action *Action, eventPayloadInfo *even
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
|
|
||||||
if bytes, err = action.Payload.MarshalJSON(); err != nil {
|
if bytes, err = action.Payload.MarshalJSON(); err != nil {
|
||||||
return newServiceError(General, err)
|
return newServiceError(general, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch action.Type {
|
switch action.Type {
|
||||||
case api.ActionTypeShoutrrr.Value():
|
case api.ActionTypeShoutrrr.Value():
|
||||||
var payload actionPayloadShoutrrrDto
|
var payload actionPayloadShoutrrrDto
|
||||||
if payload, err = util.UnmarshalGenericJSON[actionPayloadShoutrrrDto](bytes); err != nil {
|
if payload, err = util.UnmarshalGenericJSON[actionPayloadShoutrrrDto](bytes); err != nil {
|
||||||
return newServiceError(General, err)
|
return newServiceError(general, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
body := s.replaceVars(payload.Body, eventPayloadInfo)
|
body := s.replaceVars(payload.Body, eventPayloadInfo)
|
||||||
|
@ -220,7 +220,7 @@ func (s *actionInvocationService) execute(action *Action, eventPayloadInfo *even
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return newServiceError(General, errors.New("no matching action type found for invocation"))
|
return newServiceError(general, errors.New("no matching action type found for invocation"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -194,41 +194,41 @@ func (s *eventService) extractPayloadInfo(event *Event) (*eventPayloadInformatio
|
||||||
var bytes []byte
|
var bytes []byte
|
||||||
|
|
||||||
if bytes, err = event.Payload.MarshalJSON(); err != nil {
|
if bytes, err = event.Payload.MarshalJSON(); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event.Name {
|
switch event.Name {
|
||||||
case api.EventNameUpdateCreated.Value():
|
case api.EventNameUpdateCreated.Value():
|
||||||
var p api.EventPayloadUpdateCreatedDto
|
var p api.EventPayloadUpdateCreatedDto
|
||||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateCreatedDto](bytes); err != nil {
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateCreatedDto](bytes); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
case api.EventNameUpdateDeleted.Value():
|
case api.EventNameUpdateDeleted.Value():
|
||||||
var p api.EventPayloadUpdateDeletedDto
|
var p api.EventPayloadUpdateDeletedDto
|
||||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateDeletedDto](bytes); err != nil {
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateDeletedDto](bytes); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
case api.EventNameUpdateUpdatedState.Value():
|
case api.EventNameUpdateUpdatedState.Value():
|
||||||
var p api.EventPayloadUpdateUpdatedDto
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
case api.EventNameUpdateUpdatedVersion.Value():
|
case api.EventNameUpdateUpdatedVersion.Value():
|
||||||
var p api.EventPayloadUpdateUpdatedDto
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
case api.EventNameUpdateUpdated.Value():
|
case api.EventNameUpdateUpdated.Value():
|
||||||
var p api.EventPayloadUpdateUpdatedDto
|
var p api.EventPayloadUpdateUpdatedDto
|
||||||
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
if p, err = util.UnmarshalGenericJSON[api.EventPayloadUpdateUpdatedDto](bytes); err != nil {
|
||||||
return nil, newServiceError(General, err)
|
return nil, newServiceError(general, err)
|
||||||
}
|
}
|
||||||
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
return &eventPayloadInformationDto{Host: p.Host, Application: p.Application, Provider: p.Provider, Version: p.Version, State: p.State}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, newServiceError(General, errors.New("no matching event found"))
|
return nil, newServiceError(general, errors.New("no matching event found"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ type lockMemService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errLockMemNotReleased = newServiceError(Conflict, errors.New("lock service: could not release lock"))
|
errLockMemNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLockMemService() lockService {
|
func newLockMemService() lockService {
|
||||||
|
|
|
@ -15,8 +15,8 @@ type lockRedisService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errLockRedisNotObtained = newServiceError(Conflict, errors.New("lock service: could not obtain lock"))
|
errLockRedisNotObtained = newServiceError(conflict, errors.New("lock service: could not obtain lock"))
|
||||||
errLockRedisNotReleased = newServiceError(Conflict, errors.New("lock service: could not release lock"))
|
errLockRedisNotReleased = newServiceError(conflict, errors.New("lock service: could not release lock"))
|
||||||
)
|
)
|
||||||
|
|
||||||
func newLockRedisService(lc *lockConfig) (lockService, error) {
|
func newLockRedisService(lc *lockConfig) (lockService, error) {
|
||||||
|
|
|
@ -18,7 +18,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
||||||
if c.secureTokenEnabled {
|
if c.secureTokenEnabled {
|
||||||
p = ginprom.New(
|
p = ginprom.New(
|
||||||
ginprom.Engine(r),
|
ginprom.Engine(r),
|
||||||
ginprom.Namespace(Name),
|
ginprom.Namespace(name),
|
||||||
ginprom.Subsystem(""),
|
ginprom.Subsystem(""),
|
||||||
ginprom.Path(c.path),
|
ginprom.Path(c.path),
|
||||||
ginprom.Ignore(c.path),
|
ginprom.Ignore(c.path),
|
||||||
|
@ -27,7 +27,7 @@ func newPrometheusService(r *gin.Engine, c *prometheusConfig) *prometheusService
|
||||||
} else {
|
} else {
|
||||||
p = ginprom.New(
|
p = ginprom.New(
|
||||||
ginprom.Engine(r),
|
ginprom.Engine(r),
|
||||||
ginprom.Namespace(Name),
|
ginprom.Namespace(name),
|
||||||
ginprom.Subsystem(""),
|
ginprom.Subsystem(""),
|
||||||
ginprom.Ignore(c.path),
|
ginprom.Ignore(c.path),
|
||||||
ginprom.Path(c.path),
|
ginprom.Path(c.path),
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type webhookService struct {
|
type webhookService struct {
|
||||||
repo WebhookRepository
|
repo webhookRepository
|
||||||
webhookConfig *webhookConfig
|
webhookConfig *webhookConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebhookService(r WebhookRepository, c *webhookConfig) *webhookService {
|
func newWebhookService(r webhookRepository, c *webhookConfig) *webhookService {
|
||||||
return &webhookService{
|
return &webhookService{
|
||||||
repo: r,
|
repo: r,
|
||||||
webhookConfig: c,
|
webhookConfig: c,
|
||||||
|
@ -42,7 +42,7 @@ func (s *webhookService) create(label string, t api.WebhookType, ignoreHost bool
|
||||||
var token string
|
var token string
|
||||||
|
|
||||||
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
|
if token, err = util.GenerateSecureRandomString(s.webhookConfig.tokenLength); err != nil {
|
||||||
return nil, newServiceError(General, fmt.Errorf("token generation failed: %w", err))
|
return nil, newServiceError(general, fmt.Errorf("token generation failed: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var e *Webhook
|
var e *Webhook
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.myservermanager.com/varakh/upda/api"
|
"git.myservermanager.com/varakh/upda/api"
|
||||||
"git.myservermanager.com/varakh/upda/server"
|
|
||||||
"git.myservermanager.com/varakh/upda/util"
|
"git.myservermanager.com/varakh/upda/util"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
|
@ -17,7 +16,7 @@ import (
|
||||||
const (
|
const (
|
||||||
name = "upda-cli"
|
name = "upda-cli"
|
||||||
desc = "a commandline helper for upda"
|
desc = "a commandline helper for upda"
|
||||||
version = server.Version
|
version = "3.0.0"
|
||||||
|
|
||||||
envServerUrl = "UPDA_SERVER_URL"
|
envServerUrl = "UPDA_SERVER_URL"
|
||||||
envUser = "UPDA_USER"
|
envUser = "UPDA_USER"
|
||||||
|
@ -264,7 +263,7 @@ func webhookSend(cCtx *cli.Context) error {
|
||||||
client.SetDisableWarn(true)
|
client.SetDisableWarn(true)
|
||||||
res, err := client.R().
|
res, err := client.R().
|
||||||
SetHeader("Content-Type", "application/json").
|
SetHeader("Content-Type", "application/json").
|
||||||
SetHeader(server.HeaderWebhookToken, cCtx.String(flagWebhookToken)).
|
SetHeader("X-Webhook-Token", cCtx.String(flagWebhookToken)).
|
||||||
SetBody(payloadArg).
|
SetBody(payloadArg).
|
||||||
SetError(&errorRes).
|
SetError(&errorRes).
|
||||||
Post(url)
|
Post(url)
|
||||||
|
|
Loading…
Reference in a new issue