mirror of
https://github.com/rancher/norman.git
synced 2025-06-03 04:29:26 +00:00
Added enable function to schemas. This allows filtering of schemas which is a required part of feature flagging in rancher. Added addFeature functions to controller template for adding handlers that can be disabled if their associated feature is disabled.
317 lines
7.8 KiB
Go
317 lines
7.8 KiB
Go
package parse
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"sort"
|
|
|
|
"github.com/rancher/norman/api/builtin"
|
|
"github.com/rancher/norman/httperror"
|
|
"github.com/rancher/norman/types"
|
|
"github.com/rancher/norman/urlbuilder"
|
|
)
|
|
|
|
const (
|
|
maxFormSize = 2 * 1 << 20
|
|
)
|
|
|
|
var (
|
|
multiSlashRegexp = regexp.MustCompile("//+")
|
|
allowedFormats = map[string]bool{
|
|
"html": true,
|
|
"json": true,
|
|
"yaml": true,
|
|
}
|
|
)
|
|
|
|
type ParsedURL struct {
|
|
Version *types.APIVersion
|
|
SchemasVersion *types.APIVersion
|
|
Type string
|
|
ID string
|
|
Link string
|
|
Method string
|
|
Action string
|
|
SubContext map[string]string
|
|
SubContextPrefix string
|
|
Query url.Values
|
|
}
|
|
|
|
type ResolverFunc func(typeName string, context *types.APIContext) error
|
|
|
|
type URLParser func(schema *types.Schemas, url *url.URL) (ParsedURL, error)
|
|
|
|
func DefaultURLParser(schemas *types.Schemas, url *url.URL) (ParsedURL, error) {
|
|
result := ParsedURL{}
|
|
|
|
path := url.EscapedPath()
|
|
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
|
schemaVersion, version, prefix, parts, subContext := parseVersionAndSubContext(schemas, path)
|
|
|
|
if version == nil {
|
|
return result, nil
|
|
}
|
|
|
|
result.Version = version
|
|
result.SchemasVersion = schemaVersion
|
|
result.SubContext = subContext
|
|
result.SubContextPrefix = prefix
|
|
result.Action, result.Method = parseAction(url)
|
|
result.Query = url.Query()
|
|
|
|
result.Type = safeIndex(parts, 0)
|
|
result.ID = safeIndex(parts, 1)
|
|
result.Link = safeIndex(parts, 2)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, urlParser URLParser, resolverFunc ResolverFunc) (*types.APIContext, error) {
|
|
var err error
|
|
|
|
result := types.NewAPIContext(req, rw, schemas)
|
|
result.Method = parseMethod(req)
|
|
result.ResponseFormat = parseResponseFormat(req)
|
|
result.URLBuilder, _ = urlbuilder.New(req, types.APIVersion{}, schemas)
|
|
|
|
// The response format is guarenteed to be set even in the event of an error
|
|
parsedURL, err := urlParser(schemas, req.URL)
|
|
// wait to check error, want to set as much as possible
|
|
|
|
result.SubContext = parsedURL.SubContext
|
|
result.Type = parsedURL.Type
|
|
result.ID = parsedURL.ID
|
|
result.Link = parsedURL.Link
|
|
result.Action = parsedURL.Action
|
|
result.Query = parsedURL.Query
|
|
if parsedURL.Method != "" {
|
|
result.Method = parsedURL.Method
|
|
}
|
|
|
|
result.Version = parsedURL.Version
|
|
result.SchemasVersion = parsedURL.SchemasVersion
|
|
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if result.Version == nil {
|
|
result.Method = http.MethodGet
|
|
result.URLBuilder, err = urlbuilder.New(req, types.APIVersion{}, result.Schemas)
|
|
result.Type = "apiRoot"
|
|
result.Schema = result.Schemas.Schema(&builtin.Version, "apiRoot")
|
|
return result, nil
|
|
}
|
|
|
|
result.URLBuilder, err = urlbuilder.New(req, *result.Version, result.Schemas)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if parsedURL.SubContextPrefix != "" {
|
|
result.URLBuilder.SetSubContext(parsedURL.SubContextPrefix)
|
|
}
|
|
|
|
if err := resolverFunc(result.Type, result); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if result.Schema == nil {
|
|
if result.Type != "" {
|
|
err = httperror.NewAPIError(httperror.NotFound, "failed to find schema "+result.Type)
|
|
}
|
|
result.Method = http.MethodGet
|
|
result.Type = "apiRoot"
|
|
result.Schema = result.Schemas.Schema(&builtin.Version, "apiRoot")
|
|
result.ID = result.Version.Path
|
|
return result, err
|
|
}
|
|
|
|
if schema := result.Schema; schema.Enabled != nil {
|
|
if !schema.Enabled() {
|
|
return result, httperror.NewAPIError(httperror.ActionNotAvailable, "schema disabled "+schema.ID)
|
|
}
|
|
}
|
|
|
|
result.Type = result.Schema.ID
|
|
|
|
if err := ValidateMethod(result); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func versionsForPath(schemas *types.Schemas, path string) []types.APIVersion {
|
|
var matchedVersion []types.APIVersion
|
|
for _, version := range schemas.Versions() {
|
|
if strings.HasPrefix(path, version.Path) {
|
|
afterPath := path[len(version.Path):]
|
|
// if version.Path is /v3/cluster allow /v3/clusters but not /v3/clusterstuff
|
|
if len(afterPath) < 3 || strings.Contains(afterPath[:3], "/") {
|
|
matchedVersion = append(matchedVersion, version)
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(matchedVersion, func(i, j int) bool {
|
|
return len(matchedVersion[i].Path) > len(matchedVersion[j].Path)
|
|
})
|
|
return matchedVersion
|
|
}
|
|
|
|
func parseVersionAndSubContext(schemas *types.Schemas, escapedPath string) (*types.APIVersion, *types.APIVersion, string, []string, map[string]string) {
|
|
versions := versionsForPath(schemas, escapedPath)
|
|
if len(versions) == 0 {
|
|
return nil, nil, "", nil, nil
|
|
}
|
|
version := &versions[0]
|
|
|
|
if strings.HasSuffix(escapedPath, "/") {
|
|
escapedPath = escapedPath[:len(escapedPath)-1]
|
|
}
|
|
|
|
versionParts := strings.Split(version.Path, "/")
|
|
pp := strings.Split(escapedPath, "/")
|
|
var pathParts []string
|
|
for _, p := range pp {
|
|
part, err := url.PathUnescape(p)
|
|
if err == nil {
|
|
pathParts = append(pathParts, part)
|
|
} else {
|
|
pathParts = append(pathParts, p)
|
|
}
|
|
}
|
|
|
|
paths := pathParts[len(versionParts):]
|
|
|
|
if !version.SubContext || len(versions) < 2 {
|
|
return nil, version, "", paths, nil
|
|
}
|
|
|
|
// Handle the special case of /v3/clusters/schema(s)
|
|
if len(paths) >= 1 && (paths[0] == "schema" || paths[0] == "schemas") {
|
|
return nil, version, "", paths, nil
|
|
}
|
|
|
|
if len(paths) < 2 {
|
|
// Handle case like /v3/clusters/foo where /v3 and /v3/clusters are API versions.
|
|
// In this situation you want the version to be /v3 and the path "clusters", "foo"
|
|
newVersion := versions[0]
|
|
if len(paths) > 0 {
|
|
newVersion.Path = newVersion.Path + "/" + paths[0]
|
|
}
|
|
return &newVersion, &versions[1], "", pathParts[len(versionParts)-1:], nil
|
|
}
|
|
|
|
// Length is always >= 3
|
|
|
|
attrs := map[string]string{
|
|
version.SubContextSchema: paths[0],
|
|
}
|
|
|
|
for i, version := range versions {
|
|
schema := schemas.Schema(&version, paths[1])
|
|
if schema != nil {
|
|
if i == 0 {
|
|
break
|
|
}
|
|
return nil, &version, "", paths[1:], attrs
|
|
}
|
|
}
|
|
|
|
return nil, version, "/" + paths[0], paths[1:], attrs
|
|
}
|
|
|
|
func DefaultResolver(typeName string, apiContext *types.APIContext) error {
|
|
if typeName == "" {
|
|
return nil
|
|
}
|
|
|
|
schema := apiContext.Schemas.Schema(apiContext.Version, typeName)
|
|
if schema == nil && (typeName == builtin.Schema.ID || typeName == builtin.Schema.PluralName) {
|
|
// Schemas are special, we include it as though part of the API request version
|
|
schema = apiContext.Schemas.Schema(&builtin.Version, typeName)
|
|
}
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
|
|
apiContext.Schema = schema
|
|
return nil
|
|
}
|
|
|
|
func safeIndex(slice []string, index int) string {
|
|
if index >= len(slice) {
|
|
return ""
|
|
}
|
|
return slice[index]
|
|
}
|
|
|
|
func parseResponseFormat(req *http.Request) string {
|
|
format := req.URL.Query().Get("_format")
|
|
|
|
if format != "" {
|
|
format = strings.TrimSpace(strings.ToLower(format))
|
|
}
|
|
|
|
/* Format specified */
|
|
if allowedFormats[format] {
|
|
return format
|
|
}
|
|
|
|
// User agent has Mozilla and browser accepts */*
|
|
if IsBrowser(req, true) {
|
|
return "html"
|
|
}
|
|
|
|
if isYaml(req) {
|
|
return "yaml"
|
|
}
|
|
return "json"
|
|
}
|
|
|
|
func isYaml(req *http.Request) bool {
|
|
return strings.Contains(req.Header.Get("Accept"), "application/yaml")
|
|
}
|
|
|
|
func parseMethod(req *http.Request) string {
|
|
method := req.URL.Query().Get("_method")
|
|
if method == "" {
|
|
method = req.Method
|
|
}
|
|
return method
|
|
}
|
|
|
|
func parseAction(url *url.URL) (string, string) {
|
|
action := url.Query().Get("action")
|
|
if action == "remove" {
|
|
return "", http.MethodDelete
|
|
}
|
|
|
|
return action, ""
|
|
}
|
|
|
|
func Body(req *http.Request) (map[string]interface{}, error) {
|
|
req.ParseMultipartForm(maxFormSize)
|
|
if req.MultipartForm != nil {
|
|
return valuesToBody(req.MultipartForm.Value), nil
|
|
}
|
|
|
|
if req.PostForm != nil && len(req.PostForm) > 0 {
|
|
return valuesToBody(map[string][]string(req.Form)), nil
|
|
}
|
|
|
|
return ReadBody(req)
|
|
}
|
|
|
|
func valuesToBody(input map[string][]string) map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
for k, v := range input {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|