wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-26 14:52:21 +04:00
parent 2a1fa5c108
commit 015e686635
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
9 changed files with 180 additions and 52 deletions

View File

@ -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,
},
}

View File

@ -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, " +

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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 ""
}

View File

@ -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)
}

View File

@ -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