120 lines
3.6 KiB
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)
|
|
}
|