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