Template name: {{ $templateName }}
+ -
+
+
- {{ .Code }}: {{ .Message }} + +
From 4bb567b1d6d8769d7fc57609a731f4fcdd477dbb Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:11:21 +0400 Subject: [PATCH] =?UTF-8?q?wip:=20=F0=9F=94=95=20temporary=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 48 ++++++------- internal/cli/build/command.go | 112 +++++++++++++++++++++++++++++-- internal/cli/build/index.html | 122 ++++++++++++++++++++++++++++++++++ internal/cli/serve/command.go | 11 +-- internal/cli/shared/flags.go | 5 +- 5 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 internal/cli/build/index.html diff --git a/README.md b/README.md index 3182a7d..5b67b85 100644 --- a/README.md +++ b/README.md @@ -27,23 +27,23 @@ $ error-pages [GLOBAL FLAGS] serve [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:---------------------------:| -| `--listen="…"` (`-l`) | IP (v4 or v6) address to listen on | `0.0.0.0` | `LISTEN_ADDR` | -| `--port="…"` (`-p`) | TCP port number | `8080` | `LISTEN_PORT` | -| `--add-template="…"` | 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) | `[]` | *none* | -| `--disable-template="…"` | disable the specified template by its name | `[]` | *none* | -| `--add-http-code="…"` | 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, for example, '4**' will cover all 4xx codes, unless a more specific code was described previously) | `map[]` | *none* | -| `--json-format="…"` | override the default error page response in JSON format (Go templates are supported) | | `RESPONSE_JSON_FORMAT` | -| `--xml-format="…"` | override the default error page response in XML format (Go templates are supported) | | `RESPONSE_XML_FORMAT` | -| `--plaintext-format="…"` | override the default error page response in plain text format (Go templates are supported) | | `RESPONSE_PLAINTEXT_FORMAT` | -| `--template-name="…"` (`-t`) | name of the template to use for rendering error pages | `app-down` | `TEMPLATE_NAME` | -| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` | -| `--default-error-page="…"` | the code of the default (index page, when a code is not specified) error page to render | `404` | `DEFAULT_ERROR_PAGE` | -| `--send-same-http-code` | the HTTP response should have the same status code as the requested error page (by default, every response with an error page will have a status code of 200) | `false` | `SEND_SAME_HTTP_CODE` | -| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` | -| `--proxy-headers="…"` | listed here HTTP headers will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` | -| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` | +| Name | Description | Default value | Environment variables | +|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:---------------------------:| +| `--listen="…"` (`-l`) | the HTTP server will listen on this IP (v4 or v6) address (set 127.0.0.1 for localhost, 0.0.0.0 to listen on all interfaces, or specify a custom IP) | `0.0.0.0` | `LISTEN_ADDR` | +| `--port="…"` (`-p`) | the TPC port number for the HTTP server to listen on (0-65535) | `8080` | `LISTEN_PORT` | +| `--add-template="…"` | 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) | `[]` | *none* | +| `--disable-template="…"` | disable the specified template by its name (can be found useful to disable the builtit templates and use only custom ones) | `[]` | *none* | +| `--add-http-code="…"` (`--add-code`) | 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, for example, '4**' will cover all 4xx codes, unless a more specific code was described previously) | `map[]` | *none* | +| `--json-format="…"` | override the default error page response in JSON format (Go templates are supported; the error page will use this template if the client requests JSON content type) | | `RESPONSE_JSON_FORMAT` | +| `--xml-format="…"` | override the default error page response in XML format (Go templates are supported; the error page will use this template if the client requests XML content type) | | `RESPONSE_XML_FORMAT` | +| `--plaintext-format="…"` | override the default error page response in plain text format (Go templates are supported; the error page will use this template if the client requests PlainText content type or does not specify any) | | `RESPONSE_PLAINTEXT_FORMAT` | +| `--template-name="…"` (`-t`) | name of the template to use for rendering error pages (builtin templates: app-down, cats, connection, ghost, hacker-terminal, l7, lost-in-space, noise, orient, shuffle) | `app-down` | `TEMPLATE_NAME` | +| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` | +| `--default-error-page="…"` | the code of the default (index page, when a code is not specified) error page to render | `404` | `DEFAULT_ERROR_PAGE` | +| `--send-same-http-code` | the HTTP response should have the same status code as the requested error page (by default, every response with an error page will have a status code of 200) | `false` | `SEND_SAME_HTTP_CODE` | +| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` | +| `--proxy-headers="…"` | listed here HTTP headers will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` | +| `--rotation-mode="…"` | templates automatic rotation mode (disabled/random-on-startup/random-on-each-request/random-hourly/random-daily) | `disabled` | `TEMPLATES_ROTATION_MODE` | ### `build` command (aliases: `b`) @@ -57,12 +57,14 @@ $ error-pages [GLOBAL FLAGS] build [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------:|:---------------------:| -| `--add-template="…"` | 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) | `[]` | *none* | -| `--disable-template="…"` | disable the specified template by its name | `[]` | *none* | -| `--add-http-code="…"` | 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, for example, '4**' will cover all 4xx codes, unless a more specific code was described previously) | `map[]` | *none* | -| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` | +| Name | Description | Default value | Environment variables | +|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------:|:---------------------:| +| `--add-template="…"` | 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) | `[]` | *none* | +| `--disable-template="…"` | disable the specified template by its name (can be found useful to disable the builtit templates and use only custom ones) | `[]` | *none* | +| `--add-http-code="…"` (`--add-code`) | 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, for example, '4**' will cover all 4xx codes, unless a more specific code was described previously) | `map[]` | *none* | +| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` | +| `--index` (`-i`) | generate index.html file with links to all error pages | `false` | *none* | +| `--target-dir="…"` (`--out`, `--dir`, `-o`) | directory to put the built error pages into | `.` | *none* | ### `healthcheck` command (aliases: `chk`, `health`, `check`) diff --git a/internal/cli/build/command.go b/internal/cli/build/command.go index 4720aae..a5dcd8c 100644 --- a/internal/cli/build/command.go +++ b/internal/cli/build/command.go @@ -2,24 +2,34 @@ package build import ( "context" + _ "embed" "errors" "fmt" + "html/template" "os" + "path" "path/filepath" + "slices" + "strconv" + "strings" "github.com/urfave/cli/v3" "gh.tarampamp.am/error-pages/internal/cli/shared" "gh.tarampamp.am/error-pages/internal/config" "gh.tarampamp.am/error-pages/internal/logger" + appTemplate "gh.tarampamp.am/error-pages/internal/template" ) +//go:embed index.html +var indexHtml string + type command struct { c *cli.Command opt struct { - createIndex bool - targetDir string + createIndex bool + targetDirAbsPath string } } @@ -70,7 +80,7 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit Action: func(ctx context.Context, c *cli.Command) error { cfg.L10n.Disable = c.Bool(disableL10nFlag.Name) cmd.opt.createIndex = c.Bool(createIndexFlag.Name) - cmd.opt.targetDir, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator + cmd.opt.targetDirAbsPath, _ = filepath.Abs(c.String(targetDirFlag.Name)) // an error checked by [os.Stat] validator // add templates from files to the configuration if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { @@ -112,6 +122,13 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit return errors.New("no templates specified") } + log.Info("Building error pages", + logger.String("targetDir", cmd.opt.targetDirAbsPath), + logger.Strings("templates", cfg.Templates.Names()...), + logger.Bool("index", cmd.opt.createIndex), + logger.Bool("l10n", !cfg.L10n.Disable), + ) + return cmd.Run(ctx, log, &cfg) }, Flags: []cli.Flag{ @@ -127,10 +144,97 @@ func NewCommand(log *logger.Logger) *cli.Command { //nolint:funlen,gocognit return cmd.c } -func (cmd *command) Run( +func (cmd *command) Run( //nolint:funlen ctx context.Context, log *logger.Logger, cfg *config.Config, ) error { + type historyItem struct{ Code, Message, RelativePath string } + + var history = make(map[string][]historyItem, len(cfg.Codes)*len(cfg.Templates)) // map[template_name]codes + + for templateName, templateContent := range cfg.Templates { + log.Debug("Processing template", logger.String("name", templateName)) + + for code, codeDescription := range cfg.Codes { + if err := createDirectory(filepath.Join(cmd.opt.targetDirAbsPath, templateName)); err != nil { + return fmt.Errorf("cannot create directory for template '%s': %w", templateName, err) + } + + var codeAsUint, codeParsingErr = strconv.ParseUint(code, 10, 32) + if codeParsingErr != nil { + log.Warn("Cannot parse code", logger.String("code", code)) + + continue + } + + var outFilePath = path.Join(cmd.opt.targetDirAbsPath, templateName, code+".html") + + if content, renderErr := appTemplate.Render(templateContent, appTemplate.Props{ + Code: uint16(codeAsUint), + Message: codeDescription.Message, + Description: codeDescription.Description, + L10nDisabled: cfg.L10n.Disable, + ShowRequestDetails: false, + }); renderErr == nil { + if err := os.WriteFile(outFilePath, []byte(content), os.FileMode(0664)); err != nil { //nolint:mnd + return err + } + } else { + return fmt.Errorf("cannot render template '%s': %w", templateName, renderErr) + } + + log.Debug("Page built", logger.String("template", templateName), logger.String("code", code)) + + history[templateName] = append(history[templateName], historyItem{ + Code: code, + Message: codeDescription.Message, + RelativePath: "." + strings.TrimPrefix(outFilePath, cmd.opt.targetDirAbsPath), // to make it relative + }) + } + } + + if cmd.opt.createIndex { + log.Debug("Creating the index file") + + for name := range history { + slices.SortFunc(history[name], func(a, b historyItem) int { return strings.Compare(a.Code, b.Code) }) + } + + indexTpl, tplErr := template.New("index").Parse(indexHtml) + if tplErr != nil { + return tplErr + } + + var buf strings.Builder + + if err := indexTpl.Execute(&buf, history); err != nil { + return err + } + + return os.WriteFile( + filepath.Join(cmd.opt.targetDirAbsPath, "index.html"), + []byte(buf.String()), + os.FileMode(0664), //nolint:mnd + ) + } + + return nil +} + +func createDirectory(path string) error { + var stat, err = os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return os.MkdirAll(path, os.FileMode(0775)) //nolint:mnd + } + + return err + } + + if !stat.IsDir() { + return errors.New("is not a directory") + } + return nil } diff --git a/internal/cli/build/index.html b/internal/cli/build/index.html new file mode 100644 index 0000000..0374c23 --- /dev/null +++ b/internal/cli/build/index.html @@ -0,0 +1,122 @@ + + +
+ + +{{ $templateName }}