Files
go-omni/config/endpoints.go

120 lines
3.6 KiB
Go

package config
import (
"fmt"
"strings"
"unicode"
)
// EndpointsConfig controls routes installed by a generated service. A manually
// assembled App still registers its own routes explicitly.
type EndpointsConfig struct {
Liveness HealthEndpointConfig `json:"liveness"`
Readiness HealthEndpointConfig `json:"readiness"`
OpenAPI EndpointConfig `json:"openapi"`
}
type EndpointConfig struct {
Enabled bool `json:"enabled"`
Path string `json:"path"`
}
type HealthEndpointConfig struct {
Enabled bool `json:"enabled"`
Path string `json:"path"`
ResponseMode string `json:"responseMode"` // app (default) or raw
}
func DefaultEndpoints() EndpointsConfig {
return EndpointsConfig{
Liveness: HealthEndpointConfig{Enabled: true, Path: "/health/live", ResponseMode: "app"},
Readiness: HealthEndpointConfig{Enabled: true, Path: "/health/ready", ResponseMode: "app"},
OpenAPI: EndpointConfig{Enabled: true, Path: "/openapi.json"},
}
}
func (c EndpointsConfig) enabled() []struct{ name, path string } {
var result []struct{ name, path string }
for _, endpoint := range []struct {
name string
enabled bool
path string
}{
{"liveness", c.Liveness.Enabled, c.Liveness.Path},
{"readiness", c.Readiness.Enabled, c.Readiness.Path},
{"openapi", c.OpenAPI.Enabled, c.OpenAPI.Path},
} {
if endpoint.enabled {
result = append(result, struct{ name, path string }{endpoint.name, endpoint.path})
}
}
return result
}
func (c EndpointsConfig) Validate() error {
for _, mode := range []string{c.Liveness.ResponseMode, c.Readiness.ResponseMode} {
if mode != "" && mode != "app" && mode != "raw" {
return fmt.Errorf("endpoints: responseMode must be app or raw")
}
}
paths := map[string]string{}
for _, endpoint := range c.enabled() {
p := endpoint.path
if !strings.HasPrefix(p, "/") || strings.ContainsAny(p, ":*?#%\\{}") ||
strings.IndexFunc(p, func(r rune) bool { return unicode.IsSpace(r) || unicode.IsControl(r) }) >= 0 {
return fmt.Errorf("endpoints.%s.path must be an absolute literal URL path", endpoint.name)
}
parts := strings.Split(p[1:], "/")
for i, part := range parts {
if part == "." || part == ".." || (part == "" && i < len(parts)-1) {
return fmt.Errorf("endpoints.%s.path contains an invalid segment", endpoint.name)
}
}
if previous := paths[p]; previous != "" {
return fmt.Errorf("endpoints.%s and endpoints.%s share path %s", previous, endpoint.name, p)
}
paths[p] = endpoint.name
}
return nil
}
// EndpointRoute describes an already validated business route. Only GET
// patterns can overlap these built-in GET endpoints.
type EndpointRoute struct{ Method, Path string }
// ValidateRoutes rejects a built-in route that would shadow a business route
// or conflict with its catch-all. Disabled endpoints reserve no paths.
func (c EndpointsConfig) ValidateRoutes(routes ...EndpointRoute) error {
if err := c.Validate(); err != nil {
return err
}
for _, endpoint := range c.enabled() {
for _, route := range routes {
if strings.EqualFold(route.Method, "GET") && endpointOverlap(route.Path, endpoint.path) {
return fmt.Errorf("endpoints.%s path %s overlaps business route GET %s; change its path or disable it", endpoint.name, endpoint.path, route.Path)
}
}
}
return nil
}
func endpointOverlap(pattern, path string) bool {
a, b := strings.Split(strings.TrimPrefix(pattern, "/"), "/"), strings.Split(path[1:], "/")
for i, part := range a {
if i >= len(b) {
return false
}
if strings.HasPrefix(part, "*") {
return true
}
if strings.HasPrefix(part, ":") {
if b[i] == "" {
return false
}
} else if part != b[i] {
return false
}
}
return len(a) == len(b)
}