Added regex option to forward rules

This commit is contained in:
2026-03-31 12:45:56 +02:00
parent 55348057a0
commit dcc8201099
3 changed files with 49 additions and 13 deletions
+9 -1
View File
@@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"log"
"os" "os"
"path/filepath" "path/filepath"
"quay/internal/config" "quay/internal/config"
@@ -18,7 +19,14 @@ func NewStaticHandler(storagePath string, siteMap map[string]config.SiteConfig)
urlPath := filepath.Clean(c.Path()) urlPath := filepath.Clean(c.Path())
for _, rule := range site.ForwardRules { for _, rule := range site.ForwardRules {
if rule.Source == urlPath { if rule.Regex && rule.Compiled != nil {
match := rule.Compiled.FindStringSubmatchIndex(urlPath)
if match != nil {
dest := rule.Compiled.ExpandString([]byte{}, rule.Destination, urlPath, match)
log.Printf("Forwarding %s to %s based on regex rule", urlPath, dest)
return c.Redirect().Status(rule.StatusCode).To(string(dest))
}
} else if rule.Source == urlPath {
return c.Redirect().Status(rule.StatusCode).To(rule.Destination) return c.Redirect().Status(rule.StatusCode).To(rule.Destination)
} }
} }
+9 -2
View File
@@ -12,9 +12,16 @@ sites:
- source: /npm - source: /npm
destination: https://www.npmjs.com/package/my-lib destination: https://www.npmjs.com/package/my-lib
status_code: 302 status_code: 302
- source: /github
destination: https://github.com/yourname/my-lib - source: ^/docs/v(\d+)$
destination: https://v$1.docs.my-lib.com/
status_code: 301
regex: true
- source: ^/blog/(?P<slug>[^/]+)$
destination: /posts/${slug}
status_code: 302 status_code: 302
regex: true
- repo: portfolio - repo: portfolio
owner: yourname owner: yourname
+31 -10
View File
@@ -1,7 +1,10 @@
package config package config
import ( import (
"fmt"
"os" "os"
"regexp"
"strconv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -10,6 +13,8 @@ type ForwardRule struct {
Source string `yaml:"source"` Source string `yaml:"source"`
Destination string `yaml:"destination"` Destination string `yaml:"destination"`
StatusCode int `yaml:"status_code"` StatusCode int `yaml:"status_code"`
Regex bool `yaml:"regex"`
Compiled *regexp.Regexp
} }
type SiteConfig struct { type SiteConfig struct {
@@ -40,32 +45,42 @@ func (c Error) Error() string {
func validateConfig(config *Config) error { func validateConfig(config *Config) error {
for i, site := range config.Sites { for i, site := range config.Sites {
if site.Name == "" { if site.Name == "" {
return &Error{Field: "sites[" + string(i) + "].name", Message: "Name is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].name", Message: "Name is required"}
} }
if site.Repo == "" { if site.Repo == "" {
return &Error{Field: "sites[" + string(i) + "].repo", Message: "Repo is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].repo", Message: "Repo is required"}
} }
if site.Owner == "" { if site.Owner == "" {
return &Error{Field: "sites[" + string(i) + "].owner", Message: "Owner is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].owner", Message: "Owner is required"}
} }
if site.Branch == "" { if site.Branch == "" {
return &Error{Field: "sites[" + string(i) + "].branch", Message: "Branch is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].branch", Message: "Branch is required"}
} }
if site.Domain == "" { if site.Domain == "" {
return &Error{Field: "sites[" + string(i) + "].domain", Message: "Domain is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].domain", Message: "Domain is required"}
} }
for j, rule := range site.ForwardRules { for j, rule := range site.ForwardRules {
if rule.Source == "" { if rule.Source == "" {
return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].source", Message: "Source is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].source", Message: "Source is required"}
} }
if rule.Destination == "" { if rule.Destination == "" {
return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].destination", Message: "Destination is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].destination", Message: "Destination is required"}
} }
if rule.StatusCode == 0 { if rule.StatusCode == 0 {
return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].status_code", Message: "Status code is required"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].status_code", Message: "Status code is required"}
} }
if rule.StatusCode < 300 || rule.StatusCode >= 400 { if rule.StatusCode < 300 || rule.StatusCode >= 400 {
return &Error{Field: "sites[" + string(i) + "].forward_rules[" + string(j) + "].status_code", Message: "Status code must be a valid redirect code (300-399)"} return &Error{Field: "sites[" + strconv.Itoa(i) + "].forward_rules[" + strconv.Itoa(j) + "].status_code", Message: "Status code must be a valid redirect code (300-399)"}
}
if rule.Regex {
re, err := regexp.Compile(rule.Source)
if err != nil {
return &Error{
Field: fmt.Sprintf("sites[%d].forward_rules[%d].source", i, j),
Message: "Invalid regex: " + err.Error(),
}
}
config.Sites[i].ForwardRules[j].Compiled = re
} }
} }
} }
@@ -90,7 +105,13 @@ func Load(path string) (*Config, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
expanded := os.Expand(string(f), os.Getenv) expanded := os.Expand(string(f), func(key string) string {
val, ok := os.LookupEnv(key)
if !ok {
return "${" + key + "}" // env vars that aren't set are left as is to not cause errors with regex patterns
}
return val
})
var config Config var config Config
err = yaml.Unmarshal([]byte(expanded), &config) err = yaml.Unmarshal([]byte(expanded), &config)
if err != nil { if err != nil {