mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
wip: 🔕 temporary commit
This commit is contained in:
parent
2a1fa5c108
commit
015e686635
@ -39,8 +39,9 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
var (
|
||||
addrFlag = shared.ListenAddrFlag
|
||||
portFlag = shared.ListenPortFlag
|
||||
addTplFlag = shared.AddTemplateFlag
|
||||
addCodeFlag = shared.AddHTTPCodeFlag
|
||||
addTplFlag = shared.AddTemplatesFlag
|
||||
disableTplFlag = shared.DisableTemplateNamesFlag
|
||||
addCodeFlag = shared.AddHTTPCodesFlag
|
||||
jsonFormatFlag = cli.StringFlag{
|
||||
Name: "json-format",
|
||||
Usage: "override the default error page response in JSON format (Go templates are supported)",
|
||||
@ -140,16 +141,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// readBufferSizeFlag = cli.UintFlag{
|
||||
// Name: "read-buffer-size",
|
||||
// Usage: "customize the HTTP read buffer size (set per connection for reading requests, also limits the " +
|
||||
// "maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB " +
|
||||
// "headers, such as large cookies)",
|
||||
// DefaultText: "not set",
|
||||
// Sources: cli.EnvVars("READ_BUFFER_SIZE"),
|
||||
// OnlyOnce: true,
|
||||
// }
|
||||
)
|
||||
|
||||
cmd.c = &cli.Command{
|
||||
@ -160,16 +151,28 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
Action: func(ctx context.Context, c *cli.Command) error {
|
||||
cmd.opt.http.addr = c.String(addrFlag.Name)
|
||||
cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
|
||||
// cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
|
||||
|
||||
cfg.TemplateName = c.String(templateNameFlag.Name)
|
||||
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
|
||||
cfg.DefaultCodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
|
||||
cfg.RespondWithSameHTTPCode = c.Bool(sendSameHTTPCodeFlag.Name)
|
||||
cfg.RotationMode, _ = config.ParseRotationMode(c.String(rotationModeFlag.Name))
|
||||
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
|
||||
|
||||
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
|
||||
{ // override default JSON, XML, and PlainText formats
|
||||
if c.IsSet(jsonFormatFlag.Name) {
|
||||
cfg.Formats.JSON = strings.TrimSpace(c.String(jsonFormatFlag.Name))
|
||||
}
|
||||
|
||||
if c.IsSet(xmlFormatFlag.Name) {
|
||||
cfg.Formats.XML = strings.TrimSpace(c.String(xmlFormatFlag.Name))
|
||||
}
|
||||
|
||||
if c.IsSet(plainTextFormatFlag.Name) {
|
||||
cfg.Formats.PlainText = strings.TrimSpace(c.String(plainTextFormatFlag.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// add templates from files to the configuration
|
||||
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 {
|
||||
for _, templatePath := range add {
|
||||
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
|
||||
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
|
||||
@ -182,10 +185,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.Templates.Has(cfg.TemplateName) {
|
||||
return fmt.Errorf("template %s not found and cannot be used", cfg.TemplateName)
|
||||
}
|
||||
|
||||
// set the list of HTTP headers we need to proxy from the incoming request to the error page response
|
||||
if c.IsSet(proxyHeadersListFlag.Name) {
|
||||
var m = make(map[string]struct{}) // map is used to avoid duplicates
|
||||
|
||||
@ -200,7 +200,8 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
}
|
||||
}
|
||||
|
||||
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 { // add custom HTTP codes
|
||||
// add custom HTTP codes to the configuration
|
||||
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 {
|
||||
for code, msgAndDesc := range add {
|
||||
var (
|
||||
parts = strings.SplitN(msgAndDesc, "/", 2) //nolint:mnd
|
||||
@ -225,17 +226,24 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
}
|
||||
}
|
||||
|
||||
{ // override default JSON and XML formats
|
||||
if c.IsSet(jsonFormatFlag.Name) {
|
||||
cfg.Formats.JSON = strings.TrimSpace(c.String(jsonFormatFlag.Name))
|
||||
// disable templates specified by the user
|
||||
if disable := c.StringSlice(disableTplFlag.Name); len(disable) > 0 {
|
||||
for _, templateName := range disable {
|
||||
if ok := cfg.Templates.Remove(templateName); ok {
|
||||
log.Info("Template disabled", logger.String("name", templateName))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.IsSet(xmlFormatFlag.Name) {
|
||||
cfg.Formats.XML = strings.TrimSpace(c.String(xmlFormatFlag.Name))
|
||||
}
|
||||
// if the rotation mode is set to random-on-startup, pick a random template (ignore the user-provided
|
||||
// template name)
|
||||
if cfg.RotationMode == config.RotationModeRandomOnStartup {
|
||||
cfg.TemplateName = cfg.Templates.RandomName()
|
||||
} else { // otherwise, use the user-provided template name
|
||||
cfg.TemplateName = c.String(templateNameFlag.Name)
|
||||
|
||||
if c.IsSet(plainTextFormatFlag.Name) {
|
||||
cfg.Formats.PlainText = strings.TrimSpace(c.String(plainTextFormatFlag.Name))
|
||||
if !cfg.Templates.Has(cfg.TemplateName) {
|
||||
return fmt.Errorf("template %s not found and cannot be used", cfg.TemplateName)
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,10 +252,12 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
logger.Strings("described HTTP codes", cfg.Codes.Codes()...),
|
||||
logger.String("JSON format", cfg.Formats.JSON),
|
||||
logger.String("XML format", cfg.Formats.XML),
|
||||
logger.String("plain text format", cfg.Formats.PlainText),
|
||||
logger.String("template name", cfg.TemplateName),
|
||||
logger.Bool("disable localization", cfg.L10n.Disable),
|
||||
logger.Uint16("default code to render", cfg.DefaultCodeToRender),
|
||||
logger.Bool("respond with the same HTTP code", cfg.RespondWithSameHTTPCode),
|
||||
logger.String("rotation mode", cfg.RotationMode.String()),
|
||||
logger.Bool("show details", cfg.ShowDetails),
|
||||
logger.Strings("proxy HTTP headers", cfg.ProxyHeaders...),
|
||||
)
|
||||
@ -258,6 +268,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
&addrFlag,
|
||||
&portFlag,
|
||||
&addTplFlag,
|
||||
&disableTplFlag,
|
||||
&addCodeFlag,
|
||||
&jsonFormatFlag,
|
||||
&xmlFormatFlag,
|
||||
@ -269,7 +280,6 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit,gocy
|
||||
&showDetailsFlag,
|
||||
&proxyHeadersListFlag,
|
||||
&rotationModeFlag,
|
||||
// &readBufferSizeFlag,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ var ListenPortFlag = cli.UintFlag{
|
||||
},
|
||||
}
|
||||
|
||||
var AddTemplateFlag = cli.StringSliceFlag{
|
||||
var AddTemplatesFlag = cli.StringSliceFlag{
|
||||
Name: "add-template",
|
||||
Usage: "to add a new template, provide the path to the file using this flag (the filename without the extension " +
|
||||
"will be used as the template name)",
|
||||
@ -69,7 +69,13 @@ var AddTemplateFlag = cli.StringSliceFlag{
|
||||
},
|
||||
}
|
||||
|
||||
var AddHTTPCodeFlag = cli.StringMapFlag{
|
||||
var DisableTemplateNamesFlag = cli.StringSliceFlag{
|
||||
Name: "disable-template",
|
||||
Usage: "disable the specified template by its name",
|
||||
Config: cli.StringConfig{TrimSpace: true},
|
||||
}
|
||||
|
||||
var AddHTTPCodesFlag = cli.StringMapFlag{
|
||||
Name: "add-http-code",
|
||||
Usage: "to add a new HTTP status code, provide the code and its message/description using this flag (the format " +
|
||||
"should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at once, " +
|
||||
|
@ -84,10 +84,10 @@ func TestListenPortFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddTemplateFlag(t *testing.T) {
|
||||
func TestAddTemplatesFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var flag = shared.AddTemplateFlag
|
||||
var flag = shared.AddTemplatesFlag
|
||||
|
||||
assert.Equal(t, "add-template", flag.Name)
|
||||
|
||||
@ -108,10 +108,18 @@ func TestAddTemplateFlag(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddHTTPCodeFlag(t *testing.T) {
|
||||
func TestDisableTemplateNamesFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var flag = shared.AddHTTPCodeFlag
|
||||
var flag = shared.DisableTemplateNamesFlag
|
||||
|
||||
assert.Equal(t, "disable-template", flag.Name)
|
||||
}
|
||||
|
||||
func TestAddHTTPCodesFlag(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var flag = shared.AddHTTPCodesFlag
|
||||
|
||||
assert.Equal(t, "add-http-code", flag.Name)
|
||||
|
||||
|
@ -74,7 +74,8 @@ const defaultJSONFormat string = `{
|
||||
"request_id": {{ request_id | json }},
|
||||
"timestamp": {{ now.Unix }}
|
||||
}{{ end }}
|
||||
}`
|
||||
}
|
||||
` // an empty line at the end is important for better UX
|
||||
|
||||
const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<error>
|
||||
@ -92,7 +93,8 @@ const defaultXMLFormat string = `<?xml version="1.0" encoding="utf-8"?>
|
||||
<requestID>{{ request_id }}</requestID>
|
||||
<timestamp>{{ now.Unix }}</timestamp>
|
||||
</details>{{ end }}
|
||||
</error>`
|
||||
</error>
|
||||
` // an empty line at the end is important for better UX
|
||||
|
||||
const defaultPlainTextFormat string = `Error {{ code }}: {{ message }}{{ if description }}
|
||||
{{ description }}{{ end }}{{ if show_details }}
|
||||
@ -105,7 +107,8 @@ Ingress Name: {{ ingress_name }}
|
||||
Service Name: {{ service_name }}
|
||||
Service Port: {{ service_port }}
|
||||
Request ID: {{ request_id }}
|
||||
Timestamp: {{ now.Unix }}{{ end }}`
|
||||
Timestamp: {{ now.Unix }}{{ end }}
|
||||
` // an empty line at the end is important for better UX
|
||||
|
||||
//nolint:lll
|
||||
var defaultCodes = Codes{ //nolint:gochecknoglobals
|
||||
|
@ -12,8 +12,8 @@ const (
|
||||
RotationModeDisabled RotationMode = iota // do not rotate templates, default
|
||||
RotationModeRandomOnStartup // pick a random template on startup
|
||||
RotationModeRandomOnEachRequest // pick a random template on each request
|
||||
RotationModeRandomDaily // once a day switch to a random template
|
||||
RotationModeRandomHourly // once an hour switch to a random template
|
||||
RotationModeRandomDaily // once a day switch to a random template
|
||||
)
|
||||
|
||||
// String returns a human-readable representation of the rotation mode.
|
||||
@ -25,10 +25,10 @@ func (rm RotationMode) String() string {
|
||||
return "random-on-startup"
|
||||
case RotationModeRandomOnEachRequest:
|
||||
return "random-on-each-request"
|
||||
case RotationModeRandomDaily:
|
||||
return "random-daily"
|
||||
case RotationModeRandomHourly:
|
||||
return "random-hourly"
|
||||
case RotationModeRandomDaily:
|
||||
return "random-daily"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("RotationMode(%d)", rm)
|
||||
@ -40,8 +40,8 @@ func RotationModes() []RotationMode {
|
||||
RotationModeDisabled,
|
||||
RotationModeRandomOnStartup,
|
||||
RotationModeRandomOnEachRequest,
|
||||
RotationModeRandomDaily,
|
||||
RotationModeRandomHourly,
|
||||
RotationModeRandomDaily,
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,11 +77,11 @@ func ParseRotationMode[T string | []byte](text T) (RotationMode, error) {
|
||||
return RotationModeRandomOnStartup, nil
|
||||
case RotationModeRandomOnEachRequest.String():
|
||||
return RotationModeRandomOnEachRequest, nil
|
||||
case RotationModeRandomDaily.String():
|
||||
return RotationModeRandomDaily, nil
|
||||
case RotationModeRandomHourly.String():
|
||||
return RotationModeRandomHourly, nil
|
||||
case RotationModeRandomDaily.String():
|
||||
return RotationModeRandomDaily, nil
|
||||
}
|
||||
|
||||
return RotationMode(0), fmt.Errorf("unrecognized rotation mode: %q", mode)
|
||||
return RotationModeDisabled, fmt.Errorf("unrecognized rotation mode: %q", mode)
|
||||
}
|
||||
|
@ -27,8 +27,8 @@ func TestRotationModes(t *testing.T) {
|
||||
config.RotationModeDisabled,
|
||||
config.RotationModeRandomOnStartup,
|
||||
config.RotationModeRandomOnEachRequest,
|
||||
config.RotationModeRandomDaily,
|
||||
config.RotationModeRandomHourly,
|
||||
config.RotationModeRandomDaily,
|
||||
}, config.RotationModes())
|
||||
}
|
||||
|
||||
@ -39,8 +39,8 @@ func TestRotationModeStrings(t *testing.T) {
|
||||
"disabled",
|
||||
"random-on-startup",
|
||||
"random-on-each-request",
|
||||
"random-daily",
|
||||
"random-hourly",
|
||||
"random-daily",
|
||||
}, config.RotationModeStrings())
|
||||
}
|
||||
|
||||
|
@ -81,3 +81,25 @@ func (tpl templates) Has(name string) (found bool) { _, found = tpl[name]; retur
|
||||
|
||||
// Get returns the template content by the specified name, if it exists.
|
||||
func (tpl templates) Get(name string) (data string, ok bool) { data, ok = tpl[name]; return } //nolint:nlreturn
|
||||
|
||||
// Remove deletes the template by the specified name.
|
||||
func (tpl templates) Remove(name string) (ok bool) {
|
||||
if _, ok = tpl[name]; ok {
|
||||
delete(tpl, name)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RandomName picks a random template name. It returns an empty string if there are no templates.
|
||||
func (tpl templates) RandomName() string {
|
||||
if len(tpl) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for name := range tpl { // map iteration order is unpredictable (random) by design
|
||||
return name
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ func TestTemplates_Common(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []string{"_test11", "_test99", "test"}, tpl.Names()) // sorted
|
||||
assert.True(t, tpl.Has("_test99"))
|
||||
|
||||
assert.True(t, tpl.Remove("_test99"))
|
||||
assert.False(t, tpl.Has("_test99"))
|
||||
assert.False(t, tpl.Remove("_test99"))
|
||||
})
|
||||
|
||||
t.Run("adding template without a name should fail", func(t *testing.T) {
|
||||
@ -134,3 +138,27 @@ func TestTemplates_AddFromFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplates_RandomName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
tpl = templates{"test": "content", "test2": "content", "test3": "content"}
|
||||
|
||||
lastName = tpl.RandomName()
|
||||
changedCount int
|
||||
)
|
||||
|
||||
for range 1_000 {
|
||||
var name = tpl.RandomName()
|
||||
|
||||
if name != lastName {
|
||||
changedCount++
|
||||
}
|
||||
|
||||
lastName = name
|
||||
}
|
||||
|
||||
// I expect at least 100 different names in 1000 iterations
|
||||
assert.True(t, changedCount > 200)
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gh.tarampamp.am/error-pages/internal/config"
|
||||
"gh.tarampamp.am/error-pages/internal/logger"
|
||||
@ -86,7 +88,6 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
|
||||
tplProps.Host = r.Header.Get("Host") // the value of the `Host` header
|
||||
}
|
||||
|
||||
// TODO: ADD SUPPORT FOR THE RANDOM TEMPLATE AND SO ON
|
||||
// try to find the code message and description in the config and if not - use the standard status text or fallback
|
||||
if desc, found := cfg.Codes.Find(code); found {
|
||||
tplProps.Message = desc.Message
|
||||
@ -116,11 +117,13 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
|
||||
}
|
||||
|
||||
case format == htmlFormat:
|
||||
if tpl, found := cfg.Templates.Get(cfg.TemplateName); found {
|
||||
var templateName = templateToUse(cfg)
|
||||
|
||||
if tpl, found := cfg.Templates.Get(templateName); found {
|
||||
if content, err := template.Render(tpl, tplProps); err != nil {
|
||||
write(w, log, fmt.Sprintf(
|
||||
"<!DOCTYPE html>\n<html><body>Failed to render the HTML template %s: %s</body></html>",
|
||||
cfg.TemplateName,
|
||||
templateName,
|
||||
err.Error(),
|
||||
))
|
||||
} else {
|
||||
@ -128,7 +131,7 @@ func New(cfg *config.Config, log *logger.Logger) http.Handler { //nolint:funlen,
|
||||
}
|
||||
} else {
|
||||
write(w, log, fmt.Sprintf(
|
||||
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", cfg.TemplateName,
|
||||
"<!DOCTYPE html>\n<html><body>Template %s not found and cannot be used</body></html>", templateName,
|
||||
))
|
||||
}
|
||||
|
||||
@ -149,6 +152,54 @@ Supported formats: JSON, XML, HTML, Plain Text`)
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
templateChangedAt atomic.Pointer[time.Time] //nolint:gochecknoglobals // the time when the theme was changed last time
|
||||
pickedTemplate atomic.Pointer[string] //nolint:gochecknoglobals // the name of the randomly picked template
|
||||
)
|
||||
|
||||
// templateToUse decides which template to use based on the rotation mode and the last time the template was changed.
|
||||
func templateToUse(cfg *config.Config) string {
|
||||
switch rotationMode := cfg.RotationMode; rotationMode {
|
||||
case config.RotationModeDisabled:
|
||||
return cfg.TemplateName // not needed to do anything
|
||||
case config.RotationModeRandomOnStartup:
|
||||
return cfg.TemplateName // do nothing, the scope of this rotation mode is not here
|
||||
case config.RotationModeRandomOnEachRequest:
|
||||
return cfg.Templates.RandomName() // pick a random template on each request
|
||||
case config.RotationModeRandomHourly, config.RotationModeRandomDaily:
|
||||
var now, rndTemplate = time.Now(), cfg.Templates.RandomName()
|
||||
|
||||
if changedAt := templateChangedAt.Load(); changedAt == nil {
|
||||
// the template was not changed yet (first request)
|
||||
templateChangedAt.Store(&now)
|
||||
pickedTemplate.Store(&rndTemplate)
|
||||
|
||||
return rndTemplate
|
||||
} else {
|
||||
// is it time to change the template?
|
||||
if (rotationMode == config.RotationModeRandomHourly && changedAt.Hour() != now.Hour()) ||
|
||||
(rotationMode == config.RotationModeRandomDaily && changedAt.Day() != now.Day()) {
|
||||
templateChangedAt.Store(&now)
|
||||
pickedTemplate.Store(&rndTemplate)
|
||||
|
||||
return rndTemplate
|
||||
} else if lastUsed := pickedTemplate.Load(); lastUsed != nil {
|
||||
// time to change the template has not come yet, so use the last picked template
|
||||
return *lastUsed
|
||||
} else {
|
||||
// in case if the last picked template is not set, pick a random one and store it
|
||||
templateChangedAt.Store(&now)
|
||||
pickedTemplate.Store(&rndTemplate)
|
||||
|
||||
return rndTemplate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg.TemplateName // the fallback of the fallback :D
|
||||
}
|
||||
|
||||
// write the content to the response writer and log the error if any.
|
||||
func write[T string | []byte](w http.ResponseWriter, log *logger.Logger, content T) {
|
||||
var data []byte
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user