diff --git a/.dockerignore b/.dockerignore
index 6e43c2a9..2e7d9023 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,3 +2,4 @@
./.dapper
./dist
./.trash-cache
+./.idea
diff --git a/.gitignore b/.gitignore
index 81d1b990..585d0fae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/.idea
/.dapper
/bin
/dist
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 9758b7ad..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/norman.iml b/.idea/norman.iml
deleted file mode 100644
index c956989b..00000000
--- a/.idea/norman.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 89f58703..00000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,818 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- true
- DEFINITION_ORDER
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Abstraction issuesJava
-
-
- Assignment issuesJava
-
-
- Bitwise operation issuesJava
-
-
- Class metricsJava
-
-
- Class structureJava
-
-
- Cloning issuesJava
-
-
- Code maturity issuesJava
-
-
- Code style issuesJava
-
-
- Compiler issuesJava
-
-
- Concurrency annotation issuesJava
-
-
- Control flow issuesJava
-
-
- Data flow issuesJava
-
-
- Declaration redundancyJava
-
-
- Dependency issuesJava
-
-
- Encapsulation issuesJava
-
-
- Error handlingJava
-
-
- Finalization issuesJava
-
-
- GPath inspectionsGroovy
-
-
- General
-
-
- GeneralJava
-
-
- Groovy
-
-
- ImportsJava
-
-
- Inheritance issuesJava
-
-
- Initialization issuesJava
-
-
- Internationalization issuesJava
-
-
- J2ME issuesJava
-
-
- JUnit issuesJava
-
-
- Java
-
-
- Java 5Java language level migration aidsJava
-
-
- Java 7Java language level migration aidsJava
-
-
- Java 8Java language level migration aidsJava
-
-
- Java language level issuesJava
-
-
- Java language level migration aidsJava
-
-
- JavaBeans issuesJava
-
-
- JavaFX
-
-
- Javadoc issuesJava
-
-
- Kotlin
-
-
- Language Injection
-
-
- Logging issuesJava
-
-
- Manifest
-
-
- Memory issuesJava
-
-
- Method metricsJava
-
-
- Modularization issuesJava
-
-
- Naming ConventionsGroovy
-
-
- Naming conventionsJava
-
-
- Numeric issuesJava
-
-
- Packaging issuesJava
-
-
- Performance issuesJava
-
-
- Portability issuesJava
-
-
- Potentially confusing code constructsGroovy
-
-
- Probable bugsGroovy
-
-
- Probable bugsJava
-
-
- Properties Files
-
-
- Properties FilesJava
-
-
- Resource management issuesJava
-
-
- Security issuesJava
-
-
- Serialization issuesJava
-
-
- StyleGroovy
-
-
- Threading issuesGroovy
-
-
- Threading issuesJava
-
-
- Verbose or redundant code constructsJava
-
-
- Visibility issuesJava
-
-
- toString() issuesJava
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index b5579bb8..1be5cd48 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,73 @@
-norman
+Norman
========
-A microservice that does micro things.
+An API framework for Building [Rancher Style APIs](https://github.com/rancher/api-spec/) backed by K8s CustomResources.
## Building
`make`
+## Example
-## Running
+Refer to `examples/`
+
+```go
+package main
+
+import (
+ "context"
+ "net/http"
+
+ "fmt"
+
+ "os"
+
+ "github.com/rancher/go-rancher/v3"
+ "github.com/rancher/norman/api/crd"
+)
+
+var (
+ version = client.APIVersion{
+ Version: "v1",
+ Group: "io.cattle.core.example",
+ Path: "/example/v1",
+ }
+
+ Foo = client.Schema{
+ ID: "foo",
+ Version: version,
+ ResourceFields: map[string]*client.Field{
+ "foo": {
+ Type: "string",
+ Create: true,
+ Update: true,
+ },
+ "name": {
+ Type: "string",
+ Create: true,
+ Required: true,
+ },
+ },
+ }
+
+ Schemas = client.NewSchemas().
+ AddSchema(&Foo)
+)
+
+func main() {
+ server, err := crd.NewAPIServer(context.Background(), os.Getenv("KUBECONFIG"), Schemas)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("Listening on 0.0.0.0:1234")
+ http.ListenAndServe("0.0.0.0:1234", server)
+}
+```
-`./bin/norman`
## License
-Copyright (c) 2014-2016 [Rancher Labs, Inc.](http://rancher.com)
+Copyright (c) 2014-2017 [Rancher Labs, Inc.](http://rancher.com)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/api/builtin/builtin.go b/api/builtin/builtin.go
new file mode 100644
index 00000000..b3cfe3d2
--- /dev/null
+++ b/api/builtin/builtin.go
@@ -0,0 +1,186 @@
+package builtin
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/store/empty"
+ "github.com/rancher/norman/store/schema"
+ "github.com/rancher/norman/types"
+)
+
+var (
+ Version = types.APIVersion{
+ Group: "io.cattle.builtin",
+ Version: "v3",
+ Path: "/v3",
+ }
+
+ Schema = types.Schema{
+ ID: "schema",
+ Version: Version,
+ CollectionMethods: []string{"GET"},
+ ResourceMethods: []string{"GET"},
+ ResourceFields: map[string]types.Field{
+ "collectionActions": {Type: "map[json]"},
+ "collectionFields": {Type: "map[json]"},
+ "collectionFitlers": {Type: "map[json]"},
+ "collectionMethods": {Type: "array[string]"},
+ "pluralName": {Type: "string"},
+ "resourceActions": {Type: "map[json]"},
+ "resourceFields": {Type: "map[json]"},
+ "resourceMethods": {Type: "array[string]"},
+ "version": {Type: "map[json]"},
+ },
+ Formatter: SchemaFormatter,
+ Store: schema.NewSchemaStore(),
+ }
+
+ Error = types.Schema{
+ ID: "error",
+ Version: Version,
+ ResourceMethods: []string{},
+ CollectionMethods: []string{},
+ ResourceFields: map[string]types.Field{
+ "code": {Type: "string"},
+ "detail": {Type: "string"},
+ "message": {Type: "string"},
+ "status": {Type: "int"},
+ },
+ }
+
+ Collection = types.Schema{
+ ID: "error",
+ Version: Version,
+ ResourceMethods: []string{},
+ CollectionMethods: []string{},
+ ResourceFields: map[string]types.Field{
+ "data": {Type: "array[json]"},
+ "pagination": {Type: "map[json]"},
+ "sort": {Type: "map[json]"},
+ "filters": {Type: "map[json]"},
+ },
+ }
+
+ APIRoot = types.Schema{
+ ID: "apiRoot",
+ Version: Version,
+ ResourceMethods: []string{},
+ CollectionMethods: []string{},
+ ResourceFields: map[string]types.Field{
+ "apiVersion": {Type: "map[json]"},
+ "path": {Type: "string"},
+ },
+ Formatter: APIRootFormatter,
+ Store: NewAPIRootStore(nil),
+ }
+
+ Schemas = types.NewSchemas().
+ AddSchema(&Schema).
+ AddSchema(&Error).
+ AddSchema(&Collection).
+ AddSchema(&APIRoot)
+)
+
+func apiVersionFromMap(apiVersion map[string]interface{}) types.APIVersion {
+ path, _ := apiVersion["path"].(string)
+ version, _ := apiVersion["version"].(string)
+ group, _ := apiVersion["group"].(string)
+
+ return types.APIVersion{
+ Path: path,
+ Version: version,
+ Group: group,
+ }
+}
+
+func SchemaFormatter(apiContext *types.APIContext, resource *types.RawResource) {
+ data, _ := resource.Values["version"].(map[string]interface{})
+ apiVersion := apiVersionFromMap(data)
+
+ schema := apiContext.Schemas.Schema(&apiVersion, resource.ID)
+ collectionLink := getSchemaCollectionLink(apiContext, schema)
+ if collectionLink != "" {
+ resource.Links["collection"] = collectionLink
+ }
+}
+
+func getSchemaCollectionLink(apiContext *types.APIContext, schema *types.Schema) string {
+ if schema != nil && contains(schema.CollectionMethods, http.MethodGet) {
+ return apiContext.URLBuilder.Collection(schema)
+ }
+ return ""
+}
+
+func contains(list []string, needle string) bool {
+ for _, v := range list {
+ if v == needle {
+ return true
+ }
+ }
+ return false
+}
+
+func APIRootFormatter(apiContext *types.APIContext, resource *types.RawResource) {
+ path, _ := resource.Values["path"].(string)
+ if path == "" {
+ return
+ }
+
+ resource.Links["root"] = apiContext.URLBuilder.RelativeToRoot(path)
+
+ data, _ := resource.Values["apiVersion"].(map[string]interface{})
+ apiVersion := apiVersionFromMap(data)
+
+ for name, schema := range apiContext.Schemas.SchemasForVersion(apiVersion) {
+ collectionLink := getSchemaCollectionLink(apiContext, schema)
+ if collectionLink != "" {
+ resource.Links[name] = collectionLink
+ }
+ }
+}
+
+type APIRootStore struct {
+ empty.Store
+ roots []string
+}
+
+func NewAPIRootStore(roots []string) types.Store {
+ return &APIRootStore{roots: roots}
+}
+
+func (a *APIRootStore) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
+ for _, version := range apiContext.Schemas.Versions() {
+ if version.Path == id {
+ return apiVersionToAPIRootMap(version), nil
+ }
+ }
+ return nil, nil
+}
+
+func (a *APIRootStore) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
+ roots := []map[string]interface{}{}
+
+ for _, version := range apiContext.Schemas.Versions() {
+ roots = append(roots, apiVersionToAPIRootMap(version))
+ }
+
+ for _, root := range a.roots {
+ roots = append(roots, map[string]interface{}{
+ "path": root,
+ })
+ }
+
+ return roots, nil
+}
+
+func apiVersionToAPIRootMap(version types.APIVersion) map[string]interface{} {
+ return map[string]interface{}{
+ "type": "/v3/apiRoot",
+ "apiVersion": map[string]interface{}{
+ "version": version.Version,
+ "group": version.Group,
+ "path": version.Path,
+ },
+ "path": version.Path,
+ }
+}
diff --git a/api/handlers/create.go b/api/handlers/create.go
new file mode 100644
index 00000000..68384d31
--- /dev/null
+++ b/api/handlers/create.go
@@ -0,0 +1,31 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/types"
+)
+
+func CreateHandler(request *types.APIContext) error {
+ var err error
+
+ validator := request.Schema.Validator
+ if validator != nil {
+ if err := validator(request, request.Body); err != nil {
+ return err
+ }
+ }
+
+ data := request.Body
+
+ store := request.Schema.Store
+ if store != nil {
+ data, err = store.Create(request, request.Schema, data)
+ if err != nil {
+ return err
+ }
+ }
+
+ request.WriteResponse(http.StatusCreated, data)
+ return nil
+}
diff --git a/api/handlers/delete.go b/api/handlers/delete.go
new file mode 100644
index 00000000..b6718bf2
--- /dev/null
+++ b/api/handlers/delete.go
@@ -0,0 +1,20 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/types"
+)
+
+func DeleteHandler(request *types.APIContext) error {
+ store := request.Schema.Store
+ if store != nil {
+ err := store.Delete(request, request.Schema, request.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ request.WriteResponse(http.StatusOK, nil)
+ return nil
+}
diff --git a/api/handlers/list.go b/api/handlers/list.go
new file mode 100644
index 00000000..714a3105
--- /dev/null
+++ b/api/handlers/list.go
@@ -0,0 +1,36 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/parse"
+ "github.com/rancher/norman/types"
+)
+
+func ListHandler(request *types.APIContext) error {
+ var (
+ err error
+ data interface{}
+ )
+
+ store := request.Schema.Store
+ if store == nil {
+ return nil
+ }
+
+ if request.ID == "" {
+ request.QueryOptions = parse.QueryOptions(request.Request, request.Schema)
+ data, err = store.List(request, request.Schema, request.QueryOptions)
+ } else if request.Link == "" {
+ data, err = store.ByID(request, request.Schema, request.ID)
+ } else {
+ return request.Schema.LinkHandler(request)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ request.WriteResponse(http.StatusOK, data)
+ return nil
+}
diff --git a/api/handlers/update.go b/api/handlers/update.go
new file mode 100644
index 00000000..94a5978e
--- /dev/null
+++ b/api/handlers/update.go
@@ -0,0 +1,30 @@
+package handlers
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/types"
+)
+
+func UpdateHandler(request *types.APIContext) error {
+ var err error
+
+ validator := request.Schema.Validator
+ if validator != nil {
+ if err := validator(request, request.Body); err != nil {
+ return err
+ }
+ }
+
+ data := request.Body
+ store := request.Schema.Store
+ if store != nil {
+ data, err = store.Update(request, request.Schema, data, request.ID)
+ if err != nil {
+ return err
+ }
+ }
+
+ request.WriteResponse(http.StatusOK, data)
+ return nil
+}
diff --git a/api/headers.go b/api/headers.go
new file mode 100644
index 00000000..fa253e89
--- /dev/null
+++ b/api/headers.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+ "github.com/rancher/norman/api/builtin"
+ "github.com/rancher/norman/types"
+)
+
+func addCommonResponseHeader(apiContext *types.APIContext) error {
+ addExpires(apiContext)
+ return addSchemasHeader(apiContext)
+}
+
+func addSchemasHeader(apiContext *types.APIContext) error {
+ schema := apiContext.Schemas.Schema(&builtin.Version, "schema")
+ if schema == nil {
+ return nil
+ }
+
+ apiContext.Response.Header().Set("X-Api-Schemas", apiContext.URLBuilder.Collection(schema))
+ return nil
+}
+
+func addExpires(apiContext *types.APIContext) {
+ apiContext.Response.Header().Set("Expires", "Wed 24 Feb 1982 18:42:00 GMT")
+}
diff --git a/api/server.go b/api/server.go
new file mode 100644
index 00000000..909cfdfe
--- /dev/null
+++ b/api/server.go
@@ -0,0 +1,209 @@
+package api
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/rancher/norman/api/builtin"
+ "github.com/rancher/norman/api/handlers"
+ "github.com/rancher/norman/api/writer"
+ "github.com/rancher/norman/authorization"
+ "github.com/rancher/norman/httperror"
+ "github.com/rancher/norman/parse"
+ "github.com/rancher/norman/parse/builder"
+ "github.com/rancher/norman/types"
+)
+
+type Parser func(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error)
+
+type Server struct {
+ IgnoreBuiltin bool
+ Parser Parser
+ ResponseWriters map[string]ResponseWriter
+ schemas *types.Schemas
+ Defaults Defaults
+}
+
+type Defaults struct {
+ ActionHandler types.ActionHandler
+ ListHandler types.RequestHandler
+ LinkHandler types.RequestHandler
+ CreateHandler types.RequestHandler
+ DeleteHandler types.RequestHandler
+ UpdateHandler types.RequestHandler
+ Store types.Store
+ ErrorHandler types.ErrorHandler
+}
+
+func NewAPIServer() *Server {
+ s := &Server{
+ schemas: types.NewSchemas(),
+ ResponseWriters: map[string]ResponseWriter{
+ "json": &writer.JSONResponseWriter{},
+ "html": &writer.HTMLResponseWriter{},
+ },
+ Defaults: Defaults{
+ CreateHandler: handlers.CreateHandler,
+ DeleteHandler: handlers.DeleteHandler,
+ UpdateHandler: handlers.UpdateHandler,
+ ListHandler: handlers.ListHandler,
+ LinkHandler: func(*types.APIContext) error {
+ return httperror.NewAPIError(httperror.NOT_FOUND, "Link not found")
+ },
+ ErrorHandler: httperror.ErrorHandler,
+ },
+ }
+
+ s.Parser = func(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error) {
+ ctx, err := parse.Parse(rw, req, s.schemas)
+ ctx.ResponseWriter = s.ResponseWriters[ctx.ResponseFormat]
+ if ctx.ResponseWriter == nil {
+ ctx.ResponseWriter = s.ResponseWriters["json"]
+ }
+
+ ctx.AccessControl = &authorization.AllAccess{}
+
+ return ctx, err
+ }
+
+ return s
+}
+
+func (s *Server) Start(ctx context.Context) error {
+ return s.addBuiltins(ctx)
+}
+
+func (s *Server) AddSchemas(schemas *types.Schemas) error {
+ if schemas.Err() != nil {
+ return schemas.Err()
+ }
+
+ for _, schema := range schemas.Schemas() {
+ s.setupDefaults(schema)
+ s.schemas.AddSchema(schema)
+ }
+
+ return s.schemas.Err()
+}
+
+func (s *Server) setupDefaults(schema *types.Schema) {
+ if schema.ActionHandler == nil {
+ schema.ActionHandler = s.Defaults.ActionHandler
+ }
+
+ if schema.Store == nil {
+ schema.Store = s.Defaults.Store
+ }
+
+ if schema.ListHandler == nil {
+ schema.ListHandler = s.Defaults.ListHandler
+ }
+
+ if schema.LinkHandler == nil {
+ schema.LinkHandler = s.Defaults.LinkHandler
+ }
+
+ if schema.CreateHandler == nil {
+ schema.CreateHandler = s.Defaults.CreateHandler
+ }
+
+ if schema.UpdateHandler == nil {
+ schema.UpdateHandler = s.Defaults.UpdateHandler
+ }
+
+ if schema.DeleteHandler == nil {
+ schema.DeleteHandler = s.Defaults.DeleteHandler
+ }
+
+ if schema.ErrorHandler == nil {
+ schema.ErrorHandler = s.Defaults.ErrorHandler
+ }
+}
+
+func (s *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
+ if apiResponse, err := s.handle(rw, req); err != nil {
+ s.handleError(apiResponse, err)
+ }
+}
+
+func (s *Server) handle(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error) {
+ apiRequest, err := s.Parser(rw, req)
+ if err != nil {
+ return apiRequest, err
+ }
+
+ if err := CheckCSRF(rw, req); err != nil {
+ return apiRequest, err
+ }
+
+ if err := addCommonResponseHeader(apiRequest); err != nil {
+ return apiRequest, err
+ }
+
+ action, err := ValidateAction(apiRequest)
+ if err != nil {
+ return apiRequest, err
+ }
+
+ if apiRequest.Schema == nil {
+ return apiRequest, nil
+ }
+
+ b := builder.NewBuilder(apiRequest)
+
+ if action == nil && apiRequest.Type != "" {
+ var handler types.RequestHandler
+ switch apiRequest.Method {
+ case http.MethodGet:
+ handler = apiRequest.Schema.ListHandler
+ apiRequest.Body = nil
+ case http.MethodPost:
+ handler = apiRequest.Schema.CreateHandler
+ apiRequest.Body, err = b.Construct(apiRequest.Schema, apiRequest.Body, builder.Create)
+ case http.MethodPut:
+ handler = apiRequest.Schema.UpdateHandler
+ apiRequest.Body, err = b.Construct(apiRequest.Schema, apiRequest.Body, builder.Update)
+ case http.MethodDelete:
+ handler = apiRequest.Schema.DeleteHandler
+ apiRequest.Body = nil
+ }
+
+ if err != nil {
+ return apiRequest, err
+ }
+
+ if handler == nil {
+ return apiRequest, httperror.NewAPIError(httperror.NOT_FOUND, "")
+ }
+
+ return apiRequest, handler(apiRequest)
+ } else if action != nil {
+ return apiRequest, handleAction(action, apiRequest)
+ }
+
+ return apiRequest, nil
+}
+
+func handleAction(action *types.Action, request *types.APIContext) error {
+ return request.Schema.ActionHandler(request.Action, action, request)
+}
+
+func (s *Server) handleError(apiRequest *types.APIContext, err error) {
+ if apiRequest.Schema == nil {
+ s.Defaults.ErrorHandler(apiRequest, err)
+ } else if apiRequest.Schema.ErrorHandler != nil {
+ apiRequest.Schema.ErrorHandler(apiRequest, err)
+ }
+}
+
+func (s *Server) addBuiltins(ctx context.Context) error {
+ if s.IgnoreBuiltin {
+ return nil
+ }
+
+ if err := s.AddSchemas(builtin.Schemas); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/api/types.go b/api/types.go
new file mode 100644
index 00000000..032f6dc4
--- /dev/null
+++ b/api/types.go
@@ -0,0 +1,7 @@
+package api
+
+import "github.com/rancher/norman/types"
+
+type ResponseWriter interface {
+ Write(apiContext *types.APIContext, code int, obj interface{})
+}
diff --git a/handler/csrf.go b/api/validate.go
similarity index 50%
rename from handler/csrf.go
rename to api/validate.go
index f2b9d1be..a2c1b6f7 100644
--- a/handler/csrf.go
+++ b/api/validate.go
@@ -1,11 +1,14 @@
-package handler
+package api
import (
"crypto/rand"
"encoding/hex"
+ "fmt"
"net/http"
"github.com/rancher/norman/httperror"
+ "github.com/rancher/norman/parse"
+ "github.com/rancher/norman/types"
)
const (
@@ -13,17 +16,42 @@ const (
csrfHeader = "X-API-CSRF"
)
+func ValidateAction(request *types.APIContext) (*types.Action, error) {
+ if request.Action == "" || request.Method != http.MethodPost {
+ return nil, nil
+ }
+
+ actions := request.Schema.CollectionActions
+ if request.ID != "" {
+ actions = request.Schema.ResourceActions
+ }
+
+ action, ok := actions[request.Action]
+ if !ok {
+ return nil, httperror.NewAPIError(httperror.INVALID_ACTION, fmt.Sprintf("Invalid action: %s", request.Action))
+ }
+
+ if request.ID != "" {
+ resource := request.ReferenceValidator.Lookup(request.Type, request.ID)
+ if resource == nil {
+ return nil, httperror.NewAPIError(httperror.NOT_FOUND, fmt.Sprintf("Failed to find type: %s id: %s", request.Type, request.ID))
+ }
+
+ if _, ok := resource.Actions[request.Action]; !ok {
+ return nil, httperror.NewAPIError(httperror.INVALID_ACTION, fmt.Sprintf("Invalid action: %s", request.Action))
+ }
+ }
+
+ return &action, nil
+}
+
func CheckCSRF(rw http.ResponseWriter, req *http.Request) error {
- if !IsBrowser(req, false) {
+ if !parse.IsBrowser(req, false) {
return nil
}
cookie, err := req.Cookie(csrfCookie)
- if err != nil {
- return httperror.NewAPIError(httperror.INVALID_CSRF_TOKEN, "Failed to parse cookies")
- }
-
- if cookie == nil {
+ if err == http.ErrNoCookie {
bytes := make([]byte, 5)
_, err := rand.Read(bytes)
if err != nil {
@@ -34,6 +62,8 @@ func CheckCSRF(rw http.ResponseWriter, req *http.Request) error {
Name: csrfCookie,
Value: hex.EncodeToString(bytes),
}
+ } else if err != nil {
+ return httperror.NewAPIError(httperror.INVALID_CSRF_TOKEN, "Failed to parse cookies")
} else if req.Method != http.MethodGet {
/*
* Very important to use request.getMethod() and not httpRequest.getMethod(). The client can override the HTTP method with _method
diff --git a/api/writer/html.go b/api/writer/html.go
new file mode 100644
index 00000000..59b33635
--- /dev/null
+++ b/api/writer/html.go
@@ -0,0 +1,46 @@
+package writer
+
+import (
+ "strings"
+
+ "github.com/rancher/norman/types"
+)
+
+var (
+ start = `
+
+
+
+
+
+`)
+)
+
+type HTMLResponseWriter struct {
+ JSONResponseWriter
+}
+
+func (h *HTMLResponseWriter) start(apiContext *types.APIContext, code int, obj interface{}) {
+ apiContext.Response.Header().Set("content-type", "text/html")
+ apiContext.Response.WriteHeader(code)
+}
+
+func (h *HTMLResponseWriter) Write(apiContext *types.APIContext, code int, obj interface{}) {
+ h.start(apiContext, code, obj)
+ schemaSchema := apiContext.Schemas.Schema(nil, "/v3/schema")
+ if schemaSchema != nil {
+ headerString := strings.Replace(start, "%SCHEMAS%", apiContext.URLBuilder.Collection(schemaSchema), 1)
+ apiContext.Response.Write([]byte(headerString))
+ }
+ h.Body(apiContext, code, obj)
+ if schemaSchema != nil {
+ apiContext.Response.Write(end)
+ }
+}
diff --git a/api/writer/json.go b/api/writer/json.go
new file mode 100644
index 00000000..7bd44f0f
--- /dev/null
+++ b/api/writer/json.go
@@ -0,0 +1,150 @@
+package writer
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ "github.com/rancher/norman/parse/builder"
+ "github.com/rancher/norman/types"
+ "github.com/sirupsen/logrus"
+)
+
+type JSONResponseWriter struct {
+}
+
+func (j *JSONResponseWriter) start(apiContext *types.APIContext, code int, obj interface{}) {
+ apiContext.Response.Header().Set("content-type", "application/json")
+ apiContext.Response.WriteHeader(code)
+}
+
+func (j *JSONResponseWriter) Write(apiContext *types.APIContext, code int, obj interface{}) {
+ j.start(apiContext, code, obj)
+ j.Body(apiContext, code, obj)
+}
+
+func (j *JSONResponseWriter) Body(apiContext *types.APIContext, code int, obj interface{}) {
+ var output interface{}
+
+ builder := builder.NewBuilder(apiContext)
+
+ switch v := obj.(type) {
+ case []interface{}:
+ output = j.writeInterfaceSlice(builder, apiContext, v)
+ case []map[string]interface{}:
+ output = j.writeMapSlice(builder, apiContext, v)
+ case map[string]interface{}:
+ output = j.convert(builder, apiContext, v)
+ case types.RawResource:
+ output = v
+ }
+
+ if output != nil {
+ json.NewEncoder(apiContext.Response).Encode(output)
+ }
+}
+func (j *JSONResponseWriter) writeMapSlice(builder *builder.Builder, apiContext *types.APIContext, input []map[string]interface{}) *types.GenericCollection {
+ collection := newCollection(apiContext)
+ for _, value := range input {
+ converted := j.convert(builder, apiContext, value)
+ if converted != nil {
+ collection.Data = append(collection.Data, converted)
+ }
+ }
+
+ return collection
+}
+
+func (j *JSONResponseWriter) writeInterfaceSlice(builder *builder.Builder, apiContext *types.APIContext, input []interface{}) *types.GenericCollection {
+ collection := newCollection(apiContext)
+ for _, value := range input {
+ switch v := value.(type) {
+ case map[string]interface{}:
+ converted := j.convert(builder, apiContext, v)
+ if converted != nil {
+ collection.Data = append(collection.Data, converted)
+ }
+ default:
+ collection.Data = append(collection.Data, v)
+ }
+ }
+ return collection
+}
+
+func toString(val interface{}) string {
+ if val == nil {
+ return ""
+ }
+ return fmt.Sprint(val)
+}
+
+func (j *JSONResponseWriter) convert(b *builder.Builder, context *types.APIContext, input map[string]interface{}) *types.RawResource {
+ schema := context.Schemas.Schema(context.Version, fmt.Sprint(input["type"]))
+ if schema == nil {
+ return nil
+ }
+ data, err := b.Construct(schema, input, builder.List)
+ if err != nil {
+ logrus.Errorf("Failed to construct object on output: %v", err)
+ return nil
+ }
+
+ rawResource := &types.RawResource{
+ ID: toString(input["id"]),
+ Type: schema.ID,
+ Schema: schema,
+ Links: map[string]string{},
+ Actions: map[string]string{},
+ Values: data,
+ ActionLinks: context.Request.Header.Get("X-API-Action-Links") != "",
+ }
+
+ j.addLinks(b, schema, context, input, rawResource)
+
+ if schema.Formatter != nil {
+ schema.Formatter(context, rawResource)
+ }
+
+ return rawResource
+}
+
+func (j *JSONResponseWriter) addLinks(b *builder.Builder, schema *types.Schema, context *types.APIContext, input map[string]interface{}, rawResource *types.RawResource) {
+ if rawResource.ID != "" {
+ rawResource.Links["self"] = context.URLBuilder.ResourceLink(rawResource)
+ }
+}
+
+func newCollection(apiContext *types.APIContext) *types.GenericCollection {
+ result := &types.GenericCollection{
+ Collection: types.Collection{
+ Type: "collection",
+ ResourceType: apiContext.Type,
+ CreateTypes: map[string]string{},
+ Links: map[string]string{
+ "self": apiContext.URLBuilder.Current(),
+ },
+ Actions: map[string]string{},
+ },
+ Data: []interface{}{},
+ }
+
+ if apiContext.Method == http.MethodGet {
+ if apiContext.AccessControl.CanCreate(apiContext.Schema) {
+ result.CreateTypes[apiContext.Schema.ID] = apiContext.URLBuilder.Collection(apiContext.Schema)
+ }
+ }
+
+ if apiContext.QueryOptions != nil {
+ result.Sort = &apiContext.QueryOptions.Sort
+ result.Sort.Reverse = apiContext.URLBuilder.ReverseSort(result.Sort.Order)
+ result.Pagination = apiContext.QueryOptions.Pagination
+ result.Filters = map[string][]types.Condition{}
+
+ for _, cond := range apiContext.QueryOptions.Conditions {
+ filters := result.Filters[cond.Field]
+ result.Filters[cond.Field] = append(filters, cond.ToCondition())
+ }
+ }
+
+ return result
+}
diff --git a/authorization/all.go b/authorization/all.go
new file mode 100644
index 00000000..db736f80
--- /dev/null
+++ b/authorization/all.go
@@ -0,0 +1,28 @@
+package authorization
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/types"
+)
+
+type AllAccess struct {
+}
+
+func (*AllAccess) CanCreate(schema *types.Schema) bool {
+ for _, method := range schema.CollectionMethods {
+ if method == http.MethodPost {
+ return true
+ }
+ }
+ return false
+}
+
+func (*AllAccess) CanList(schema *types.Schema) bool {
+ for _, method := range schema.CollectionMethods {
+ if method == http.MethodGet {
+ return true
+ }
+ }
+ return false
+}
diff --git a/clientbase/client.go b/clientbase/client.go
new file mode 100644
index 00000000..69805cab
--- /dev/null
+++ b/clientbase/client.go
@@ -0,0 +1,19 @@
+package clientbase
+
+import (
+ "net/http"
+
+ "github.com/rancher/norman/types"
+)
+
+type APIBaseClient struct {
+ Ops *APIOperations
+ Opts *ClientOpts
+ Types map[string]types.Schema
+}
+
+type APIOperations struct {
+ Opts *ClientOpts
+ Types map[string]types.Schema
+ Client *http.Client
+}
diff --git a/clientbase/common.go b/clientbase/common.go
new file mode 100644
index 00000000..a663b3cc
--- /dev/null
+++ b/clientbase/common.go
@@ -0,0 +1,291 @@
+package clientbase
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "time"
+
+ "github.com/gorilla/websocket"
+ "github.com/pkg/errors"
+ "github.com/rancher/norman/types"
+)
+
+const (
+ SELF = "self"
+ COLLECTION = "collection"
+)
+
+var (
+ debug = false
+ dialer = &websocket.Dialer{}
+)
+
+type ClientOpts struct {
+ URL string
+ AccessKey string
+ SecretKey string
+ Timeout time.Duration
+ HTTPClient *http.Client
+}
+
+type APIError struct {
+ StatusCode int
+ URL string
+ Msg string
+ Status string
+ Body string
+}
+
+func (e *APIError) Error() string {
+ return e.Msg
+}
+
+func IsNotFound(err error) bool {
+ apiError, ok := err.(*APIError)
+ if !ok {
+ return false
+ }
+
+ return apiError.StatusCode == http.StatusNotFound
+}
+
+func newApiError(resp *http.Response, url string) *APIError {
+ contents, err := ioutil.ReadAll(resp.Body)
+ var body string
+ if err != nil {
+ body = "Unreadable body."
+ } else {
+ body = string(contents)
+ }
+
+ data := map[string]interface{}{}
+ if json.Unmarshal(contents, &data) == nil {
+ delete(data, "id")
+ delete(data, "links")
+ delete(data, "actions")
+ delete(data, "type")
+ delete(data, "status")
+ buf := &bytes.Buffer{}
+ for k, v := range data {
+ if v == nil {
+ continue
+ }
+ if buf.Len() > 0 {
+ buf.WriteString(", ")
+ }
+ fmt.Fprintf(buf, "%s=%v", k, v)
+ }
+ body = buf.String()
+ }
+ formattedMsg := fmt.Sprintf("Bad response statusCode [%d]. Status [%s]. Body: [%s] from [%s]",
+ resp.StatusCode, resp.Status, body, url)
+ return &APIError{
+ URL: url,
+ Msg: formattedMsg,
+ StatusCode: resp.StatusCode,
+ Status: resp.Status,
+ Body: body,
+ }
+}
+
+func contains(array []string, item string) bool {
+ for _, check := range array {
+ if check == item {
+ return true
+ }
+ }
+
+ return false
+}
+
+func appendFilters(urlString string, filters map[string]interface{}) (string, error) {
+ if len(filters) == 0 {
+ return urlString, nil
+ }
+
+ u, err := url.Parse(urlString)
+ if err != nil {
+ return "", err
+ }
+
+ q := u.Query()
+ for k, v := range filters {
+ if l, ok := v.([]string); ok {
+ for _, v := range l {
+ q.Add(k, v)
+ }
+ } else {
+ q.Add(k, fmt.Sprintf("%v", v))
+ }
+ }
+
+ u.RawQuery = q.Encode()
+ return u.String(), nil
+}
+
+func NewAPIClient(opts *ClientOpts) (APIBaseClient, error) {
+ var err error
+
+ result := APIBaseClient{
+ Types: map[string]types.Schema{},
+ }
+
+ client := opts.HTTPClient
+ if client == nil {
+ client = &http.Client{}
+ }
+
+ if opts.Timeout == 0 {
+ opts.Timeout = time.Second * 10
+ }
+
+ client.Timeout = opts.Timeout
+
+ req, err := http.NewRequest("GET", opts.URL, nil)
+ if err != nil {
+ return result, err
+ }
+
+ req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return result, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return result, newApiError(resp, opts.URL)
+ }
+
+ schemasURLs := resp.Header.Get("X-API-Schemas")
+ if len(schemasURLs) == 0 {
+ return result, errors.New("Failed to find schema at [" + opts.URL + "]")
+ }
+
+ if schemasURLs != opts.URL {
+ req, err = http.NewRequest("GET", schemasURLs, nil)
+ req.SetBasicAuth(opts.AccessKey, opts.SecretKey)
+ if err != nil {
+ return result, err
+ }
+
+ resp, err = client.Do(req)
+ if err != nil {
+ return result, err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return result, newApiError(resp, opts.URL)
+ }
+ }
+
+ var schemas types.SchemaCollection
+ bytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return result, err
+ }
+
+ err = json.Unmarshal(bytes, &schemas)
+ if err != nil {
+ return result, err
+ }
+
+ for _, schema := range schemas.Data {
+ result.Types[schema.ID] = schema
+ }
+
+ result.Opts = opts
+ result.Ops = &APIOperations{
+ Opts: opts,
+ Client: client,
+ Types: result.Types,
+ }
+
+ return result, nil
+}
+
+func NewListOpts() *types.ListOpts {
+ return &types.ListOpts{
+ Filters: map[string]interface{}{},
+ }
+}
+
+func (a *APIBaseClient) Websocket(url string, headers map[string][]string) (*websocket.Conn, *http.Response, error) {
+ httpHeaders := http.Header{}
+ for k, v := range httpHeaders {
+ httpHeaders[k] = v
+ }
+
+ if a.Opts != nil {
+ s := a.Opts.AccessKey + ":" + a.Opts.SecretKey
+ httpHeaders.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(s)))
+ }
+
+ return dialer.Dial(url, http.Header(httpHeaders))
+}
+
+func (a *APIBaseClient) List(schemaType string, opts *types.ListOpts, respObject interface{}) error {
+ return a.Ops.DoList(schemaType, opts, respObject)
+}
+
+func (a *APIBaseClient) Post(url string, createObj interface{}, respObject interface{}) error {
+ return a.Ops.DoModify("POST", url, createObj, respObject)
+}
+
+func (a *APIBaseClient) GetLink(resource types.Resource, link string, respObject interface{}) error {
+ url := resource.Links[link]
+ if url == "" {
+ return fmt.Errorf("Failed to find link: %s", link)
+ }
+
+ return a.Ops.DoGet(url, &types.ListOpts{}, respObject)
+}
+
+func (a *APIBaseClient) Create(schemaType string, createObj interface{}, respObject interface{}) error {
+ return a.Ops.DoCreate(schemaType, createObj, respObject)
+}
+
+func (a *APIBaseClient) Update(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error {
+ return a.Ops.DoUpdate(schemaType, existing, updates, respObject)
+}
+
+func (a *APIBaseClient) ById(schemaType string, id string, respObject interface{}) error {
+ return a.Ops.DoById(schemaType, id, respObject)
+}
+
+func (a *APIBaseClient) Delete(existing *types.Resource) error {
+ if existing == nil {
+ return nil
+ }
+ return a.Ops.DoResourceDelete(existing.Type, existing)
+}
+
+func (a *APIBaseClient) Reload(existing *types.Resource, output interface{}) error {
+ selfUrl, ok := existing.Links[SELF]
+ if !ok {
+ return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
+ }
+
+ return a.Ops.DoGet(selfUrl, NewListOpts(), output)
+}
+
+func (a *APIBaseClient) Action(schemaType string, action string,
+ existing *types.Resource, inputObject, respObject interface{}) error {
+ return a.Ops.DoAction(schemaType, action, existing, inputObject, respObject)
+}
+
+func init() {
+ debug = os.Getenv("RANCHER_CLIENT_DEBUG") == "true"
+ if debug {
+ fmt.Println("Rancher client debug on")
+ }
+}
diff --git a/clientbase/object_client.go b/clientbase/object_client.go
new file mode 100644
index 00000000..32e7558b
--- /dev/null
+++ b/clientbase/object_client.go
@@ -0,0 +1,184 @@
+package clientbase
+
+import (
+ "encoding/json"
+
+ "github.com/pkg/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/rest"
+)
+
+type ObjectFactory interface {
+ Object() runtime.Object
+ List() runtime.Object
+}
+
+type ObjectClient struct {
+ restClient rest.Interface
+ resource *metav1.APIResource
+ gvk schema.GroupVersionKind
+ ns string
+ Factory ObjectFactory
+}
+
+func NewObjectClient(namespace string, config rest.Config, apiResource *metav1.APIResource, gvk schema.GroupVersionKind, factory ObjectFactory) (*ObjectClient, error) {
+ if config.NegotiatedSerializer == nil {
+ configConfig := dynamic.ContentConfig()
+ config.NegotiatedSerializer = configConfig.NegotiatedSerializer
+ }
+
+ restClient, err := rest.UnversionedRESTClientFor(&config)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ObjectClient{
+ restClient: restClient,
+ resource: apiResource,
+ gvk: gvk,
+ ns: namespace,
+ Factory: factory,
+ }, nil
+}
+
+func (p *ObjectClient) Create(o runtime.Object) (runtime.Object, error) {
+ ns := p.ns
+ if obj, ok := o.(metav1.Object); ok && obj.GetNamespace() != "" {
+ ns = obj.GetNamespace()
+ }
+ result := p.Factory.Object()
+ err := p.restClient.Post().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ Body(o).
+ Do().
+ Into(result)
+ return result, err
+}
+
+func (p *ObjectClient) Get(name string, opts metav1.GetOptions) (runtime.Object, error) {
+ result := p.Factory.Object()
+ err := p.restClient.Get().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(p.ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
+ Name(name).
+ Do().
+ Into(result)
+ return result, err
+}
+
+func (p *ObjectClient) Update(name string, o runtime.Object) (runtime.Object, error) {
+ ns := p.ns
+ if obj, ok := o.(metav1.Object); ok && obj.GetNamespace() != "" {
+ ns = obj.GetNamespace()
+ }
+ result := p.Factory.Object()
+ if len(name) == 0 {
+ return result, errors.New("object missing name")
+ }
+ err := p.restClient.Put().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ Name(name).
+ Body(o).
+ Do().
+ Into(result)
+ return result, err
+}
+
+func (p *ObjectClient) Delete(name string, opts *metav1.DeleteOptions) error {
+ return p.restClient.Delete().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(p.ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ Name(name).
+ Body(opts).
+ Do().
+ Error()
+}
+
+func (p *ObjectClient) List(opts metav1.ListOptions) (runtime.Object, error) {
+ result := p.Factory.List()
+ return result, p.restClient.Get().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(p.ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
+ Do().
+ Into(result)
+}
+
+func (p *ObjectClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
+ r, err := p.restClient.Get().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ Prefix("watch").
+ Namespace(p.ns).
+ NamespaceIfScoped(p.ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ VersionedParams(&opts, dynamic.VersionedParameterEncoderWithV1Fallback).
+ Stream()
+ if err != nil {
+ return nil, err
+ }
+ return watch.NewStreamWatcher(&dynamicDecoder{
+ factory: p.Factory,
+ dec: json.NewDecoder(r),
+ close: r.Close,
+ }), nil
+}
+
+func (p *ObjectClient) DeleteCollection(deleteOptions *metav1.DeleteOptions, listOptions metav1.ListOptions) error {
+ return p.restClient.Delete().
+ Prefix("apis", p.gvk.Group, p.gvk.Version).
+ NamespaceIfScoped(p.ns, p.resource.Namespaced).
+ Resource(p.resource.Name).
+ VersionedParams(&listOptions, dynamic.VersionedParameterEncoderWithV1Fallback).
+ Body(deleteOptions).
+ Do().
+ Error()
+}
+
+type dynamicDecoder struct {
+ factory ObjectFactory
+ dec *json.Decoder
+ close func() error
+}
+
+func (d *dynamicDecoder) Close() {
+ d.close()
+}
+
+func (d *dynamicDecoder) Decode() (action watch.EventType, object runtime.Object, err error) {
+ e := dynamicEvent{
+ Object: holder{
+ factory: d.factory,
+ },
+ }
+ if err := d.dec.Decode(&e); err != nil {
+ return watch.Error, nil, err
+ }
+ return e.Type, e.Object.obj, nil
+}
+
+type dynamicEvent struct {
+ Type watch.EventType
+ Object holder
+}
+
+type holder struct {
+ factory ObjectFactory
+ obj runtime.Object
+}
+
+func (h *holder) UnmarshalJSON(b []byte) error {
+ h.obj = h.factory.Object()
+ return json.Unmarshal(b, h.obj)
+}
diff --git a/clientbase/ops.go b/clientbase/ops.go
new file mode 100644
index 00000000..7ceca7f1
--- /dev/null
+++ b/clientbase/ops.go
@@ -0,0 +1,313 @@
+package clientbase
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+
+ "github.com/pkg/errors"
+ "github.com/rancher/norman/types"
+)
+
+func (a *APIOperations) setupRequest(req *http.Request) {
+ req.SetBasicAuth(a.Opts.AccessKey, a.Opts.SecretKey)
+}
+
+func (a *APIOperations) DoDelete(url string) error {
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+
+ a.setupRequest(req)
+
+ resp, err := a.Client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ io.Copy(ioutil.Discard, resp.Body)
+
+ if resp.StatusCode >= 300 {
+ return newApiError(resp, url)
+ }
+
+ return nil
+}
+
+func (a *APIOperations) DoGet(url string, opts *types.ListOpts, respObject interface{}) error {
+ if opts == nil {
+ opts = NewListOpts()
+ }
+ url, err := appendFilters(url, opts.Filters)
+ if err != nil {
+ return err
+ }
+
+ if debug {
+ fmt.Println("GET " + url)
+ }
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return err
+ }
+
+ a.setupRequest(req)
+
+ resp, err := a.Client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 200 {
+ return newApiError(resp, url)
+ }
+
+ byteContent, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ if debug {
+ fmt.Println("Response <= " + string(byteContent))
+ }
+
+ if err := json.Unmarshal(byteContent, respObject); err != nil {
+ return errors.Wrap(err, fmt.Sprintf("Failed to parse: %s", byteContent))
+ }
+
+ return nil
+}
+
+func (a *APIOperations) DoList(schemaType string, opts *types.ListOpts, respObject interface{}) error {
+ schema, ok := a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ if !contains(schema.CollectionMethods, "GET") {
+ return errors.New("Resource type [" + schemaType + "] is not listable")
+ }
+
+ collectionUrl, ok := schema.Links[COLLECTION]
+ if !ok {
+ return errors.New("Failed to find collection URL for [" + schemaType + "]")
+ }
+
+ return a.DoGet(collectionUrl, opts, respObject)
+}
+
+func (a *APIOperations) DoNext(nextUrl string, respObject interface{}) error {
+ return a.DoGet(nextUrl, nil, respObject)
+}
+
+func (a *APIOperations) DoModify(method string, url string, createObj interface{}, respObject interface{}) error {
+ bodyContent, err := json.Marshal(createObj)
+ if err != nil {
+ return err
+ }
+
+ if debug {
+ fmt.Println(method + " " + url)
+ fmt.Println("Request => " + string(bodyContent))
+ }
+
+ req, err := http.NewRequest(method, url, bytes.NewBuffer(bodyContent))
+ if err != nil {
+ return err
+ }
+
+ a.setupRequest(req)
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := a.Client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 300 {
+ return newApiError(resp, url)
+ }
+
+ byteContent, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ if len(byteContent) > 0 {
+ if debug {
+ fmt.Println("Response <= " + string(byteContent))
+ }
+ return json.Unmarshal(byteContent, respObject)
+ }
+
+ return nil
+}
+
+func (a *APIOperations) DoCreate(schemaType string, createObj interface{}, respObject interface{}) error {
+ if createObj == nil {
+ createObj = map[string]string{}
+ }
+ if respObject == nil {
+ respObject = &map[string]interface{}{}
+ }
+ schema, ok := a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ if !contains(schema.CollectionMethods, "POST") {
+ return errors.New("Resource type [" + schemaType + "] is not creatable")
+ }
+
+ var collectionUrl string
+ collectionUrl, ok = schema.Links[COLLECTION]
+ if !ok {
+ // return errors.New("Failed to find collection URL for [" + schemaType + "]")
+ // This is a hack to address https://github.com/rancher/cattle/issues/254
+ re := regexp.MustCompile("schemas.*")
+ collectionUrl = re.ReplaceAllString(schema.Links[SELF], schema.PluralName)
+ }
+
+ return a.DoModify("POST", collectionUrl, createObj, respObject)
+}
+
+func (a *APIOperations) DoUpdate(schemaType string, existing *types.Resource, updates interface{}, respObject interface{}) error {
+ if existing == nil {
+ return errors.New("Existing object is nil")
+ }
+
+ selfUrl, ok := existing.Links[SELF]
+ if !ok {
+ return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
+ }
+
+ if updates == nil {
+ updates = map[string]string{}
+ }
+
+ if respObject == nil {
+ respObject = &map[string]interface{}{}
+ }
+
+ schema, ok := a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ if !contains(schema.ResourceMethods, "PUT") {
+ return errors.New("Resource type [" + schemaType + "] is not updatable")
+ }
+
+ return a.DoModify("PUT", selfUrl, updates, respObject)
+}
+
+func (a *APIOperations) DoById(schemaType string, id string, respObject interface{}) error {
+ schema, ok := a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ if !contains(schema.ResourceMethods, "GET") {
+ return errors.New("Resource type [" + schemaType + "] can not be looked up by ID")
+ }
+
+ collectionUrl, ok := schema.Links[COLLECTION]
+ if !ok {
+ return errors.New("Failed to find collection URL for [" + schemaType + "]")
+ }
+
+ err := a.DoGet(collectionUrl+"/"+id, nil, respObject)
+ //TODO check for 404 and return nil, nil
+ return err
+}
+
+func (a *APIOperations) DoResourceDelete(schemaType string, existing *types.Resource) error {
+ schema, ok := a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ if !contains(schema.ResourceMethods, "DELETE") {
+ return errors.New("Resource type [" + schemaType + "] can not be deleted")
+ }
+
+ selfUrl, ok := existing.Links[SELF]
+ if !ok {
+ return errors.New(fmt.Sprintf("Failed to find self URL of [%v]", existing))
+ }
+
+ return a.DoDelete(selfUrl)
+}
+
+func (a *APIOperations) DoAction(schemaType string, action string,
+ existing *types.Resource, inputObject, respObject interface{}) error {
+
+ if existing == nil {
+ return errors.New("Existing object is nil")
+ }
+
+ actionUrl, ok := existing.Actions[action]
+ if !ok {
+ return errors.New(fmt.Sprintf("Action [%v] not available on [%v]", action, existing))
+ }
+
+ _, ok = a.Types[schemaType]
+ if !ok {
+ return errors.New("Unknown schema type [" + schemaType + "]")
+ }
+
+ var input io.Reader
+
+ if inputObject != nil {
+ bodyContent, err := json.Marshal(inputObject)
+ if err != nil {
+ return err
+ }
+ if debug {
+ fmt.Println("Request => " + string(bodyContent))
+ }
+ input = bytes.NewBuffer(bodyContent)
+ }
+
+ req, err := http.NewRequest("POST", actionUrl, input)
+ if err != nil {
+ return err
+ }
+
+ a.setupRequest(req)
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("Content-Length", "0")
+
+ resp, err := a.Client.Do(req)
+ if err != nil {
+ return err
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 300 {
+ return newApiError(resp, actionUrl)
+ }
+
+ byteContent, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+
+ if debug {
+ fmt.Println("Response <= " + string(byteContent))
+ }
+
+ return json.Unmarshal(byteContent, respObject)
+}
diff --git a/controller/generic_controller.go b/controller/generic_controller.go
new file mode 100644
index 00000000..4ebe911d
--- /dev/null
+++ b/controller/generic_controller.go
@@ -0,0 +1,152 @@
+package controller
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/rancher/norman/clientbase"
+ "github.com/rancher/norman/types"
+ "github.com/sirupsen/logrus"
+ utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+ "k8s.io/apimachinery/pkg/util/wait"
+ "k8s.io/client-go/tools/cache"
+ "k8s.io/client-go/util/workqueue"
+)
+
+var (
+ resyncPeriod = 5 * time.Minute
+)
+
+type HandlerFunc func(key string) error
+
+type GenericController interface {
+ Informer() cache.SharedIndexInformer
+ AddHandler(handler HandlerFunc)
+ Enqueue(namespace, name string)
+ Start(threadiness int, ctx context.Context) error
+}
+
+type genericController struct {
+ sync.Mutex
+ informer cache.SharedIndexInformer
+ handlers []HandlerFunc
+ queue workqueue.RateLimitingInterface
+ name string
+ running bool
+}
+
+func NewGenericController(name string, objectClient *clientbase.ObjectClient) (GenericController, error) {
+ informer := cache.NewSharedIndexInformer(
+ &cache.ListWatch{
+ ListFunc: objectClient.List,
+ WatchFunc: objectClient.Watch,
+ },
+ objectClient.Factory.Object(), resyncPeriod, cache.Indexers{})
+
+ return &genericController{
+ informer: informer,
+ queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(),
+ name),
+ name: name,
+ }, nil
+}
+
+func (g *genericController) Informer() cache.SharedIndexInformer {
+ return g.informer
+}
+
+func (g *genericController) Enqueue(namespace, name string) {
+ if namespace == "" {
+ g.queue.Add(name)
+ } else {
+ g.queue.Add(namespace + "/" + name)
+ }
+}
+
+func (g *genericController) AddHandler(handler HandlerFunc) {
+ g.handlers = append(g.handlers, handler)
+}
+
+func (g *genericController) Start(threadiness int, ctx context.Context) error {
+ g.Lock()
+ defer g.Unlock()
+
+ if !g.running {
+ go g.run(threadiness, ctx)
+ }
+
+ g.running = true
+ return nil
+}
+
+func (g *genericController) queueObject(obj interface{}) {
+ key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
+ if err == nil {
+ g.queue.Add(key)
+ }
+}
+
+func (g *genericController) run(threadiness int, ctx context.Context) {
+ defer utilruntime.HandleCrash()
+ defer g.queue.ShutDown()
+
+ g.informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
+ AddFunc: g.queueObject,
+ UpdateFunc: func(_, obj interface{}) {
+ g.queueObject(obj)
+ },
+ DeleteFunc: g.queueObject,
+ })
+
+ logrus.Infof("Starting %s Controller", g.name)
+
+ go g.informer.Run(ctx.Done())
+
+ if !cache.WaitForCacheSync(ctx.Done(), g.informer.HasSynced) {
+ return
+ }
+
+ for i := 0; i < threadiness; i++ {
+ go wait.Until(g.runWorker, time.Second, ctx.Done())
+ }
+
+ <-ctx.Done()
+ logrus.Infof("Shutting down %s controller", g.name)
+}
+
+func (g *genericController) runWorker() {
+ for g.processNextWorkItem() {
+ }
+}
+
+func (g *genericController) processNextWorkItem() bool {
+ key, quit := g.queue.Get()
+ if quit {
+ return false
+ }
+ defer g.queue.Done(key)
+
+ // do your work on the key. This method will contains your "do stuff" logic
+ err := g.syncHandler(key.(string))
+ if err == nil {
+ g.queue.Forget(key)
+ return true
+ }
+
+ utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err))
+ g.queue.AddRateLimited(key)
+
+ return true
+}
+
+func (g *genericController) syncHandler(s string) error {
+ var errs []error
+ for _, handler := range g.handlers {
+ if err := handler(s); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ return types.NewErrors(errs)
+}
diff --git a/example/main.go b/example/main.go
new file mode 100644
index 00000000..82622efb
--- /dev/null
+++ b/example/main.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/rancher/norman/generator"
+ "github.com/rancher/norman/server"
+ "github.com/rancher/norman/types"
+)
+
+type Foo struct {
+ types.Resource
+ Name string `json:"name"`
+ Foo string `json:"foo"`
+ SubThing Baz `json:"subThing"`
+}
+
+type Baz struct {
+ Name string `json:"name"`
+}
+
+var (
+ version = types.APIVersion{
+ Version: "v1",
+ Group: "io.cattle.core.example",
+ Path: "/example/v1",
+ }
+
+ Schemas = types.NewSchemas()
+)
+
+func main() {
+ if _, err := Schemas.Import(&version, Foo{}); err != nil {
+ panic(err)
+ }
+
+ if err := generator.Generate("example_gen", Schemas); err != nil {
+ panic(err)
+ }
+
+ server, err := server.NewAPIServer(context.Background(), os.Getenv("KUBECONFIG"), Schemas)
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println("Listening on 0.0.0.0:1234")
+ http.ListenAndServe("0.0.0.0:1234", server)
+}
diff --git a/generator/client_template.go b/generator/client_template.go
new file mode 100644
index 00000000..89d95526
--- /dev/null
+++ b/generator/client_template.go
@@ -0,0 +1,31 @@
+package generator
+
+var clientTemplate = `package client
+
+import (
+ "github.com/rancher/norman/clientbase"
+)
+
+type Client struct {
+ clientbase.APIBaseClient
+
+ {{range .schemas}}
+ {{- if . | hasGet }}{{.ID | capitalize}} {{.ID | capitalize}}Operations
+{{end}}{{end}}}
+
+func NewClient(opts *clientbase.ClientOpts) (*Client, error) {
+ baseClient, err := clientbase.NewAPIClient(opts)
+ if err != nil {
+ return nil, err
+ }
+
+ client := &Client{
+ APIBaseClient: baseClient,
+ }
+
+ {{range .schemas}}
+ {{- if . | hasGet }}client.{{.ID | capitalize}} = new{{.ID | capitalize}}Client(client)
+{{end}}{{end}}
+ return client, nil
+}
+`
diff --git a/generator/controller_template.go b/generator/controller_template.go
new file mode 100644
index 00000000..a1eb7a23
--- /dev/null
+++ b/generator/controller_template.go
@@ -0,0 +1,146 @@
+package generator
+
+var controllerTemplate = `package {{.schema.Version.Version}}
+
+import (
+ "sync"
+
+ "context"
+
+ "github.com/rancher/norman/clientbase"
+ "github.com/rancher/norman/controller"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/cache"
+)
+
+var (
+ {{.schema.CodeName}}GroupVersionKind = schema.GroupVersionKind{
+ Version: "{{.schema.Version.Version}}",
+ Group: "{{.schema.Version.Group}}",
+ Kind: "{{.schema.CodeName}}",
+ }
+ {{.schema.CodeName}}Resource = metav1.APIResource{
+ Name: "{{.schema.PluralName | toLower}}",
+ SingularName: "{{.schema.ID | toLower}}",
+ Namespaced: false,
+ Kind: {{.schema.CodeName}}GroupVersionKind.Kind,
+ }
+)
+
+type {{.schema.CodeName}}HandlerFunc func(key string, obj *{{.schema.CodeName}}) error
+
+type {{.schema.CodeName}}Controller interface {
+ Informer() cache.SharedIndexInformer
+ AddHandler(handler {{.schema.CodeName}}HandlerFunc)
+ Enqueue(namespace, name string)
+ Start(threadiness int, ctx context.Context) error
+}
+
+type {{.schema.CodeName}}Interface interface {
+ Create(*{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
+ Get(name string, opts metav1.GetOptions) (*{{.schema.CodeName}}, error)
+ Update(*{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
+ Delete(name string, options *metav1.DeleteOptions) error
+ List(opts metav1.ListOptions) (*{{.schema.CodeName}}List, error)
+ Watch(opts metav1.ListOptions) (watch.Interface, error)
+ DeleteCollection(deleteOpts *metav1.DeleteOptions, listOpts metav1.ListOptions) error
+ Controller() ({{.schema.CodeName}}Controller, error)
+}
+
+type {{.schema.ID}}Controller struct {
+ controller.GenericController
+}
+
+func (c *{{.schema.ID}}Controller) AddHandler(handler {{.schema.CodeName}}HandlerFunc) {
+ c.GenericController.AddHandler(func(key string) error {
+ obj, exists, err := c.Informer().GetStore().GetByKey(key)
+ if err != nil {
+ return err
+ }
+ if !exists {
+ return handler(key, nil)
+ }
+ return handler(key, obj.(*{{.schema.CodeName}}))
+ })
+}
+
+type {{.schema.ID}}Factory struct {
+}
+
+func (c {{.schema.ID}}Factory) Object() runtime.Object {
+ return &{{.schema.CodeName}}{}
+}
+
+func (c {{.schema.ID}}Factory) List() runtime.Object {
+ return &{{.schema.CodeName}}List{}
+}
+
+func New{{.schema.CodeName}}Client(namespace string, config rest.Config) ({{.schema.CodeName}}Interface, error) {
+ objectClient, err := clientbase.NewObjectClient(namespace, config, &{{.schema.CodeName}}Resource, {{.schema.CodeName}}GroupVersionKind, {{.schema.ID}}Factory{})
+ return &{{.schema.ID}}Client{
+ objectClient: objectClient,
+ }, err
+}
+
+func (s *{{.schema.ID}}Client) Controller() ({{.schema.CodeName}}Controller, error) {
+ s.Lock()
+ defer s.Unlock()
+
+ if s.controller != nil {
+ return s.controller, nil
+ }
+
+ controller, err := controller.NewGenericController({{.schema.CodeName}}GroupVersionKind.Kind+"Controller",
+ s.objectClient)
+ if err != nil {
+ return nil, err
+ }
+
+ s.controller = &{{.schema.ID}}Controller{
+ GenericController: controller,
+ }
+ return s.controller, nil
+}
+
+type {{.schema.ID}}Client struct {
+ sync.Mutex
+ objectClient *clientbase.ObjectClient
+ controller {{.schema.CodeName}}Controller
+}
+
+func (s *{{.schema.ID}}Client) Create(o *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
+ obj, err := s.objectClient.Create(o)
+ return obj.(*{{.schema.CodeName}}), err
+}
+
+func (s *{{.schema.ID}}Client) Get(name string, opts metav1.GetOptions) (*{{.schema.CodeName}}, error) {
+ obj, err := s.objectClient.Get(name, opts)
+ return obj.(*{{.schema.CodeName}}), err
+}
+
+func (s *{{.schema.ID}}Client) Update(o *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
+ obj, err := s.objectClient.Update(o.Name, o)
+ return obj.(*{{.schema.CodeName}}), err
+}
+
+func (s *{{.schema.ID}}Client) Delete(name string, options *metav1.DeleteOptions) error {
+ return s.objectClient.Delete(name, options)
+}
+
+func (s *{{.schema.ID}}Client) List(opts metav1.ListOptions) (*{{.schema.CodeName}}List, error) {
+ obj, err := s.objectClient.List(opts)
+ return obj.(*{{.schema.CodeName}}List), err
+}
+
+func (s *{{.schema.ID}}Client) Watch(opts metav1.ListOptions) (watch.Interface, error) {
+ return s.objectClient.Watch(opts)
+}
+
+func (s *{{.schema.ID}}Client) DeleteCollection(deleteOpts *metav1.DeleteOptions, listOpts metav1.ListOptions) error {
+ return s.objectClient.DeleteCollection(deleteOpts, listOpts)
+}
+`
diff --git a/generator/funcs.go b/generator/funcs.go
new file mode 100644
index 00000000..c744b373
--- /dev/null
+++ b/generator/funcs.go
@@ -0,0 +1,37 @@
+package generator
+
+import (
+ "net/http"
+ "strings"
+ "text/template"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+)
+
+func funcs() template.FuncMap {
+ return template.FuncMap{
+ "toLowerCamelCase": convert.LowerTitle,
+ "capitalize": convert.Capitalize,
+ "upper": strings.ToUpper,
+ "toLower": strings.ToLower,
+ "hasGet": hasGet,
+ }
+}
+
+func addUnderscore(input string) string {
+ return strings.ToLower(underscoreRegexp.ReplaceAllString(input, `${1}_${2}`))
+}
+
+func hasGet(schema *types.Schema) bool {
+ return contains(schema.CollectionMethods, http.MethodGet)
+}
+
+func contains(list []string, needle string) bool {
+ for _, i := range list {
+ if i == needle {
+ return true
+ }
+ }
+ return false
+}
diff --git a/generator/generator.go b/generator/generator.go
new file mode 100644
index 00000000..c51fc093
--- /dev/null
+++ b/generator/generator.go
@@ -0,0 +1,261 @@
+package generator
+
+import (
+ "k8s.io/gengo/args"
+ "k8s.io/gengo/examples/deepcopy-gen/generators"
+
+ "os"
+ "path"
+ "regexp"
+ "strings"
+ "text/template"
+
+ "net/http"
+
+ "os/exec"
+
+ "fmt"
+ "io/ioutil"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+)
+
+var (
+ blackListTypes = map[string]bool{
+ "schema": true,
+ "resource": true,
+ "collection": true,
+ }
+ underscoreRegexp = regexp.MustCompile(`([a-z])([A-Z])`)
+)
+
+func getGoType(field types.Field, schema *types.Schema, schemas *types.Schemas) string {
+ return getTypeString(field.Nullable, field.Type, schema, schemas)
+}
+
+func getTypeString(nullable bool, typeName string, schema *types.Schema, schemas *types.Schemas) string {
+ switch {
+ case strings.HasPrefix(typeName, "reference["):
+ return "string"
+ case strings.HasPrefix(typeName, "map["):
+ return "map[string]" + getTypeString(false, typeName[len("map["):len(typeName)-1], schema, schemas)
+ case strings.HasPrefix(typeName, "array["):
+ return "[]" + getTypeString(false, typeName[len("array["):len(typeName)-1], schema, schemas)
+ }
+
+ name := ""
+
+ switch typeName {
+ case "json":
+ return "interface{}"
+ case "boolean":
+ name = "bool"
+ case "float":
+ name = "float64"
+ case "int":
+ name = "int64"
+ case "password":
+ return "string"
+ case "date":
+ return "string"
+ case "string":
+ return "string"
+ case "enum":
+ return "string"
+ default:
+ if schema != nil && schemas != nil {
+ otherSchema := schemas.Schema(&schema.Version, typeName)
+ if otherSchema != nil {
+ name = otherSchema.CodeName
+ }
+ }
+
+ if name == "" {
+ name = convert.Capitalize(typeName)
+ }
+ }
+
+ if nullable {
+ return "*" + name
+ }
+
+ return name
+}
+
+func getTypeMap(schema *types.Schema, schemas *types.Schemas) map[string]string {
+ result := map[string]string{}
+ for _, field := range schema.ResourceFields {
+ result[field.CodeName] = getGoType(field, schema, schemas)
+ }
+ return result
+}
+
+func getResourceActions(schema *types.Schema, schemas *types.Schemas) map[string]types.Action {
+ result := map[string]types.Action{}
+ for name, action := range schema.ResourceActions {
+ if schemas.Schema(&schema.Version, action.Output) != nil {
+ result[name] = action
+ }
+ }
+ return result
+}
+
+func generateType(outputDir string, schema *types.Schema, schemas *types.Schemas) error {
+ filePath := strings.ToLower("zz_generated_" + addUnderscore(schema.ID) + ".go")
+ output, err := os.Create(path.Join(outputDir, filePath))
+ if err != nil {
+ return err
+ }
+ defer output.Close()
+
+ typeTemplate, err := template.New("type.template").
+ Funcs(funcs()).
+ Parse(strings.Replace(typeTemplate, "%BACK%", "`", -1))
+ if err != nil {
+ return err
+ }
+
+ return typeTemplate.Execute(output, map[string]interface{}{
+ "schema": schema,
+ "structFields": getTypeMap(schema, schemas),
+ "resourceActions": getResourceActions(schema, schemas),
+ })
+}
+
+func generateController(outputDir string, schema *types.Schema, schemas *types.Schemas) error {
+ filePath := strings.ToLower("zz_generated_" + addUnderscore(schema.ID) + "_controller.go")
+ output, err := os.Create(path.Join(outputDir, filePath))
+ if err != nil {
+ return err
+ }
+ defer output.Close()
+
+ typeTemplate, err := template.New("controller.template").
+ Funcs(funcs()).
+ Parse(strings.Replace(controllerTemplate, "%BACK%", "`", -1))
+ if err != nil {
+ return err
+ }
+
+ if schema.InternalSchema != nil {
+ schema = schema.InternalSchema
+ }
+
+ return typeTemplate.Execute(output, map[string]interface{}{
+ "schema": schema,
+ "structFields": getTypeMap(schema, schemas),
+ "resourceActions": getResourceActions(schema, schemas),
+ })
+}
+
+func generateClient(outputDir string, schemas []*types.Schema) error {
+ template, err := template.New("client.template").
+ Funcs(funcs()).
+ Parse(clientTemplate)
+ if err != nil {
+ return err
+ }
+
+ output, err := os.Create(path.Join(outputDir, "zz_generated_client.go"))
+ if err != nil {
+ return err
+ }
+ defer output.Close()
+
+ return template.Execute(output, map[string]interface{}{
+ "schemas": schemas,
+ })
+}
+
+func Generate(schemas *types.Schemas, cattleOutputPackage, k8sOutputPackage string) error {
+ baseDir := args.DefaultSourceTree()
+ cattleDir := path.Join(baseDir, cattleOutputPackage)
+ k8sDir := path.Join(baseDir, k8sOutputPackage)
+
+ if err := prepareDirs(cattleDir, k8sDir); err != nil {
+ return err
+ }
+
+ generated := []*types.Schema{}
+ for _, schema := range schemas.Schemas() {
+ if blackListTypes[schema.ID] {
+ continue
+ }
+
+ if err := generateType(cattleDir, schema, schemas); err != nil {
+ return err
+ }
+
+ if contains(schema.CollectionMethods, http.MethodGet) {
+ if err := generateController(k8sDir, schema, schemas); err != nil {
+ return err
+ }
+ }
+
+ generated = append(generated, schema)
+ }
+
+ if err := generateClient(cattleDir, generated); err != nil {
+ return err
+ }
+
+ if err := deepCopyGen(baseDir, k8sOutputPackage); err != nil {
+ return err
+ }
+
+ if err := gofmt(baseDir, k8sOutputPackage); err != nil {
+ return err
+ }
+
+ return gofmt(baseDir, cattleOutputPackage)
+}
+
+func prepareDirs(dirs ...string) error {
+ for _, dir := range dirs {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+
+ files, err := ioutil.ReadDir(dir)
+ if err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ if strings.HasPrefix(file.Name(), "zz_generated") {
+ fmt.Println("DELETING", path.Join(dir, file.Name()))
+ //if err != os.Remove(path.Join(dir, file.Name()); err != nil {
+ // return errors.Wrapf(err, "failed to delete %s", path.Join(dir, file.Name()))
+ //}
+ }
+ }
+ }
+
+ return nil
+}
+
+func gofmt(workDir, pkg string) error {
+ cmd := exec.Command("go", "fmt", "./"+pkg+"/...")
+ cmd.Dir = workDir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ return cmd.Run()
+}
+
+func deepCopyGen(workDir, pkg string) error {
+ arguments := &args.GeneratorArgs{
+ InputDirs: []string{pkg},
+ OutputBase: workDir,
+ OutputPackagePath: pkg,
+ OutputFileBaseName: "zz_generated_deepcopy",
+ GoHeaderFilePath: "/dev/null",
+ GeneratedBuildTag: "ignore_autogenerated",
+ }
+
+ return arguments.Execute(
+ generators.NameSystems(),
+ generators.DefaultNameSystem(),
+ generators.Packages)
+}
diff --git a/generator/type_template.go b/generator/type_template.go
new file mode 100644
index 00000000..dc89b802
--- /dev/null
+++ b/generator/type_template.go
@@ -0,0 +1,111 @@
+package generator
+
+var typeTemplate = `package client
+
+{{- if .schema | hasGet }}
+import (
+ "github.com/rancher/norman/types"
+)
+{{- end}}
+
+const (
+ {{.schema.CodeName}}Type = "{{.schema.ID}}"
+{{- range $key, $value := .structFields}}
+ {{$.schema.CodeName}}Field{{$key}} = "{{$key | toLowerCamelCase }}"
+{{- end}}
+)
+
+type {{.schema.CodeName}} struct {
+{{- if .schema | hasGet }}
+ types.Resource
+{{- end}}
+ {{- range $key, $value := .structFields}}
+ {{$key}} {{$value}} %BACK%json:"{{$key | toLowerCamelCase }},omitempty"%BACK%
+ {{- end}}
+}
+
+{{- if .schema | hasGet }}
+type {{.schema.CodeName}}Collection struct {
+ types.Collection
+ Data []{{.schema.CodeName}} %BACK%json:"data,omitempty"%BACK%
+ client *{{.schema.CodeName}}Client
+}
+
+type {{.schema.CodeName}}Client struct {
+ apiClient *Client
+}
+
+type {{.schema.CodeName}}Operations interface {
+ List(opts *types.ListOpts) (*{{.schema.CodeName}}Collection, error)
+ Create(opts *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error)
+ Update(existing *{{.schema.CodeName}}, updates interface{}) (*{{.schema.CodeName}}, error)
+ ById(id string) (*{{.schema.CodeName}}, error)
+ Delete(container *{{.schema.CodeName}}) error{{range $key, $value := .resourceActions}}
+ {{if eq $value.Input "" }}
+ Action{{$key | capitalize}} (*{{$.schema.CodeName}}) (*{{.Output | capitalize}}, error)
+ {{else}}
+ Action{{$key | capitalize}} (*{{$.schema.CodeName}}, *{{$value.Input | capitalize}}) (*{{.Output | capitalize}}, error)
+ {{end}}{{end}}
+}
+
+func new{{.schema.CodeName}}Client(apiClient *Client) *{{.schema.CodeName}}Client {
+ return &{{.schema.CodeName}}Client{
+ apiClient: apiClient,
+ }
+}
+
+func (c *{{.schema.CodeName}}Client) Create(container *{{.schema.CodeName}}) (*{{.schema.CodeName}}, error) {
+ resp := &{{.schema.CodeName}}{}
+ err := c.apiClient.Ops.DoCreate({{.schema.CodeName}}Type, container, resp)
+ return resp, err
+}
+
+func (c *{{.schema.CodeName}}Client) Update(existing *{{.schema.CodeName}}, updates interface{}) (*{{.schema.CodeName}}, error) {
+ resp := &{{.schema.CodeName}}{}
+ err := c.apiClient.Ops.DoUpdate({{.schema.CodeName}}Type, &existing.Resource, updates, resp)
+ return resp, err
+}
+
+func (c *{{.schema.CodeName}}Client) List(opts *types.ListOpts) (*{{.schema.CodeName}}Collection, error) {
+ resp := &{{.schema.CodeName}}Collection{}
+ err := c.apiClient.Ops.DoList({{.schema.CodeName}}Type, opts, resp)
+ resp.client = c
+ return resp, err
+}
+
+func (cc *{{.schema.CodeName}}Collection) Next() (*{{.schema.CodeName}}Collection, error) {
+ if cc != nil && cc.Pagination != nil && cc.Pagination.Next != "" {
+ resp := &{{.schema.CodeName}}Collection{}
+ err := cc.client.apiClient.Ops.DoNext(cc.Pagination.Next, resp)
+ resp.client = cc.client
+ return resp, err
+ }
+ return nil, nil
+}
+
+func (c *{{.schema.CodeName}}Client) ById(id string) (*{{.schema.CodeName}}, error) {
+ resp := &{{.schema.CodeName}}{}
+ err := c.apiClient.Ops.DoById({{.schema.CodeName}}Type, id, resp)
+ return resp, err
+}
+
+func (c *{{.schema.CodeName}}Client) Delete(container *{{.schema.CodeName}}) error {
+ return c.apiClient.Ops.DoResourceDelete({{.schema.CodeName}}Type, &container.Resource)
+}
+
+{{range $key, $value := .resourceActions}}
+ {{if eq $value.Input "" }}
+ func (c *{{$.schema.CodeName}}Client) Action{{$key | capitalize}} (resource *{{$.schema.CodeName}}) (*{{.Output | capitalize}}, error) {
+ {{else}}
+ func (c *{{$.schema.CodeName}}Client) Action{{$key | capitalize}} (resource *{{$.schema.CodeName}}, input *{{$value.Input | capitalize}}) (*{{.Output | capitalize}}, error) {
+ {{end}}
+ resp := &{{.Output | capitalize}}{}
+ {{if eq $value.Input "" }}
+ err := c.apiClient.Ops.DoAction({{$.schema.CodeName}}Type, "{{$key}}", &resource.Resource, nil, resp)
+ {{else}}
+ err := c.apiClient.Ops.DoAction({{$.schema.CodeName}}Type, "{{$key}}", &resource.Resource, input, resp)
+ {{end}}
+ return resp, err
+ }
+{{end}}
+{{end}}`
diff --git a/handler/parse_collection.go b/handler/parse_collection.go
deleted file mode 100644
index 7dbe69eb..00000000
--- a/handler/parse_collection.go
+++ /dev/null
@@ -1,130 +0,0 @@
-package handler
-
-import (
- "net/http"
-
- "strconv"
-
- "strings"
-
- "github.com/rancher/go-rancher/v3"
- "github.com/rancher/norman/query"
-)
-
-var (
- ASC = SortOrder("asc")
- DESC = SortOrder("desc")
- defaultLimit = 100
- maxLimit = 3000
-)
-
-type SortOrder string
-
-type Pagination struct {
- Limit int
- Marker string
-}
-
-type CollectionAttributes struct {
- Sort string
- Order SortOrder
- Pagination *Pagination
- Conditions []*query.Condition
-}
-
-func ParseCollectionAttributes(req *http.Request, schema client.Schema) *CollectionAttributes {
- if req.Method != http.MethodGet {
- return nil
- }
-
- result := &CollectionAttributes{}
-
- result.Order = parseOrder(req)
- result.Sort = parseSort(schema, req)
- result.Pagination = parsePagination(req)
- result.Conditions = parseFilters(schema, req)
-
- return result
-}
-
-func parseOrder(req *http.Request) SortOrder {
- order := req.URL.Query().Get("order")
- if SortOrder(order) == DESC {
- return DESC
- }
- return ASC
-}
-
-func parseSort(schema client.Schema, req *http.Request) string {
- sort := req.URL.Query().Get("sort")
- if _, ok := schema.CollectionFilters[sort]; ok {
- return sort
- }
- return ""
-}
-
-func parsePagination(req *http.Request) *Pagination {
- q := req.URL.Query()
- limit := q.Get("limit")
- marker := q.Get("marker")
-
- result := &Pagination{
- Limit: defaultLimit,
- Marker: marker,
- }
-
- if limit != "" {
- limitInt, err := strconv.Atoi(limit)
- if err != nil {
- return result
- }
-
- if limitInt > maxLimit {
- result.Limit = maxLimit
- } else if limitInt > 0 {
- result.Limit = limitInt
- }
- }
-
- return result
-}
-
-func parseNameAndOp(value string) (string, string) {
- name := value
- op := "eq"
-
- idx := strings.LastIndex(value, "_")
- if idx > 0 {
- op = value[idx+1:]
- name = value[0:idx]
- }
-
- return name, op
-}
-
-func parseFilters(schema client.Schema, req *http.Request) []*query.Condition {
- conditions := []*query.Condition{}
- q := req.URL.Query()
- for key, values := range req.URL.Query() {
- name, op := parseNameAndOp(key)
- filter, ok := schema.CollectionFilters[name]
- if !ok {
- continue
- }
-
- for _, mod := range filter.Modifiers {
- if op != mod || !query.ValidMod(op) {
- continue
- }
-
- genericValues := []interface{}{}
- for _, value := range values {
- genericValues = append(genericValues, value)
- }
-
- conditions = append(conditions, query.NewCondition(query.ConditionType(mod), genericValues))
- }
- }
-
- return conditions
-}
diff --git a/httperror/error.go b/httperror/error.go
index fc260180..51f1aeb9 100644
--- a/httperror/error.go
+++ b/httperror/error.go
@@ -5,28 +5,38 @@ import (
)
var (
- INVALID_DATE_FORMAT = ErrorCode("InvalidDateFormat")
- INVALID_FORMAT = ErrorCode("InvalidFormat")
- INVALID_REFERENCE = ErrorCode("InvalidReference")
- NOT_NULLABLE = ErrorCode("NotNullable")
- NOT_UNIQUE = ErrorCode("NotUnique")
- MIN_LIMIT_EXCEEDED = ErrorCode("MinLimitExceeded")
- MAX_LIMIT_EXCEEDED = ErrorCode("MaxLimitExceeded")
- MIN_LENGTH_EXCEEDED = ErrorCode("MinLengthExceeded")
- MAX_LENGTH_EXCEEDED = ErrorCode("MaxLengthExceeded")
- INVALID_OPTION = ErrorCode("InvalidOption")
- INVALID_CHARACTERS = ErrorCode("InvalidCharacters")
- MISSING_REQUIRED = ErrorCode("MissingRequired")
- INVALID_CSRF_TOKEN = ErrorCode("InvalidCSRFToken")
- INVALID_ACTION = ErrorCode("InvalidAction")
- INVALID_BODY_CONTENT = ErrorCode("InvalidBodyContent")
- INVALID_TYPE = ErrorCode("InvalidType")
- ACTION_NOT_AVAILABLE = ErrorCode("ActionNotAvailable")
- INVALID_STATE = ErrorCode("InvalidState")
- SERVER_ERROR = ErrorCode("ServerError")
+ INVALID_DATE_FORMAT = ErrorCode{"InvalidDateFormat", 422}
+ INVALID_FORMAT = ErrorCode{"InvalidFormat", 422}
+ INVALID_REFERENCE = ErrorCode{"InvalidReference", 422}
+ NOT_NULLABLE = ErrorCode{"NotNullable", 422}
+ NOT_UNIQUE = ErrorCode{"NotUnique", 422}
+ MIN_LIMIT_EXCEEDED = ErrorCode{"MinLimitExceeded", 422}
+ MAX_LIMIT_EXCEEDED = ErrorCode{"MaxLimitExceeded", 422}
+ MIN_LENGTH_EXCEEDED = ErrorCode{"MinLengthExceeded", 422}
+ MAX_LENGTH_EXCEEDED = ErrorCode{"MaxLengthExceeded", 422}
+ INVALID_OPTION = ErrorCode{"InvalidOption", 422}
+ INVALID_CHARACTERS = ErrorCode{"InvalidCharacters", 422}
+ MISSING_REQUIRED = ErrorCode{"MissingRequired", 422}
+ INVALID_CSRF_TOKEN = ErrorCode{"InvalidCSRFToken", 422}
+ INVALID_ACTION = ErrorCode{"InvalidAction", 422}
+ INVALID_BODY_CONTENT = ErrorCode{"InvalidBodyContent", 422}
+ INVALID_TYPE = ErrorCode{"InvalidType", 422}
+ ACTION_NOT_AVAILABLE = ErrorCode{"ActionNotAvailable", 404}
+ INVALID_STATE = ErrorCode{"InvalidState", 422}
+ SERVER_ERROR = ErrorCode{"ServerError", 500}
+
+ METHOD_NOT_ALLOWED = ErrorCode{"MethodNotAllow", 405}
+ NOT_FOUND = ErrorCode{"NotFound", 404}
)
-type ErrorCode string
+type ErrorCode struct {
+ code string
+ status int
+}
+
+func (e ErrorCode) String() string {
+ return fmt.Sprintf("%s %d", e.code, e.status)
+}
type APIError struct {
code ErrorCode
@@ -52,7 +62,7 @@ func NewFieldAPIError(code ErrorCode, fieldName, message string) error {
func WrapFieldAPIError(err error, code ErrorCode, fieldName, message string) error {
return &APIError{
- Cause: err,
+ Cause: err,
code: code,
message: message,
fieldName: fieldName,
@@ -68,5 +78,8 @@ func WrapAPIError(err error, code ErrorCode, message string) error {
}
func (a *APIError) Error() string {
+ if a.fieldName != "" {
+ return fmt.Sprintf("%s=%s: %s", a.fieldName, a.code, a.message)
+ }
return fmt.Sprintf("%s: %s", a.code, a.message)
}
diff --git a/httperror/handler.go b/httperror/handler.go
new file mode 100644
index 00000000..5f8fc334
--- /dev/null
+++ b/httperror/handler.go
@@ -0,0 +1,35 @@
+package httperror
+
+import (
+ "github.com/rancher/norman/types"
+ "github.com/sirupsen/logrus"
+)
+
+func ErrorHandler(request *types.APIContext, err error) {
+ var error *APIError
+ if apiError, ok := err.(*APIError); ok {
+ error = apiError
+ } else {
+ logrus.Errorf("Unknown error: %v", err)
+ error = &APIError{
+ code: SERVER_ERROR,
+ message: err.Error(),
+ }
+ }
+
+ data := toError(error)
+ request.WriteResponse(error.code.status, data)
+}
+
+func toError(apiError *APIError) map[string]interface{} {
+ e := map[string]interface{}{
+ "type": "/v3/schema",
+ "code": apiError.code.code,
+ "message": apiError.message,
+ }
+ if apiError.fieldName != "" {
+ e["fieldName"] = apiError.fieldName
+ }
+
+ return e
+}
diff --git a/main.go b/main.go
deleted file mode 100644
index adab4a91..00000000
--- a/main.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package main
-
-import (
- "os"
-
- "github.com/Sirupsen/logrus"
- "github.com/urfave/cli"
-)
-
-var VERSION = "v0.0.0-dev"
-
-func main() {
- app := cli.NewApp()
- app.Name = "norman"
- app.Version = VERSION
- app.Usage = "You need help!"
- app.Action = func(c *cli.Context) error {
- logrus.Info("I'm a turkey")
- return nil
- }
-
- app.Run(os.Args)
-}
diff --git a/name/name.go b/name/name.go
new file mode 100644
index 00000000..045d7f60
--- /dev/null
+++ b/name/name.go
@@ -0,0 +1,23 @@
+package name
+
+import "strings"
+
+func GuessPluralName(name string) string {
+ if name == "" {
+ return name
+ }
+
+ if suffix(name, "s") || suffix(name, "ch") || suffix(name, "x") {
+ return name + "es"
+ }
+
+ if suffix(name, "y") && len(name) > 2 && !strings.ContainsAny(name[len(name)-2:len(name)-1], "[aeiou]") {
+ return name[0:len(name)-1] + "ies"
+ }
+
+ return name + "s"
+}
+
+func suffix(str, end string) bool {
+ return strings.HasSuffix(str, end)
+}
diff --git a/handler/browser.go b/parse/browser.go
similarity index 91%
rename from handler/browser.go
rename to parse/browser.go
index 8e9bb2ab..6d195d0e 100644
--- a/handler/browser.go
+++ b/parse/browser.go
@@ -1,4 +1,4 @@
-package handler
+package parse
import (
"net/http"
@@ -14,5 +14,5 @@ func IsBrowser(req *http.Request, checkAccepts bool) bool {
}
// User agent has Mozilla and browser accepts */*
- return strings.Contains(userAgent, "mozilla") && strings.Contains(accepts,"*/*")
+ return strings.Contains(userAgent, "mozilla") && strings.Contains(accepts, "*/*")
}
diff --git a/handler/validation.go b/parse/builder/builder.go
similarity index 64%
rename from handler/validation.go
rename to parse/builder/builder.go
index dbd1152d..af44acd7 100644
--- a/handler/validation.go
+++ b/parse/builder/builder.go
@@ -1,34 +1,43 @@
-package handler
+package builder
import (
"fmt"
- "strconv"
"strings"
- "github.com/rancher/go-rancher/v3"
"github.com/rancher/norman/httperror"
- "github.com/rancher/norman/registry"
- "github.com/rancher/norman/store"
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+ "github.com/rancher/norman/types/definition"
)
var (
Create = Operation("create")
Update = Operation("update")
Action = Operation("action")
+ List = Operation("list")
)
type Operation string
type Builder struct {
- Registry registry.SchemaRegistry
- RefValidator store.ReferenceValidator
+ Version *types.APIVersion
+ Schemas *types.Schemas
+ RefValidator types.ReferenceValidator
}
-func (b *Builder) Construct(schema *client.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
+func NewBuilder(apiRequest *types.APIContext) *Builder {
+ return &Builder{
+ Version: apiRequest.Version,
+ Schemas: apiRequest.Schemas,
+ RefValidator: apiRequest.ReferenceValidator,
+ }
+}
+
+func (b *Builder) Construct(schema *types.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
return b.copyFields(schema, input, op)
}
-func (b *Builder) copyInputs(schema *client.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
+func (b *Builder) copyInputs(schema *types.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
for fieldName, value := range input {
field, ok := schema.ResourceFields[fieldName]
if !ok {
@@ -46,28 +55,37 @@ func (b *Builder) copyInputs(schema *client.Schema, input map[string]interface{}
}
if value != nil || wasNull {
- if slice, ok := value.([]interface{}); ok {
- for _, sliceValue := range slice {
- if sliceValue == nil {
- return httperror.NewFieldAPIError(httperror.NOT_NULLABLE, fieldName, "Individual array values can not be null")
+ if op != List {
+ if slice, ok := value.([]interface{}); ok {
+ for _, sliceValue := range slice {
+ if sliceValue == nil {
+ return httperror.NewFieldAPIError(httperror.NOT_NULLABLE, fieldName, "Individual array values can not be null")
+ }
+ if err := checkFieldCriteria(fieldName, field, sliceValue); err != nil {
+ return err
+ }
}
- if err := checkFieldCriteria(fieldName, field, sliceValue); err != nil {
+ } else {
+ if err := checkFieldCriteria(fieldName, field, value); err != nil {
return err
}
}
- } else {
- if err := checkFieldCriteria(fieldName, field, value); err != nil {
- return err
- }
}
result[fieldName] = value
+
+ if op == List && field.Type == "date" && value != "" {
+ ts, err := convert.ToTimestamp(value)
+ if err == nil {
+ result[fieldName+"TS"] = ts
+ }
+ }
}
}
return nil
}
-func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
+func (b *Builder) checkDefaultAndRequired(schema *types.Schema, input map[string]interface{}, op Operation, result map[string]interface{}) error {
for fieldName, field := range schema.ResourceFields {
_, hasKey := result[fieldName]
if op == Create && !hasKey && field.Default != nil {
@@ -80,7 +98,7 @@ func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[strin
return httperror.NewFieldAPIError(httperror.MISSING_REQUIRED, fieldName, "")
}
- if isArrayType(field.Type) {
+ if definition.IsArrayType(field.Type) {
slice, err := b.convertArray(fieldName, result[fieldName], op)
if err != nil {
return err
@@ -95,7 +113,7 @@ func (b *Builder) checkDefaultAndRequired(schema *client.Schema, input map[strin
return nil
}
-func (b *Builder) copyFields(schema *client.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
+func (b *Builder) copyFields(schema *types.Schema, input map[string]interface{}, op Operation) (map[string]interface{}, error) {
result := map[string]interface{}{}
if err := b.copyInputs(schema, input, op, result); err != nil {
@@ -105,7 +123,7 @@ func (b *Builder) copyFields(schema *client.Schema, input map[string]interface{}
return result, b.checkDefaultAndRequired(schema, input, op, result)
}
-func checkFieldCriteria(fieldName string, field client.Field, value interface{}) error {
+func checkFieldCriteria(fieldName string, field types.Field, value interface{}) error {
numVal, isNum := value.(int64)
strVal := ""
hasStrVal := false
@@ -133,10 +151,10 @@ func checkFieldCriteria(fieldName string, field client.Field, value interface{})
}
if hasStrVal {
- if field.MinLength != nil && len(strVal) < *field.MinLength {
+ if field.MinLength != nil && int64(len(strVal)) < *field.MinLength {
return httperror.NewFieldAPIError(httperror.MIN_LENGTH_EXCEEDED, fieldName, "")
}
- if field.MaxLength != nil && len(strVal) > *field.MaxLength {
+ if field.MaxLength != nil && int64(len(strVal)) > *field.MaxLength {
return httperror.NewFieldAPIError(httperror.MAX_LENGTH_EXCEEDED, fieldName, "")
}
}
@@ -181,11 +199,11 @@ func (b *Builder) convert(fieldType string, value interface{}, op Operation) (in
}
switch {
- case isMapType(fieldType):
- return b.convertMap(fieldType, value, op), nil
- case isArrayType(fieldType):
- return b.convertArray(fieldType, value, op), nil
- case isReferenceType(fieldType):
+ case definition.IsMapType(fieldType):
+ return b.convertMap(fieldType, value, op)
+ case definition.IsArrayType(fieldType):
+ return b.convertArray(fieldType, value, op)
+ case definition.IsReferenceType(fieldType):
return b.convertReferenceType(fieldType, value)
}
@@ -193,26 +211,26 @@ func (b *Builder) convert(fieldType string, value interface{}, op Operation) (in
case "json":
return value, nil
case "date":
- return convertString(value), nil
+ return convert.ToString(value), nil
case "boolean":
- return convertBool(value), nil
+ return convert.ToBool(value), nil
case "enum":
- return convertString(value), nil
+ return convert.ToString(value), nil
case "int":
- return convertNumber(value)
+ return convert.ToNumber(value)
case "password":
- return convertString(value), nil
+ return convert.ToString(value), nil
case "string":
- return convertString(value), nil
+ return convert.ToString(value), nil
case "reference":
- return convertString(value), nil
+ return convert.ToString(value), nil
}
return b.convertType(fieldType, value, op)
}
func (b *Builder) convertType(fieldType string, value interface{}, op Operation) (interface{}, error) {
- schema := b.Registry.GetSchema(fieldType)
+ schema := b.Schemas.Schema(b.Version, fieldType)
if schema == nil {
return nil, httperror.NewAPIError(httperror.INVALID_TYPE, "Failed to find type "+fieldType)
}
@@ -225,45 +243,32 @@ func (b *Builder) convertType(fieldType string, value interface{}, op Operation)
return b.Construct(schema, mapValue, op)
}
-func convertNumber(value interface{}) (int64, error) {
- i, ok := value.(int64)
- if ok {
- return i, nil
- }
- return strconv.ParseInt(convertString(value), 10, 64)
-}
-
-func convertBool(value interface{}) bool {
- b, ok := value.(bool)
- if ok {
- return b
- }
-
- str := strings.ToLower(convertString(value))
- return str == "true" || str == "t" || str == "yes" || str == "y"
-}
-
-func convertString(value interface{}) string {
- return fmt.Sprint(value)
-}
-
func (b *Builder) convertReferenceType(fieldType string, value interface{}) (string, error) {
- subType := fieldType[len("array[") : len(fieldType)-1]
- strVal := convertString(value)
- if !b.RefValidator.Validate(subType, strVal) {
+ subType := definition.SubType(fieldType)
+ strVal := convert.ToString(value)
+ if b.RefValidator != nil && !b.RefValidator.Validate(subType, strVal) {
return "", httperror.NewAPIError(httperror.INVALID_REFERENCE, fmt.Sprintf("Not found type: %s id: %s", subType, strVal))
}
return strVal, nil
}
func (b *Builder) convertArray(fieldType string, value interface{}, op Operation) ([]interface{}, error) {
+ if strSliceValue, ok := value.([]string); ok {
+ // Form data will be []string
+ result := []interface{}{}
+ for _, value := range strSliceValue {
+ result = append(result, value)
+ }
+ return result, nil
+ }
+
sliceValue, ok := value.([]interface{})
if !ok {
return nil, nil
}
result := []interface{}{}
- subType := fieldType[len("array[") : len(fieldType)-1]
+ subType := definition.SubType(fieldType)
for _, value := range sliceValue {
val, err := b.convert(subType, value, op)
@@ -283,7 +288,7 @@ func (b *Builder) convertMap(fieldType string, value interface{}, op Operation)
}
result := map[string]interface{}{}
- subType := fieldType[len("map[") : len(fieldType)-1]
+ subType := definition.SubType(fieldType)
for key, value := range mapValue {
val, err := b.convert(subType, value, op)
@@ -296,24 +301,14 @@ func (b *Builder) convertMap(fieldType string, value interface{}, op Operation)
return result, nil
}
-func isMapType(fieldType string) bool {
- return strings.HasPrefix(fieldType, "map[") && strings.HasSuffix(fieldType, "]")
-}
-
-func isArrayType(fieldType string) bool {
- return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
-}
-
-func isReferenceType(fieldType string) bool {
- return strings.HasPrefix(fieldType, "reference[") && strings.HasSuffix(fieldType, "]")
-}
-
-func fieldMatchesOp(field client.Field, op Operation) bool {
+func fieldMatchesOp(field types.Field, op Operation) bool {
switch op {
case Create:
return field.Create
case Update:
return field.Update
+ case List:
+ return !field.WriteOnly
default:
return false
}
diff --git a/parse/parse.go b/parse/parse.go
new file mode 100644
index 00000000..f7e0d509
--- /dev/null
+++ b/parse/parse.go
@@ -0,0 +1,228 @@
+package parse
+
+import (
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/rancher/norman/api/builtin"
+ "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,
+ }
+)
+
+func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas) (*types.APIContext, error) {
+ var err error
+
+ result := &types.APIContext{
+ Request: req,
+ Response: rw,
+ }
+
+ // The response format is guarenteed to be set even in the event of an error
+ result.ResponseFormat = parseResponseFormat(req)
+ result.Version = parseVersion(schemas, req.URL.Path)
+ result.Schemas = schemas
+
+ if result.Version == nil {
+ result.Method = http.MethodGet
+ result.URLBuilder, err = urlbuilder.New(req, types.APIVersion{}, result.Schemas)
+ result.Type = "apiRoot"
+ result.Schema = &builtin.APIRoot
+ return result, nil
+ }
+
+ result.Method = parseMethod(req)
+ result.Action, result.Method = parseAction(req, result.Method)
+ result.Body, err = parseBody(req)
+ if err != nil {
+ return result, err
+ }
+
+ result.URLBuilder, err = urlbuilder.New(req, *result.Version, result.Schemas)
+ if err != nil {
+ return result, err
+ }
+
+ parsePath(result, req)
+
+ if result.Schema == nil {
+ result.Method = http.MethodGet
+ result.Type = "apiRoot"
+ result.Schema = &builtin.APIRoot
+ result.ID = result.Version.Path
+ return result, nil
+ }
+
+ if err := ValidateMethod(result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
+
+func parseSubContext(parts []string, apiRequest *types.APIContext) []string {
+ subContext := ""
+ apiRequest.SubContext = map[string]interface{}{}
+
+ for len(parts) > 3 && apiRequest.Version != nil {
+ resourceType := parts[1]
+ resourceID := parts[2]
+
+ if !apiRequest.Version.SubContexts[resourceType] {
+ break
+ }
+
+ if apiRequest.ReferenceValidator != nil && !apiRequest.ReferenceValidator.Validate(resourceType, resourceID) {
+ return parts
+ }
+
+ apiRequest.SubContext[resourceType] = resourceID
+ subContext = subContext + "/" + resourceType + "/" + resourceID
+ parts = append(parts[:1], parts[3:]...)
+ }
+
+ if subContext != "" {
+ apiRequest.URLBuilder.SetSubContext(subContext)
+ }
+
+ return parts
+}
+
+func parsePath(apiRequest *types.APIContext, request *http.Request) {
+ if apiRequest.Version == nil {
+ return
+ }
+
+ path := request.URL.Path
+ path = multiSlashRegexp.ReplaceAllString(path, "/")
+
+ versionPrefix := apiRequest.Version.Path
+ if !strings.HasPrefix(path, versionPrefix) {
+ return
+ }
+
+ parts := strings.Split(path[len(versionPrefix):], "/")
+ parts = parseSubContext(parts, apiRequest)
+
+ if len(parts) > 4 {
+ return
+ }
+
+ typeName := safeIndex(parts, 1)
+ id := safeIndex(parts, 2)
+ link := safeIndex(parts, 3)
+
+ if typeName == "" {
+ return
+ }
+
+ schema := apiRequest.Schemas.Schema(apiRequest.Version, typeName)
+ if schema == nil {
+ return
+ }
+
+ apiRequest.Schema = schema
+ apiRequest.Type = schema.ID
+
+ if id == "" {
+ return
+ }
+
+ apiRequest.ID = id
+ apiRequest.Link = link
+}
+
+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"
+ } else {
+ return "json"
+ }
+}
+
+func parseMethod(req *http.Request) string {
+ method := req.URL.Query().Get("_method")
+ if method == "" {
+ method = req.Method
+ }
+ return method
+}
+
+func parseAction(req *http.Request, method string) (string, string) {
+ if req.Method != http.MethodPost {
+ return "", method
+ }
+
+ action := req.URL.Query().Get("action")
+ if action == "remove" {
+ return "", http.MethodDelete
+ }
+
+ return action, method
+}
+
+func parseVersion(schemas *types.Schemas, path string) *types.APIVersion {
+ path = multiSlashRegexp.ReplaceAllString(path, "/")
+ for _, version := range schemas.Versions() {
+ if version.Path == "" {
+ continue
+ }
+ if strings.HasPrefix(path, version.Path) {
+ return &version
+ }
+ }
+
+ return nil
+}
+
+func parseBody(req *http.Request) (map[string]interface{}, error) {
+ req.ParseMultipartForm(maxFormSize)
+ if req.MultipartForm != nil {
+ return valuesToBody(req.MultipartForm.Value), nil
+ }
+
+ if req.Form != nil && len(req.Form) > 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
+}
diff --git a/parse/parse_collection.go b/parse/parse_collection.go
new file mode 100644
index 00000000..a47de015
--- /dev/null
+++ b/parse/parse_collection.go
@@ -0,0 +1,112 @@
+package parse
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+
+ "github.com/rancher/norman/types"
+)
+
+var (
+ defaultLimit = int64(100)
+ maxLimit = int64(3000)
+)
+
+func QueryOptions(req *http.Request, schema *types.Schema) *types.QueryOptions {
+ if req.Method != http.MethodGet {
+ return nil
+ }
+
+ result := &types.QueryOptions{}
+
+ result.Sort = parseSort(schema, req)
+ result.Pagination = parsePagination(req)
+ result.Conditions = parseFilters(schema, req)
+
+ return result
+}
+
+func parseOrder(req *http.Request) types.SortOrder {
+ order := req.URL.Query().Get("order")
+ if types.SortOrder(order) == types.DESC {
+ return types.DESC
+ }
+ return types.ASC
+}
+
+func parseSort(schema *types.Schema, req *http.Request) types.Sort {
+ sortField := req.URL.Query().Get("sort")
+ if _, ok := schema.CollectionFilters[sortField]; !ok {
+ sortField = ""
+ }
+ return types.Sort{
+ Order: parseOrder(req),
+ Name: sortField,
+ }
+}
+
+func parsePagination(req *http.Request) *types.Pagination {
+ q := req.URL.Query()
+ limit := q.Get("limit")
+ marker := q.Get("marker")
+
+ result := &types.Pagination{
+ Limit: &defaultLimit,
+ Marker: marker,
+ }
+
+ if limit != "" {
+ limitInt, err := strconv.ParseInt(limit, 10, 64)
+ if err != nil {
+ return result
+ }
+
+ if limitInt > maxLimit {
+ result.Limit = &maxLimit
+ } else if limitInt > 0 {
+ result.Limit = &limitInt
+ }
+ }
+
+ return result
+}
+
+func parseNameAndOp(value string) (string, string) {
+ name := value
+ op := "eq"
+
+ idx := strings.LastIndex(value, "_")
+ if idx > 0 {
+ op = value[idx+1:]
+ name = value[0:idx]
+ }
+
+ return name, op
+}
+
+func parseFilters(schema *types.Schema, req *http.Request) []*types.QueryCondition {
+ conditions := []*types.QueryCondition{}
+ for key, values := range req.URL.Query() {
+ name, op := parseNameAndOp(key)
+ filter, ok := schema.CollectionFilters[name]
+ if !ok {
+ continue
+ }
+
+ for _, mod := range filter.Modifiers {
+ if op != mod || !types.ValidMod(op) {
+ continue
+ }
+
+ genericValues := []interface{}{}
+ for _, value := range values {
+ genericValues = append(genericValues, value)
+ }
+
+ conditions = append(conditions, types.NewConditionFromString(name, mod, genericValues))
+ }
+ }
+
+ return conditions
+}
diff --git a/handler/read_input.go b/parse/read_input.go
similarity index 73%
rename from handler/read_input.go
rename to parse/read_input.go
index 379abe3c..d5d555e4 100644
--- a/handler/read_input.go
+++ b/parse/read_input.go
@@ -1,22 +1,23 @@
-package handler
+package parse
import (
- "net/http"
- "io/ioutil"
- "io"
- "github.com/rancher/norman/httperror"
- "fmt"
"encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+
+ "github.com/rancher/norman/httperror"
)
-const reqMaxSize = (2 * 1<<20) + 1
+const reqMaxSize = (2 * 1 << 20) + 1
var bodyMethods = map[string]bool{
- http.MethodPut: true,
+ http.MethodPut: true,
http.MethodPost: true,
}
-func ReadBody(rw http.ResponseWriter, req *http.Request) (map[string]interface{}, error) {
+func ReadBody(req *http.Request) (map[string]interface{}, error) {
if !bodyMethods[req.Method] {
return nil, nil
}
@@ -28,7 +29,7 @@ func ReadBody(rw http.ResponseWriter, req *http.Request) (map[string]interface{}
}
data := map[string]interface{}{}
- if err := json.Unmarshal(content, data); err != nil {
+ if err := json.Unmarshal(content, &data); err != nil {
return nil, httperror.NewAPIError(httperror.INVALID_BODY_CONTENT,
fmt.Sprintf("Failed to parse body: %v", err))
}
diff --git a/parse/validate.go b/parse/validate.go
new file mode 100644
index 00000000..ff782e58
--- /dev/null
+++ b/parse/validate.go
@@ -0,0 +1,45 @@
+package parse
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/rancher/norman/httperror"
+ "github.com/rancher/norman/types"
+)
+
+var (
+ supportedMethods = map[string]bool{
+ http.MethodPost: true,
+ http.MethodGet: true,
+ http.MethodPut: true,
+ http.MethodDelete: true,
+ }
+)
+
+func ValidateMethod(request *types.APIContext) error {
+ if request.Action != "" && request.Method == http.MethodPost {
+ return nil
+ }
+
+ if !supportedMethods[request.Method] {
+ return httperror.NewAPIError(httperror.METHOD_NOT_ALLOWED, fmt.Sprintf("Method %s not supported", request.Method))
+ }
+
+ if request.Type == "" || request.Schema == nil {
+ return nil
+ }
+
+ allowed := request.Schema.ResourceMethods
+ if request.ID == "" {
+ allowed = request.Schema.CollectionMethods
+ }
+
+ for _, method := range allowed {
+ if method == request.Method {
+ return nil
+ }
+ }
+
+ return httperror.NewAPIError(httperror.METHOD_NOT_ALLOWED, fmt.Sprintf("Method %s not supported", request.Method))
+}
diff --git a/query/condition.go b/query/condition.go
deleted file mode 100644
index fa4ac6b0..00000000
--- a/query/condition.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package query
-
-var (
- COND_EQ = ConditionType("eq")
- COND_NE = ConditionType("ne")
- COND_NULL = ConditionType("null")
- COND_NOTNULL = ConditionType("notnull")
- COND_IN = ConditionType("in")
- COND_NOTIN = ConditionType("notin")
- COND_OR = ConditionType("or")
- COND_AND = ConditionType("AND")
-
- mods = map[ConditionType]bool{
- COND_EQ: true,
- COND_NE: true,
- COND_NULL: true,
- COND_NOTNULL: true,
- COND_IN: true,
- COND_NOTIN: true,
- COND_OR: true,
- COND_AND: true,
- }
-)
-
-type ConditionType string
-
-type Condition struct {
- values []interface{}
- conditionType ConditionType
- left, right *Condition
-}
-
-func ValidMod(mod string) bool {
- return mods[ConditionType(mod)]
-}
-
-func NewCondition(conditionType ConditionType, values ...interface{}) *Condition {
- return &Condition{
- values: values,
- conditionType: conditionType,
- }
-}
-
-func NE(value interface{}) *Condition {
- return NewCondition(COND_NE, value)
-}
-
-func EQ(value interface{}) *Condition {
- return NewCondition(COND_EQ, value)
-}
-
-func NULL(value interface{}) *Condition {
- return NewCondition(COND_NULL)
-}
-
-func NOTNULL(value interface{}) *Condition {
- return NewCondition(COND_NOTNULL)
-}
-
-func IN(values ...interface{}) *Condition {
- return NewCondition(COND_IN, values...)
-}
-
-func NOTIN(values ...interface{}) *Condition {
- return NewCondition(COND_NOTIN, values...)
-}
-
-func (c *Condition) AND(right *Condition) *Condition {
- return &Condition{
- conditionType: COND_AND,
- left: c,
- right: right,
- }
-}
-
-func (c *Condition) OR(right *Condition) *Condition {
- return &Condition{
- conditionType: COND_OR,
- left: c,
- right: right,
- }
-}
diff --git a/registry/registry.go b/registry/registry.go
deleted file mode 100644
index 1ed21fe0..00000000
--- a/registry/registry.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package registry
-
-import "github.com/rancher/go-rancher/v3"
-
-type SchemaRegistry interface {
- GetSchema(name string) *client.Schema
-}
diff --git a/server/server.go b/server/server.go
new file mode 100644
index 00000000..adec69c8
--- /dev/null
+++ b/server/server.go
@@ -0,0 +1,74 @@
+package server
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/pkg/errors"
+ "github.com/rancher/norman/api"
+ "github.com/rancher/norman/store/crd"
+ "github.com/rancher/norman/types"
+ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+ "k8s.io/client-go/dynamic"
+ "k8s.io/client-go/rest"
+ "k8s.io/client-go/tools/clientcmd"
+)
+
+func NewAPIServer(ctx context.Context, kubeConfig string, schemas *types.Schemas) (http.Handler, error) {
+ config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to build kubeConfig")
+ }
+ return NewAPIServerFromConfig(ctx, config, schemas)
+}
+
+func NewClients(kubeConfig string) (*rest.RESTClient, clientset.Interface, error) {
+ config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
+ if err != nil {
+ return nil, nil, err
+ }
+ return NewClientsFromConfig(config)
+}
+
+func NewClientsFromConfig(config *rest.Config) (*rest.RESTClient, clientset.Interface, error) {
+ dynamicConfig := *config
+ if dynamicConfig.NegotiatedSerializer == nil {
+ configConfig := dynamic.ContentConfig()
+ dynamicConfig.NegotiatedSerializer = configConfig.NegotiatedSerializer
+ }
+
+ k8sClient, err := rest.UnversionedRESTClientFor(&dynamicConfig)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ apiExtClient, err := clientset.NewForConfig(&dynamicConfig)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return k8sClient, apiExtClient, nil
+}
+
+func NewAPIServerFromConfig(ctx context.Context, config *rest.Config, schemas *types.Schemas) (http.Handler, error) {
+ k8sClient, apiExtClient, err := NewClientsFromConfig(config)
+ if err != nil {
+ return nil, err
+ }
+ return NewAPIServerFromClients(ctx, k8sClient, apiExtClient, schemas)
+}
+
+func NewAPIServerFromClients(ctx context.Context, k8sClient *rest.RESTClient, apiExtClient clientset.Interface, schemas *types.Schemas) (http.Handler, error) {
+ store := crd.NewCRDStore(apiExtClient, k8sClient)
+ if err := store.AddSchemas(ctx, schemas); err != nil {
+ return nil, err
+ }
+
+ server := api.NewAPIServer()
+ if err := server.AddSchemas(schemas); err != nil {
+ return nil, err
+ }
+
+ err := server.Start(ctx)
+ return server, err
+}
diff --git a/store/crd/crd_store.go b/store/crd/crd_store.go
new file mode 100644
index 00000000..3d4404d5
--- /dev/null
+++ b/store/crd/crd_store.go
@@ -0,0 +1,389 @@
+package crd
+
+import (
+ "context"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/rancher/norman/types"
+ "github.com/sirupsen/logrus"
+ apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
+ apiextclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/util/wait"
+ "k8s.io/client-go/rest"
+)
+
+type Store struct {
+ schemas []*types.Schema
+ apiExtClientSet apiextclientset.Interface
+ k8sClient rest.Interface
+ schemaStatus map[*types.Schema]*apiext.CustomResourceDefinition
+}
+
+func NewCRDStore(apiExtClientSet apiextclientset.Interface, k8sClient rest.Interface) *Store {
+ return &Store{
+ apiExtClientSet: apiExtClientSet,
+ k8sClient: k8sClient,
+ schemaStatus: map[*types.Schema]*apiext.CustomResourceDefinition{},
+ }
+}
+
+func (c *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
+ crd, ok := c.schemaStatus[schema]
+ if !ok {
+ return nil, nil
+ }
+
+ namespace := ""
+ parts := strings.SplitN(id, ":", 2)
+
+ if len(parts) == 2 {
+ namespace = parts[0]
+ id = parts[1]
+ }
+
+ req := c.k8sClient.Get().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Resource(crd.Status.AcceptedNames.Plural).
+ Name(id)
+
+ if namespace != "" {
+ req.Namespace(namespace)
+ }
+
+ result := &unstructured.Unstructured{}
+ err := req.Do().Into(result)
+ if err != nil {
+ return nil, err
+ }
+
+ c.back(result.Object, schema)
+
+ return result.Object, nil
+}
+
+func (c *Store) back(data map[string]interface{}, schema *types.Schema) {
+ //mapping.Metadata.Back(data)
+ data["type"] = schema.ID
+ name, _ := data["name"].(string)
+ namespace, _ := data["namespace"].(string)
+
+ if name != "" {
+ if namespace == "" {
+ data["id"] = name
+ } else {
+ data["id"] = namespace + ":" + name
+ }
+ }
+}
+
+func (c *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
+ crd, ok := c.schemaStatus[schema]
+ if !ok {
+ return nil
+ }
+
+ namespace := ""
+ parts := strings.SplitN(id, ":", 2)
+
+ if len(parts) == 2 {
+ namespace = parts[0]
+ id = parts[1]
+ }
+
+ prop := metav1.DeletePropagationForeground
+ req := c.k8sClient.Delete().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Resource(crd.Status.AcceptedNames.Plural).
+ Body(&metav1.DeleteOptions{
+ PropagationPolicy: &prop,
+ }).
+ Name(id)
+
+ if namespace != "" {
+ req.Namespace(namespace)
+ }
+
+ result := &unstructured.Unstructured{}
+ return req.Do().Into(result)
+}
+
+func (c *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
+ crd, ok := c.schemaStatus[schema]
+ if !ok {
+ return nil, nil
+ }
+
+ req := c.k8sClient.Get().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Resource(crd.Status.AcceptedNames.Plural)
+
+ resultList := &unstructured.UnstructuredList{}
+ err := req.Do().Into(resultList)
+ if err != nil {
+ return nil, err
+ }
+
+ result := []map[string]interface{}{}
+
+ for _, obj := range resultList.Items {
+ c.back(obj.Object, schema)
+ result = append(result, obj.Object)
+ }
+
+ return result, nil
+}
+
+func (c *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
+ crd, ok := c.schemaStatus[schema]
+ if !ok {
+ return nil, nil
+ }
+
+ namespace := ""
+ parts := strings.SplitN(id, ":", 2)
+
+ if len(parts) == 2 {
+ namespace = parts[0]
+ id = parts[1]
+ }
+
+ req := c.k8sClient.Get().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Resource(crd.Status.AcceptedNames.Plural).
+ Name(id)
+
+ if namespace != "" {
+ req.Namespace(namespace)
+ }
+
+ result := &unstructured.Unstructured{}
+ err := req.Do().Into(result)
+ if err != nil {
+ return nil, err
+ }
+
+ //mapping.Metadata.Forward(data)
+ for k, v := range data {
+ if k == "metadata" {
+ continue
+ }
+ result.Object[k] = v
+ }
+
+ req = c.k8sClient.Put().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Resource(crd.Status.AcceptedNames.Plural).
+ Body(result).
+ Name(id)
+
+ if namespace != "" {
+ req.Namespace(namespace)
+ }
+
+ result = &unstructured.Unstructured{}
+ err = req.Do().Into(result)
+ if err != nil {
+ return nil, err
+ }
+
+ c.back(result.Object, schema)
+ return result.Object, nil
+}
+
+func (c *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
+ crd, ok := c.schemaStatus[schema]
+ if !ok {
+ return nil, nil
+ }
+
+ namespace, _ := data["namespace"].(string)
+
+ //mapping.Metadata.Forward(data)
+
+ data["apiVersion"] = crd.Spec.Group + "/" + crd.Spec.Version
+ data["kind"] = crd.Status.AcceptedNames.Kind
+
+ req := c.k8sClient.Post().
+ Prefix("apis", crd.Spec.Group, crd.Spec.Version).
+ Body(&unstructured.Unstructured{
+ Object: data,
+ }).
+ Resource(crd.Status.AcceptedNames.Plural)
+
+ if crd.Spec.Scope == apiext.NamespaceScoped {
+ req.Namespace(namespace)
+ }
+
+ result := &unstructured.Unstructured{}
+ err := req.Do().Into(result)
+ if err != nil {
+ return nil, err
+ }
+
+ c.back(result.Object, schema)
+ return result.Object, nil
+}
+
+func (c *Store) AddSchemas(ctx context.Context, schemas *types.Schemas) error {
+ if schemas.Err() != nil {
+ return schemas.Err()
+ }
+
+ for _, schema := range schemas.Schemas() {
+ if schema.Store != nil || !contains(schema.CollectionMethods, http.MethodGet) {
+ continue
+ }
+
+ schema.Store = c
+ c.schemas = append(c.schemas, schema)
+ }
+
+ ready, err := c.getReadyCRDs()
+ if err != nil {
+ return err
+ }
+
+ for _, schema := range c.schemas {
+ crd, err := c.createCRD(schema, ready)
+ if err != nil {
+ return err
+ }
+ c.schemaStatus[schema] = crd
+ }
+
+ ready, err = c.getReadyCRDs()
+ if err != nil {
+ return err
+ }
+
+ for schema, crd := range c.schemaStatus {
+ if _, ok := ready[crd.Name]; !ok {
+ if err := c.waitCRD(ctx, crd.Name, schema); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func contains(list []string, s string) bool {
+ for _, i := range list {
+ if i == s {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *Store) waitCRD(ctx context.Context, crdName string, schema *types.Schema) error {
+ logrus.Infof("Waiting for CRD %s to become available", crdName)
+ defer logrus.Infof("Done waiting for CRD %s to become available", crdName)
+
+ first := true
+ return wait.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) {
+ if !first {
+ logrus.Infof("Waiting for CRD %s to become available", crdName)
+ }
+ first = false
+
+ crd, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crdName, metav1.GetOptions{})
+ if err != nil {
+ return false, err
+ }
+
+ for _, cond := range crd.Status.Conditions {
+ switch cond.Type {
+ case apiext.Established:
+ if cond.Status == apiext.ConditionTrue {
+ c.schemaStatus[schema] = crd
+ return true, err
+ }
+ case apiext.NamesAccepted:
+ if cond.Status == apiext.ConditionFalse {
+ logrus.Infof("Name conflict on %s: %v\n", crdName, cond.Reason)
+ }
+ }
+ }
+
+ return false, ctx.Err()
+ })
+}
+
+func (c *Store) createCRD(schema *types.Schema, ready map[string]apiext.CustomResourceDefinition) (*apiext.CustomResourceDefinition, error) {
+ plural := strings.ToLower(schema.PluralName)
+ name := strings.ToLower(plural + "." + schema.Version.Group)
+
+ crd, ok := ready[name]
+ if ok {
+ return &crd, nil
+ }
+
+ crd = apiext.CustomResourceDefinition{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ },
+ Spec: apiext.CustomResourceDefinitionSpec{
+ Group: schema.Version.Group,
+ Version: schema.Version.Version,
+ Scope: getScope(schema),
+ Names: apiext.CustomResourceDefinitionNames{
+ Plural: plural,
+ Kind: capitalize(schema.ID),
+ },
+ },
+ }
+
+ logrus.Infof("Creating CRD %s", name)
+ _, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().Create(&crd)
+ if errors.IsAlreadyExists(err) {
+ return &crd, nil
+ }
+ return &crd, err
+}
+
+func (c *Store) getReadyCRDs() (map[string]apiext.CustomResourceDefinition, error) {
+ list, err := c.apiExtClientSet.ApiextensionsV1beta1().CustomResourceDefinitions().List(metav1.ListOptions{})
+ if err != nil {
+ return nil, err
+ }
+
+ result := map[string]apiext.CustomResourceDefinition{}
+
+ for _, crd := range list.Items {
+ for _, cond := range crd.Status.Conditions {
+ switch cond.Type {
+ case apiext.Established:
+ if cond.Status == apiext.ConditionTrue {
+ result[crd.Name] = crd
+ }
+ }
+ }
+ }
+
+ return result, nil
+}
+
+func getScope(schema *types.Schema) apiext.ResourceScope {
+ for name := range schema.ResourceFields {
+ if name == "namespace" {
+ return apiext.NamespaceScoped
+ }
+ }
+
+ return apiext.ClusterScoped
+}
+
+func capitalize(s string) string {
+ if len(s) == 0 {
+ return s
+ }
+
+ return strings.ToUpper(s[0:1]) + s[1:]
+}
diff --git a/store/empty/empty_store.go b/store/empty/empty_store.go
new file mode 100644
index 00000000..4c458e02
--- /dev/null
+++ b/store/empty/empty_store.go
@@ -0,0 +1,26 @@
+package empty
+
+import "github.com/rancher/norman/types"
+
+type Store struct {
+}
+
+func (e *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
+ return nil
+}
+
+func (e *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
+ return nil, nil
+}
+
+func (e *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
+ return nil, nil
+}
+
+func (e *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
+ return nil, nil
+}
+
+func (e *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
+ return nil, nil
+}
diff --git a/store/proxy/proxy_store.go b/store/proxy/proxy_store.go
new file mode 100644
index 00000000..1dd462de
--- /dev/null
+++ b/store/proxy/proxy_store.go
@@ -0,0 +1,196 @@
+package proxy
+
+import (
+ "strings"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/rest"
+)
+
+type Store struct {
+ k8sClient *rest.RESTClient
+ prefix []string
+ group string
+ version string
+ kind string
+ resourcePlural string
+}
+
+func NewProxyStore(k8sClient *rest.RESTClient,
+ prefix []string, group, version, kind, resourcePlural string) *Store {
+ return &Store{
+ k8sClient: k8sClient,
+ prefix: prefix,
+ group: group,
+ version: version,
+ kind: kind,
+ resourcePlural: resourcePlural,
+ }
+}
+
+func (p *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
+ namespace, id := splitID(id)
+
+ req := p.common(namespace, p.k8sClient.Get()).
+ Name(id)
+
+ return p.singleResult(schema, req)
+}
+
+func (p *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
+ namespace := getNamespace(apiContext, opt)
+
+ req := p.common(namespace, p.k8sClient.Get())
+
+ resultList := &unstructured.UnstructuredList{}
+ err := req.Do().Into(resultList)
+ if err != nil {
+ return nil, err
+ }
+
+ result := []map[string]interface{}{}
+
+ for _, obj := range resultList.Items {
+ result = append(result, p.fromInternal(schema, obj.Object))
+ }
+
+ return result, nil
+}
+
+func getNamespace(apiContext *types.APIContext, opt *types.QueryOptions) string {
+ if val, ok := apiContext.SubContext["namespace"]; ok {
+ return convert.ToString(val)
+ }
+
+ if opt == nil {
+ return ""
+ }
+
+ for _, condition := range opt.Conditions {
+ if condition.Field == "namespace" && len(condition.Values) > 0 {
+ return convert.ToString(condition.Values[0])
+ }
+ }
+
+ return ""
+}
+
+func (p *Store) Create(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}) (map[string]interface{}, error) {
+ namespace, _ := data["namespace"].(string)
+ p.toInternal(schema.Mapper, data)
+
+ req := p.common(namespace, p.k8sClient.Post()).
+ Body(&unstructured.Unstructured{
+ Object: data,
+ })
+
+ return p.singleResult(schema, req)
+}
+
+func (p *Store) toInternal(mapper types.Mapper, data map[string]interface{}) {
+ if mapper != nil {
+ mapper.ToInternal(data)
+ }
+
+ if p.group == "" {
+ data["apiVersion"] = p.version
+ } else {
+ data["apiVersion"] = p.group + "/" + p.version
+ }
+ data["kind"] = p.kind
+}
+
+func (p *Store) Update(apiContext *types.APIContext, schema *types.Schema, data map[string]interface{}, id string) (map[string]interface{}, error) {
+ existing, err := p.ByID(apiContext, schema, id)
+ if err != nil {
+ return data, nil
+ }
+
+ for k, v := range data {
+ existing[k] = v
+ }
+
+ p.toInternal(schema.Mapper, existing)
+ namespace, id := splitID(id)
+
+ req := p.common(namespace, p.k8sClient.Put()).
+ Body(&unstructured.Unstructured{
+ Object: existing,
+ }).
+ Name(id)
+
+ return p.singleResult(schema, req)
+}
+
+func (p *Store) Delete(apiContext *types.APIContext, schema *types.Schema, id string) error {
+ namespace, id := splitID(id)
+
+ prop := metav1.DeletePropagationForeground
+ req := p.common(namespace, p.k8sClient.Delete()).
+ Body(&metav1.DeleteOptions{
+ PropagationPolicy: &prop,
+ }).
+ Name(id)
+
+ return req.Do().Error()
+}
+
+func (p *Store) singleResult(schema *types.Schema, req *rest.Request) (map[string]interface{}, error) {
+ result := &unstructured.Unstructured{}
+ err := req.Do().Into(result)
+ if err != nil {
+ return nil, err
+ }
+
+ p.fromInternal(schema, result.Object)
+ return result.Object, nil
+}
+
+func splitID(id string) (string, string) {
+ namespace := ""
+ parts := strings.SplitN(id, ":", 2)
+ if len(parts) == 2 {
+ namespace = parts[0]
+ id = parts[1]
+ }
+
+ return namespace, id
+}
+
+func (p *Store) common(namespace string, req *rest.Request) *rest.Request {
+ prefix := append([]string{}, p.prefix...)
+ if p.group != "" {
+ prefix = append(prefix, p.group)
+ }
+ prefix = append(prefix, p.version)
+ req.Prefix(prefix...).
+ Resource(p.resourcePlural)
+
+ if namespace != "" {
+ req.Namespace(namespace)
+ }
+
+ return req
+}
+
+func (p *Store) fromInternal(schema *types.Schema, data map[string]interface{}) map[string]interface{} {
+ if schema.Mapper != nil {
+ schema.Mapper.FromInternal(data)
+ }
+ data["type"] = schema.ID
+ name, _ := data["name"].(string)
+ namespace, _ := data["namespace"].(string)
+
+ if name != "" {
+ if namespace == "" {
+ data["id"] = name
+ } else {
+ data["id"] = namespace + ":" + name
+ }
+ }
+
+ return data
+}
diff --git a/store/reference.go b/store/reference.go
deleted file mode 100644
index 12937f18..00000000
--- a/store/reference.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package store
-
-type ReferenceValidator interface {
-
- Validate(resourceType, resourceID string) bool
-
-}
\ No newline at end of file
diff --git a/store/schema/schema_store.go b/store/schema/schema_store.go
new file mode 100644
index 00000000..bd62eaf5
--- /dev/null
+++ b/store/schema/schema_store.go
@@ -0,0 +1,48 @@
+package schema
+
+import (
+ "encoding/json"
+
+ "strings"
+
+ "github.com/rancher/norman/store/empty"
+ "github.com/rancher/norman/types"
+)
+
+type Store struct {
+ empty.Store
+}
+
+func NewSchemaStore() types.Store {
+ return &Store{}
+}
+
+func (s *Store) ByID(apiContext *types.APIContext, schema *types.Schema, id string) (map[string]interface{}, error) {
+ for _, schema := range apiContext.Schemas.Schemas() {
+ if strings.EqualFold(schema.ID, id) {
+ schemaData := map[string]interface{}{}
+
+ data, err := json.Marshal(schema)
+ if err != nil {
+ return nil, err
+ }
+
+ return schemaData, json.Unmarshal(data, &schemaData)
+ }
+ }
+ return nil, nil
+}
+
+func (s *Store) List(apiContext *types.APIContext, schema *types.Schema, opt *types.QueryOptions) ([]map[string]interface{}, error) {
+ schemaData := []map[string]interface{}{}
+
+ data, err := json.Marshal(apiContext.Schemas.Schemas())
+ if err != nil {
+ return nil, err
+ }
+
+ if err := json.Unmarshal(data, &schemaData); err != nil {
+ return nil, err
+ }
+ return schemaData, nil
+}
diff --git a/types/condition.go b/types/condition.go
new file mode 100644
index 00000000..0d5bdd07
--- /dev/null
+++ b/types/condition.go
@@ -0,0 +1,108 @@
+package types
+
+var (
+ COND_EQ = QueryConditionType{"eq", 1}
+ COND_NE = QueryConditionType{"ne", 1}
+ COND_NULL = QueryConditionType{"null", 0}
+ COND_NOTNULL = QueryConditionType{"notnull", 0}
+ COND_IN = QueryConditionType{"in", -1}
+ COND_NOTIN = QueryConditionType{"notin", -1}
+ COND_OR = QueryConditionType{"or", 1}
+ COND_AND = QueryConditionType{"and", 1}
+
+ mods = map[string]QueryConditionType{
+ COND_EQ.Name: COND_EQ,
+ COND_NE.Name: COND_NE,
+ COND_NULL.Name: COND_NULL,
+ COND_NOTNULL.Name: COND_NOTNULL,
+ COND_IN.Name: COND_IN,
+ COND_NOTIN.Name: COND_NOTIN,
+ COND_OR.Name: COND_OR,
+ COND_AND.Name: COND_AND,
+ }
+)
+
+type QueryConditionType struct {
+ Name string
+ Args int
+}
+
+type QueryCondition struct {
+ Field string
+ Values []interface{}
+ conditionType QueryConditionType
+ left, right *QueryCondition
+}
+
+func (q *QueryCondition) ToCondition() Condition {
+ cond := Condition{
+ Modifier: q.conditionType.Name,
+ }
+ if q.conditionType.Args == 1 && len(q.Values) > 0 {
+ cond.Value = q.Values[0]
+ } else if q.conditionType.Args == -1 {
+ cond.Value = q.Values
+ }
+
+ return cond
+}
+
+func ValidMod(mod string) bool {
+ _, ok := mods[mod]
+ return ok
+}
+
+func NewConditionFromString(field, mod string, values ...interface{}) *QueryCondition {
+ return &QueryCondition{
+ Field: field,
+ Values: values,
+ conditionType: mods[mod],
+ }
+}
+
+func NewCondition(mod QueryConditionType, values ...interface{}) *QueryCondition {
+ return &QueryCondition{
+ Values: values,
+ conditionType: mod,
+ }
+}
+
+func NE(value interface{}) *QueryCondition {
+ return NewCondition(COND_NE, value)
+}
+
+func EQ(value interface{}) *QueryCondition {
+ return NewCondition(COND_EQ, value)
+}
+
+func NULL(value interface{}) *QueryCondition {
+ return NewCondition(COND_NULL)
+}
+
+func NOTNULL(value interface{}) *QueryCondition {
+ return NewCondition(COND_NOTNULL)
+}
+
+func IN(values ...interface{}) *QueryCondition {
+ return NewCondition(COND_IN, values...)
+}
+
+func NOTIN(values ...interface{}) *QueryCondition {
+ return NewCondition(COND_NOTIN, values...)
+}
+
+func (c *QueryCondition) AND(right *QueryCondition) *QueryCondition {
+ return &QueryCondition{
+ conditionType: COND_AND,
+ left: c,
+ right: right,
+ }
+}
+
+func (c *QueryCondition) OR(right *QueryCondition) *QueryCondition {
+ return &QueryCondition{
+ conditionType: COND_OR,
+ left: c,
+ right: right,
+ }
+}
diff --git a/types/convert/convert.go b/types/convert/convert.go
new file mode 100644
index 00000000..1e585bc5
--- /dev/null
+++ b/types/convert/convert.go
@@ -0,0 +1,150 @@
+package convert
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+ "unicode"
+)
+
+func Singular(value interface{}) interface{} {
+ if slice, ok := value.([]string); ok {
+ if len(slice) == 0 {
+ return nil
+ }
+ return slice[0]
+ }
+ if slice, ok := value.([]interface{}); ok {
+ if len(slice) == 0 {
+ return nil
+ }
+ return slice[0]
+ }
+ return value
+}
+
+func ToString(value interface{}) string {
+ single := Singular(value)
+ if single == nil {
+ return ""
+ }
+ return fmt.Sprint(single)
+}
+
+func ToTimestamp(value interface{}) (int64, error) {
+ str := ToString(value)
+ if str == "" {
+ return 0, errors.New("Invalid date")
+ }
+ t, err := time.Parse(time.RFC3339, str)
+ if err != nil {
+ return 0, err
+ }
+ return t.UnixNano() / 1000000, nil
+}
+
+func ToBool(value interface{}) bool {
+ value = Singular(value)
+
+ b, ok := value.(bool)
+ if ok {
+ return b
+ }
+
+ str := strings.ToLower(ToString(value))
+ return str == "true" || str == "t" || str == "yes" || str == "y"
+}
+
+func ToNumber(value interface{}) (int64, error) {
+ value = Singular(value)
+
+ i, ok := value.(int64)
+ if ok {
+ return i, nil
+ }
+ return strconv.ParseInt(ToString(value), 10, 64)
+}
+
+func Capitalize(s string) string {
+ if len(s) <= 1 {
+ return strings.ToUpper(s)
+ }
+
+ return strings.ToUpper(s[:1]) + s[1:]
+}
+
+func LowerTitle(input string) string {
+ runes := []rune(input)
+ for i := 0; i < len(runes); i++ {
+ if unicode.IsUpper(runes[i]) &&
+ (i == 0 ||
+ i == len(runes)-1 ||
+ unicode.IsUpper(runes[i+1])) {
+ runes[i] = unicode.ToLower(runes[i])
+ } else {
+ break
+ }
+ }
+
+ return string(runes)
+}
+
+func IsEmpty(v interface{}) bool {
+ return v == nil || v == "" || v == 0 || v == false
+}
+
+func ToMapInterface(obj interface{}) map[string]interface{} {
+ v, _ := obj.(map[string]interface{})
+ return v
+}
+
+func ToMapSlice(obj interface{}) []map[string]interface{} {
+ if v, ok := obj.([]map[string]interface{}); ok {
+ return v
+ }
+ vs, _ := obj.([]interface{})
+ result := []map[string]interface{}{}
+ for _, item := range vs {
+ if v, ok := item.(map[string]interface{}); ok {
+ result = append(result, v)
+ } else {
+ return nil
+ }
+ }
+
+ return result
+}
+
+func ToStringSlice(data interface{}) []string {
+ if v, ok := data.([]string); ok {
+ return v
+ }
+ if v, ok := data.([]interface{}); ok {
+ result := []string{}
+ for _, item := range v {
+ result = append(result, ToString(item))
+ }
+ return result
+ }
+ return nil
+}
+
+func ToObj(data interface{}, obj interface{}) error {
+ bytes, err := json.Marshal(data)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(bytes, obj)
+}
+
+func EncodeToMap(obj interface{}) (map[string]interface{}, error) {
+ bytes, err := json.Marshal(obj)
+ if err != nil {
+ return nil, err
+ }
+ result := map[string]interface{}{}
+ return result, json.Unmarshal(bytes, result)
+}
diff --git a/types/convert/value_set_string.go b/types/convert/value_set_string.go
new file mode 100644
index 00000000..88f981d3
--- /dev/null
+++ b/types/convert/value_set_string.go
@@ -0,0 +1,19 @@
+package convert
+
+import (
+ "regexp"
+ "strings"
+)
+
+var (
+ splitRegexp = regexp.MustCompile("[[:space:]]*,[[:space:]]*")
+)
+
+func ToValuesSlice(value string) []string {
+ value = strings.TrimSpace(value)
+ if strings.HasPrefix(value, "(") && strings.HasSuffix(value, ")") {
+ return splitRegexp.Split(value[1:len(value)-1], -1)
+ } else {
+ return []string{value}
+ }
+}
diff --git a/types/definition/definition.go b/types/definition/definition.go
new file mode 100644
index 00000000..220563f2
--- /dev/null
+++ b/types/definition/definition.go
@@ -0,0 +1,24 @@
+package definition
+
+import "strings"
+
+func IsMapType(fieldType string) bool {
+ return strings.HasPrefix(fieldType, "map[") && strings.HasSuffix(fieldType, "]")
+}
+
+func IsArrayType(fieldType string) bool {
+ return strings.HasPrefix(fieldType, "array[") && strings.HasSuffix(fieldType, "]")
+}
+
+func IsReferenceType(fieldType string) bool {
+ return strings.HasPrefix(fieldType, "reference[") && strings.HasSuffix(fieldType, "]")
+}
+
+func SubType(fieldType string) string {
+ i := strings.Index(fieldType, "[")
+ if i <= 0 || i >= len(fieldType)-1 {
+ return fieldType
+ }
+
+ return fieldType[i+1 : len(fieldType)-1]
+}
diff --git a/types/mapper.go b/types/mapper.go
new file mode 100644
index 00000000..ba99c363
--- /dev/null
+++ b/types/mapper.go
@@ -0,0 +1,104 @@
+package types
+
+import (
+ "github.com/pkg/errors"
+ "github.com/rancher/norman/types/definition"
+)
+
+type Mapper interface {
+ FromInternal(data map[string]interface{})
+ ToInternal(data map[string]interface{})
+ ModifySchema(schema *Schema, schemas *Schemas) error
+}
+
+type TypeMapper struct {
+ Mappers []Mapper
+ typeName string
+ subSchemas map[string]*Schema
+ subArraySchemas map[string]*Schema
+}
+
+func (t *TypeMapper) FromInternal(data map[string]interface{}) {
+ for fieldName, schema := range t.subSchemas {
+ if schema.Mapper == nil {
+ continue
+ }
+ fieldData, _ := data[fieldName].(map[string]interface{})
+ schema.Mapper.FromInternal(fieldData)
+ }
+
+ for fieldName, schema := range t.subArraySchemas {
+ if schema.Mapper == nil {
+ continue
+ }
+ datas, _ := data[fieldName].([]interface{})
+ for _, fieldData := range datas {
+ mapFieldData, _ := fieldData.(map[string]interface{})
+ schema.Mapper.FromInternal(mapFieldData)
+ }
+ }
+
+ for _, mapper := range t.Mappers {
+ mapper.FromInternal(data)
+ }
+
+ if data != nil {
+ data["type"] = t.typeName
+ }
+}
+
+func (t *TypeMapper) ToInternal(data map[string]interface{}) {
+ for i := len(t.Mappers) - 1; i <= 0; i-- {
+ t.Mappers[i].ToInternal(data)
+ }
+
+ for fieldName, schema := range t.subArraySchemas {
+ if schema.Mapper == nil {
+ continue
+ }
+ datas, _ := data[fieldName].([]map[string]interface{})
+ for _, fieldData := range datas {
+ schema.Mapper.ToInternal(fieldData)
+ }
+ }
+
+ for fieldName, schema := range t.subSchemas {
+ if schema.Mapper == nil {
+ continue
+ }
+ fieldData, _ := data[fieldName].(map[string]interface{})
+ schema.Mapper.ToInternal(fieldData)
+ }
+}
+
+func (t *TypeMapper) ModifySchema(schema *Schema, schemas *Schemas) error {
+ t.subSchemas = map[string]*Schema{}
+ t.subArraySchemas = map[string]*Schema{}
+ t.typeName = schema.ID
+
+ mapperSchema := schema
+ if schema.InternalSchema != nil {
+ mapperSchema = schema.InternalSchema
+ }
+ for name, field := range mapperSchema.ResourceFields {
+ fieldType := field.Type
+ targetMap := t.subSchemas
+ if definition.IsArrayType(fieldType) {
+ fieldType = definition.SubType(fieldType)
+ targetMap = t.subArraySchemas
+ }
+
+ schema := schemas.Schema(&schema.Version, fieldType)
+ if schema != nil {
+ targetMap[name] = schema
+ }
+ }
+
+ for _, mapper := range t.Mappers {
+ if err := mapper.ModifySchema(schema, schemas); err != nil {
+ return errors.Wrapf(err, "mapping type %s", schema.ID)
+ }
+ }
+
+ return nil
+}
diff --git a/types/mapping/mapper.go b/types/mapping/mapper.go
new file mode 100644
index 00000000..cff51216
--- /dev/null
+++ b/types/mapping/mapper.go
@@ -0,0 +1,49 @@
+package mapping
+
+var (
+//Metadata m.Mapper = m.CombinedMapper{
+// Mappers: []m.Mapper{
+// m.Enum{
+// From: "name",
+// To: "metadata/name",
+// },
+// m.Enum{
+// From: "uuid",
+// To: "metadata/uid",
+// },
+// m.Enum{
+// From: "resourceVersion",
+// To: "metadata/resourceVersion",
+// },
+// m.Enum{
+// From: "created",
+// To: "metadata/creationTimestamp",
+// },
+// m.Enum{
+// From: "removed",
+// To: "metadata/deletionTimestamp",
+// },
+// m.Enum{
+// From: "namespace",
+// To: "metadata/namespace",
+// },
+// m.Enum{
+// From: "labels",
+// To: "metadata/labels",
+// },
+// m.Enum{
+// From: "annotations",
+// To: "metadata/annotations",
+// },
+// m.Swap{
+// Left: "type",
+// Right: "kind",
+// },
+// m.LabelField{
+// Fields: []string{
+// "description",
+// },
+// },
+// },
+//}
+)
diff --git a/types/mapping/mapper/check.go b/types/mapping/mapper/check.go
new file mode 100644
index 00000000..58993615
--- /dev/null
+++ b/types/mapping/mapper/check.go
@@ -0,0 +1,28 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+)
+
+func getInternal(schema *types.Schema) (*types.Schema, error) {
+ if schema.InternalSchema == nil {
+ return nil, fmt.Errorf("no internal schema found for schema %s", schema.ID)
+ }
+
+ return schema.InternalSchema, nil
+}
+
+func validateInternalField(field string, schema *types.Schema) (*types.Schema, error) {
+ internalSchema, err := getInternal(schema)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, ok := internalSchema.ResourceFields[field]; !ok {
+ return nil, fmt.Errorf("field %s missing on internal schema %s", field, schema.ID)
+ }
+
+ return internalSchema, nil
+}
diff --git a/types/mapping/mapper/copy.go b/types/mapping/mapper/copy.go
new file mode 100644
index 00000000..45baab24
--- /dev/null
+++ b/types/mapping/mapper/copy.go
@@ -0,0 +1,25 @@
+package mapper
+
+import "strings"
+
+type Copy struct {
+ From, To string
+}
+
+func (c Copy) Forward(data map[string]interface{}) {
+ val, ok := GetValue(data, strings.Split(c.From, "/")...)
+ if !ok {
+ return
+ }
+
+ PutValue(data, val, strings.Split(c.To, "/")...)
+}
+
+func (c Copy) Back(data map[string]interface{}) {
+ val, ok := GetValue(data, strings.Split(c.To, "/")...)
+ if !ok {
+ return
+ }
+
+ PutValue(data, val, strings.Split(c.From, "/")...)
+}
diff --git a/types/mapping/mapper/drop.go b/types/mapping/mapper/drop.go
new file mode 100644
index 00000000..a76d9ac5
--- /dev/null
+++ b/types/mapping/mapper/drop.go
@@ -0,0 +1,32 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+)
+
+type Drop struct {
+ Field string
+}
+
+func (d Drop) FromInternal(data map[string]interface{}) {
+ delete(data, d.Field)
+}
+
+func (d Drop) ToInternal(data map[string]interface{}) {
+}
+
+func (d Drop) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ _, err := getInternal(schema)
+ if err != nil {
+ return err
+ }
+
+ if _, ok := schema.ResourceFields[d.Field]; !ok {
+ return fmt.Errorf("can not drop missing field %s on %s", d.Field, schema.ID)
+ }
+
+ delete(schema.ResourceFields, d.Field)
+ return nil
+}
diff --git a/types/mapping/mapper/embed.go b/types/mapping/mapper/embed.go
new file mode 100644
index 00000000..0db2b9ae
--- /dev/null
+++ b/types/mapping/mapper/embed.go
@@ -0,0 +1,65 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+)
+
+type Embed struct {
+ Field string
+ ignoreOverride bool
+ embeddedFields []string
+}
+
+func (e *Embed) FromInternal(data map[string]interface{}) {
+ sub, _ := data[e.Field].(map[string]interface{})
+ for _, fieldName := range e.embeddedFields {
+ if v, ok := sub[fieldName]; ok {
+ data[fieldName] = v
+ }
+ }
+ delete(data, e.Field)
+}
+
+func (e *Embed) ToInternal(data map[string]interface{}) {
+ sub := map[string]interface{}{}
+ for _, fieldName := range e.embeddedFields {
+ if v, ok := data[fieldName]; ok {
+ sub[fieldName] = v
+ }
+
+ delete(data, fieldName)
+ }
+
+ data[e.Field] = sub
+}
+
+func (e *Embed) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ internalSchema, err := validateInternalField(e.Field, schema)
+ if err != nil {
+ return err
+ }
+
+ e.embeddedFields = []string{}
+
+ embeddedSchemaID := internalSchema.ResourceFields[e.Field].Type
+ embeddedSchema := schemas.Schema(&schema.Version, embeddedSchemaID)
+ if embeddedSchema == nil {
+ return fmt.Errorf("failed to find schema %s for embedding", embeddedSchemaID)
+ }
+
+ for name, field := range embeddedSchema.ResourceFields {
+ if !e.ignoreOverride {
+ if _, ok := schema.ResourceFields[name]; ok {
+ return fmt.Errorf("embedding field %s on %s will overwrite the field %s",
+ e.Field, schema.ID, name)
+ }
+ }
+ schema.ResourceFields[name] = field
+ e.embeddedFields = append(e.embeddedFields, name)
+ }
+
+ delete(schema.ResourceFields, e.Field)
+ return nil
+}
diff --git a/types/mapping/mapper/enum.go b/types/mapping/mapper/enum.go
new file mode 100644
index 00000000..e2c276e3
--- /dev/null
+++ b/types/mapping/mapper/enum.go
@@ -0,0 +1,49 @@
+package mapper
+
+import (
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+)
+
+type Enum struct {
+ Field string
+ Values map[string][]string
+}
+
+func (e Enum) FromInternal(data map[string]interface{}) {
+ v, ok := data[e.Field]
+ if !ok {
+ return
+ }
+
+ str := convert.ToString(v)
+
+ mapping, ok := e.Values[str]
+ if ok {
+ data[e.Field] = mapping[0]
+ } else {
+ data[e.Field] = v
+ }
+}
+
+func (e Enum) ToInternal(data map[string]interface{}) {
+ v, ok := data[e.Field]
+ if !ok {
+ return
+ }
+
+ str := convert.ToString(v)
+ for newValue, values := range e.Values {
+ for _, value := range values {
+ if str == value {
+ data[e.Field] = newValue
+ return
+ }
+ }
+ }
+}
+
+func (e Enum) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ _, err := validateInternalField(e.Field, schema)
+ return err
+}
diff --git a/types/mapping/mapper/label_field.go b/types/mapping/mapper/label_field.go
new file mode 100644
index 00000000..5a0babf9
--- /dev/null
+++ b/types/mapping/mapper/label_field.go
@@ -0,0 +1,25 @@
+package mapper
+
+//type LabelField struct {
+// Fields []string
+//}
+//
+//func (l LabelField) Forward(data map[string]interface{}) {
+// for _, field := range l.Fields {
+// moveForLabel(field).Forward(data)
+// }
+//
+//}
+//
+//func (l LabelField) Back(data map[string]interface{}) {
+// for _, field := range l.Fields {
+// moveForLabel(field).Back(data)
+// }
+//}
+//
+//func moveForLabel(field string) *Enum {
+// return &Enum{
+// From: field,
+// To: "metadata/labels/io.cattle.field." + strings.ToLower(field),
+// }
+//}
diff --git a/types/mapping/mapper/move.go b/types/mapping/mapper/move.go
new file mode 100644
index 00000000..6c7ba333
--- /dev/null
+++ b/types/mapping/mapper/move.go
@@ -0,0 +1,48 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+)
+
+type Move struct {
+ From, To string
+}
+
+func (m Move) FromInternal(data map[string]interface{}) {
+ if v, ok := GetValue(data, m.From); ok {
+ data[m.To] = v
+ }
+}
+
+func (m Move) ToInternal(data map[string]interface{}) {
+ if v, ok := GetValue(data, m.To); ok {
+ data[m.From] = v
+ }
+}
+
+func (m Move) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ internalSchema, err := getInternal(schema)
+ if err != nil {
+ return err
+ }
+
+ field, ok := internalSchema.ResourceFields[m.From]
+ if !ok {
+ return fmt.Errorf("missing field %s on internal schema %s", m.From, internalSchema.ID)
+ }
+
+ _, ok = schema.ResourceFields[m.To]
+ if ok {
+ return fmt.Errorf("field %s already exists on schema %s", m.From, internalSchema.ID)
+ }
+
+ delete(schema.ResourceFields, m.From)
+
+ field.CodeName = convert.Capitalize(m.To)
+ schema.ResourceFields[m.To] = field
+
+ return nil
+}
diff --git a/types/mapping/mapper/slice_to_map.go b/types/mapping/mapper/slice_to_map.go
new file mode 100644
index 00000000..c80b827c
--- /dev/null
+++ b/types/mapping/mapper/slice_to_map.go
@@ -0,0 +1,60 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/definition"
+)
+
+type SliceToMap struct {
+ Field string
+ Key string
+}
+
+func (s SliceToMap) FromInternal(data map[string]interface{}) {
+ datas, _ := data[s.Field].([]interface{})
+ result := map[string]interface{}{}
+
+ for _, item := range datas {
+ if mapItem, ok := item.(map[string]interface{}); ok {
+ name, _ := mapItem[s.Key].(string)
+ delete(mapItem, s.Key)
+ result[name] = mapItem
+ }
+ }
+
+ data[s.Field] = result
+}
+
+func (s SliceToMap) ToInternal(data map[string]interface{}) {
+ datas, _ := data[s.Field].(map[string]interface{})
+ result := []map[string]interface{}{}
+
+ for name, item := range datas {
+ mapItem, _ := item.(map[string]interface{})
+ if mapItem != nil {
+ mapItem[s.Key] = name
+ result = append(result, mapItem)
+ }
+ }
+
+ data[s.Field] = result
+}
+
+func (s SliceToMap) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ internalSchema, err := validateInternalField(s.Field, schema)
+ if err != nil {
+ return err
+ }
+
+ field := internalSchema.ResourceFields[s.Field]
+ if !definition.IsArrayType(field.Type) {
+ return fmt.Errorf("field %s on %s is not an array", s.Field, internalSchema.ID)
+ }
+
+ field.Type = "map[" + definition.SubType(field.Type) + "]"
+ schema.ResourceFields[s.Field] = field
+
+ return nil
+}
diff --git a/types/mapping/mapper/swap.go b/types/mapping/mapper/swap.go
new file mode 100644
index 00000000..f135ac4c
--- /dev/null
+++ b/types/mapping/mapper/swap.go
@@ -0,0 +1,20 @@
+package mapper
+
+type Swap struct {
+ Left, Right string
+}
+
+func (s Swap) Forward(data map[string]interface{}) {
+ rightValue, rightOk := data[s.Right]
+ leftValue, leftOk := data[s.Left]
+ if rightOk {
+ data[s.Left] = rightValue
+ }
+ if leftOk {
+ data[s.Right] = leftValue
+ }
+}
+
+func (s Swap) Back(data map[string]interface{}) {
+ s.Forward(data)
+}
diff --git a/types/mapping/mapper/type.go b/types/mapping/mapper/type.go
new file mode 100644
index 00000000..ad5eac35
--- /dev/null
+++ b/types/mapping/mapper/type.go
@@ -0,0 +1 @@
+package mapper
diff --git a/types/mapping/mapper/union_embed.go b/types/mapping/mapper/union_embed.go
new file mode 100644
index 00000000..e9ac12ee
--- /dev/null
+++ b/types/mapping/mapper/union_embed.go
@@ -0,0 +1,68 @@
+package mapper
+
+import (
+ "fmt"
+
+ "github.com/rancher/norman/types"
+ "github.com/rancher/norman/types/convert"
+)
+
+type UnionMapping struct {
+ FieldName string
+ CheckFields []string
+}
+
+type UnionEmbed struct {
+ Fields []UnionMapping
+ embeds map[string]Embed
+}
+
+func (u *UnionEmbed) FromInternal(data map[string]interface{}) {
+ for _, embed := range u.embeds {
+ embed.FromInternal(data)
+ }
+}
+
+func (u *UnionEmbed) ToInternal(data map[string]interface{}) {
+outer:
+ for _, mapper := range u.Fields {
+ if len(mapper.CheckFields) == 0 {
+ continue
+ }
+
+ for _, check := range mapper.CheckFields {
+ v, ok := data[check]
+ if !ok || convert.IsEmpty(v) {
+ continue outer
+ }
+ }
+
+ embed := u.embeds[mapper.FieldName]
+ embed.ToInternal(data)
+ return
+ }
+}
+
+func (u *UnionEmbed) ModifySchema(schema *types.Schema, schemas *types.Schemas) error {
+ u.embeds = map[string]Embed{}
+
+ for _, mapping := range u.Fields {
+ embed := Embed{
+ Field: mapping.FieldName,
+ ignoreOverride: true,
+ }
+ if err := embed.ModifySchema(schema, schemas); err != nil {
+ return err
+ }
+
+ for _, checkField := range mapping.CheckFields {
+ if _, ok := schema.ResourceFields[checkField]; !ok {
+ return fmt.Errorf("missing check field %s on schema %s", checkField, schema.ID)
+ }
+ }
+
+ u.embeds[mapping.FieldName] = embed
+ }
+
+ return nil
+}
diff --git a/types/mapping/mapper/values.go b/types/mapping/mapper/values.go
new file mode 100644
index 00000000..f19b002d
--- /dev/null
+++ b/types/mapping/mapper/values.go
@@ -0,0 +1,83 @@
+package mapper
+
+func RemoveValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
+ for i, key := range keys {
+ if i == len(keys)-1 {
+ val, ok := data[key]
+ delete(data, key)
+ return val, ok
+ } else {
+ data, _ = data[key].(map[string]interface{})
+ }
+ }
+
+ return nil, false
+}
+
+func GetSlice(data map[string]interface{}, keys ...string) ([]map[string]interface{}, bool) {
+ val, ok := GetValue(data, keys...)
+ if !ok {
+ return nil, ok
+ }
+
+ slice, typeOk := val.([]map[string]interface{})
+ if typeOk {
+ return slice, typeOk
+ }
+
+ sliceNext, typeOk := val.([]interface{})
+ if !typeOk {
+ return nil, typeOk
+ }
+
+ result := []map[string]interface{}{}
+ for _, val := range sliceNext {
+ if v, ok := val.(map[string]interface{}); ok {
+ result = append(result, v)
+ }
+ }
+
+ return result, true
+
+}
+
+func GetValueN(data map[string]interface{}, keys ...string) interface{} {
+ val, _ := GetValue(data, keys...)
+ return val
+}
+
+func GetValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
+ for i, key := range keys {
+ if i == len(keys)-1 {
+ val, ok := data[key]
+ return val, ok
+ } else {
+ data, _ = data[key].(map[string]interface{})
+ }
+ }
+
+ return nil, false
+}
+
+func PutValue(data map[string]interface{}, val interface{}, keys ...string) {
+ // This is so ugly
+ for i, key := range keys {
+ if i == len(keys)-1 {
+ data[key] = val
+ } else {
+ newData, ok := data[key]
+ if ok {
+ newMap, ok := newData.(map[string]interface{})
+ if ok {
+ data = newMap
+ } else {
+ return
+ }
+ } else {
+ newMap := map[string]interface{}{}
+ data[key] = newMap
+ data = newMap
+ }
+ }
+ }
+}
diff --git a/types/reflection.go b/types/reflection.go
new file mode 100644
index 00000000..8d6a1878
--- /dev/null
+++ b/types/reflection.go
@@ -0,0 +1,326 @@
+package types
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/rancher/norman/types/convert"
+ "github.com/sirupsen/logrus"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+var (
+ resourceType = reflect.TypeOf(Resource{})
+ metaType = reflect.TypeOf(metav1.ObjectMeta{})
+ blacklistNames = map[string]bool{
+ "links": true,
+ "actions": true,
+ }
+)
+
+func (s *Schemas) AddMapperForType(version *APIVersion, obj interface{}, mapper Mapper) *Schemas {
+ t := reflect.TypeOf(obj)
+ typeName := convert.LowerTitle(t.Name())
+ return s.AddMapper(version, typeName, mapper)
+}
+
+func (s *Schemas) MustImport(version *APIVersion, obj interface{}, externalOverrides ...interface{}) *Schemas {
+ //TODO: remove
+ logrus.SetLevel(logrus.DebugLevel)
+ if _, err := s.Import(version, obj, externalOverrides...); err != nil {
+ panic(err)
+ }
+ return s
+}
+
+func (s *Schemas) Import(version *APIVersion, obj interface{}, externalOverrides ...interface{}) (*Schema, error) {
+ types := []reflect.Type{}
+ for _, override := range externalOverrides {
+ types = append(types, reflect.TypeOf(override))
+ }
+
+ return s.importType(version, reflect.TypeOf(obj), types...)
+}
+
+func (s *Schemas) newSchemaFromType(version *APIVersion, t reflect.Type, typeName string) (*Schema, error) {
+ schema := &Schema{
+ ID: typeName,
+ Version: *version,
+ CodeName: t.Name(),
+ ResourceFields: map[string]Field{},
+ }
+
+ if err := s.readFields(schema, t); err != nil {
+ return nil, err
+ }
+
+ return schema, nil
+}
+
+func (s *Schemas) importType(version *APIVersion, t reflect.Type, overrides ...reflect.Type) (*Schema, error) {
+ typeName := convert.LowerTitle(t.Name())
+
+ existing := s.Schema(version, typeName)
+ if existing != nil {
+ return existing, nil
+ }
+
+ logrus.Debugf("Inspecting schema %s for %v", typeName, t)
+
+ schema, err := s.newSchemaFromType(version, t, typeName)
+ if err != nil {
+ return nil, err
+ }
+
+ mapper := s.mapper(&schema.Version, schema.ID)
+ if mapper != nil {
+ copy, err := s.newSchemaFromType(version, t, typeName)
+ if err != nil {
+ return nil, err
+ }
+ schema.InternalSchema = copy
+ }
+
+ for _, override := range overrides {
+ if err := s.readFields(schema, override); err != nil {
+ return nil, err
+ }
+ }
+
+ if mapper == nil {
+ mapper = &TypeMapper{}
+ }
+
+ if err := mapper.ModifySchema(schema, s); err != nil {
+ return nil, err
+ }
+
+ schema.Mapper = mapper
+ s.AddSchema(schema)
+
+ return schema, nil
+}
+
+func jsonName(f reflect.StructField) string {
+ return strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
+}
+
+func (s *Schemas) readFields(schema *Schema, t reflect.Type) error {
+ if t == resourceType {
+ schema.CollectionMethods = []string{"GET", "POST"}
+ schema.ResourceMethods = []string{"GET", "PUT", "DELETE"}
+ }
+
+ for i := 0; i < t.NumField(); i++ {
+ field := t.Field(i)
+
+ if field.PkgPath != "" {
+ // unexported field
+ continue
+ }
+
+ jsonName := jsonName(field)
+
+ if jsonName == "-" {
+ continue
+ }
+
+ if field.Anonymous && jsonName == "" {
+ t := field.Type
+ if t.Kind() == reflect.Ptr {
+ t = t.Elem()
+ }
+ if t.Kind() == reflect.Struct {
+ if err := s.readFields(schema, t); err != nil {
+ return err
+ }
+ }
+ continue
+ }
+
+ fieldName := jsonName
+ if fieldName == "" {
+ fieldName = convert.LowerTitle(field.Name)
+ }
+
+ if blacklistNames[fieldName] {
+ logrus.Debugf("Ignoring blacklisted field %s.%s for %v", schema.ID, fieldName, field)
+ continue
+ }
+
+ logrus.Debugf("Inspecting field %s.%s for %v", schema.ID, fieldName, field)
+
+ schemaField := Field{
+ Create: true,
+ Update: true,
+ CodeName: field.Name,
+ }
+
+ fieldType := field.Type
+ if fieldType.Kind() == reflect.Ptr {
+ schemaField.Nullable = true
+ fieldType = fieldType.Elem()
+ }
+
+ if err := applyTag(&field, &schemaField); err != nil {
+ return err
+ }
+
+ if schemaField.Type == "" {
+ inferedType, err := s.determineSchemaType(&schema.Version, fieldType)
+ if err != nil {
+ return err
+ }
+ schemaField.Type = inferedType
+ }
+
+ if field.Type == metaType {
+ schema.CollectionMethods = []string{"GET", "POST"}
+ schema.ResourceMethods = []string{"GET", "PUT", "DELETE"}
+ }
+
+ logrus.Debugf("Setting field %s.%s: %#v", schema.ID, fieldName, schemaField)
+ schema.ResourceFields[fieldName] = schemaField
+ }
+
+ return nil
+}
+
+func applyTag(structField *reflect.StructField, field *Field) error {
+ for _, part := range strings.Split(structField.Tag.Get("norman"), ",") {
+ if part == "" {
+ continue
+ }
+
+ var err error
+ key, value := getKeyValue(part)
+
+ switch key {
+ case "type":
+ field.Type = value
+ case "codeName":
+ field.CodeName = value
+ case "default":
+ field.Default = value
+ case "nullabled":
+ field.Nullable = true
+ case "nocreate":
+ field.Create = false
+ case "writeOnly":
+ field.WriteOnly = true
+ case "required":
+ field.Required = true
+ case "noupdate":
+ field.Update = false
+ case "minLength":
+ field.MinLength, err = toInt(value, structField)
+ case "maxLength":
+ field.MaxLength, err = toInt(value, structField)
+ case "min":
+ field.Min, err = toInt(value, structField)
+ case "max":
+ field.Max, err = toInt(value, structField)
+ case "options":
+ field.Options = split(value)
+ if field.Type == "" {
+ field.Type = "enum"
+ }
+ case "validChars":
+ field.ValidChars = value
+ case "invalidChars":
+ field.InvalidChars = value
+ default:
+ return fmt.Errorf("invalid tag %s on field %s", key, structField.Name)
+ }
+
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func toInt(value string, structField *reflect.StructField) (*int64, error) {
+ i, err := strconv.ParseInt(value, 10, 64)
+ if err != nil {
+ return nil, fmt.Errorf("invalid number on field %s: %v", structField.Name, err)
+ }
+ return &i, nil
+}
+
+func split(input string) []string {
+ result := []string{}
+ for _, i := range strings.Split(input, "|") {
+ for _, part := range strings.Split(i, " ") {
+ part = strings.TrimSpace(part)
+ if len(part) > 0 {
+ result = append(result, part)
+ }
+ }
+ }
+
+ return result
+}
+
+func getKeyValue(input string) (string, string) {
+ var (
+ key, value string
+ )
+ parts := strings.SplitN(input, "=", 2)
+ key = parts[0]
+ if len(parts) > 1 {
+ value = parts[1]
+ }
+
+ return key, value
+}
+
+func (s *Schemas) determineSchemaType(version *APIVersion, t reflect.Type) (string, error) {
+ switch t.Kind() {
+ case reflect.Bool:
+ return "boolean", nil
+ case reflect.Int:
+ fallthrough
+ case reflect.Int32:
+ fallthrough
+ case reflect.Int64:
+ return "int", nil
+ case reflect.Interface:
+ return "json", nil
+ case reflect.Map:
+ subType, err := s.determineSchemaType(version, t.Elem())
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("map[%s]", subType), nil
+ case reflect.Slice:
+ subType, err := s.determineSchemaType(version, t.Elem())
+ if err != nil {
+ return "", err
+ }
+ return fmt.Sprintf("array[%s]", subType), nil
+ case reflect.String:
+ return "string", nil
+ case reflect.Struct:
+ if t.Name() == "Time" {
+ return "date", nil
+ }
+ if t.Name() == "IntOrString" {
+ return "string", nil
+ }
+ if t.Name() == "Quantity" {
+ return "string", nil
+ }
+ schema, err := s.importType(version, t)
+ if err != nil {
+ return "", err
+ }
+ return schema.ID, nil
+ default:
+ return "", fmt.Errorf("unknown type kind %s", t.Kind())
+ }
+
+}
diff --git a/types/schemas.go b/types/schemas.go
new file mode 100644
index 00000000..7b14833d
--- /dev/null
+++ b/types/schemas.go
@@ -0,0 +1,180 @@
+package types
+
+import (
+ "bytes"
+ "fmt"
+ "strings"
+
+ "github.com/rancher/norman/name"
+ "github.com/rancher/norman/types/convert"
+)
+
+type SchemaCollection struct {
+ Data []Schema
+}
+
+type Schemas struct {
+ schemasByPath map[string]map[string]*Schema
+ mappers map[string]map[string]Mapper
+ versions []APIVersion
+ schemas []*Schema
+ errors []error
+}
+
+func NewSchemas() *Schemas {
+ return &Schemas{
+ schemasByPath: map[string]map[string]*Schema{},
+ mappers: map[string]map[string]Mapper{},
+ }
+}
+
+func (s *Schemas) Err() error {
+ return NewErrors(s.errors)
+}
+
+func (s *Schemas) AddSchema(schema *Schema) *Schemas {
+ schema.Type = "schema"
+ if schema.ID == "" {
+ s.errors = append(s.errors, fmt.Errorf("ID is not set on schema: %v", schema))
+ return s
+ }
+ if schema.Version.Path == "" || schema.Version.Group == "" || schema.Version.Version == "" {
+ s.errors = append(s.errors, fmt.Errorf("version is not set on schema: %s", schema.ID))
+ return s
+ }
+ if schema.PluralName == "" {
+ schema.PluralName = name.GuessPluralName(schema.ID)
+ }
+ if schema.CodeName == "" {
+ schema.CodeName = convert.Capitalize(schema.ID)
+ }
+
+ schemas, ok := s.schemasByPath[schema.Version.Path]
+ if !ok {
+ schemas = map[string]*Schema{}
+ s.schemasByPath[schema.Version.Path] = schemas
+ s.versions = append(s.versions, schema.Version)
+ }
+
+ if _, ok := schemas[schema.ID]; !ok {
+ schemas[schema.ID] = schema
+ s.schemas = append(s.schemas, schema)
+ }
+
+ return s
+}
+
+func (s *Schemas) AddMapper(version *APIVersion, schemaID string, mapper Mapper) *Schemas {
+ mappers, ok := s.mappers[version.Path]
+ if !ok {
+ mappers = map[string]Mapper{}
+ s.mappers[version.Path] = mappers
+ }
+
+ if _, ok := mappers[schemaID]; !ok {
+ mappers[schemaID] = mapper
+ }
+
+ return s
+}
+
+func (s *Schemas) SchemasForVersion(version APIVersion) map[string]*Schema {
+ return s.schemasByPath[version.Path]
+}
+
+func (s *Schemas) Versions() []APIVersion {
+ return s.versions
+}
+
+func (s *Schemas) Schemas() []*Schema {
+ return s.schemas
+}
+
+func (s *Schemas) mapper(version *APIVersion, name string) Mapper {
+ var (
+ path string
+ )
+
+ if strings.Contains(name, "/") {
+ idx := strings.LastIndex(name, "/")
+ path = name[0:idx]
+ name = name[idx+1:]
+ } else if version != nil {
+ path = version.Path
+ } else {
+ path = "core"
+ }
+
+ mappers, ok := s.mappers[path]
+ if !ok {
+ return nil
+ }
+
+ mapper := mappers[name]
+ if mapper != nil {
+ return mapper
+ }
+
+ return nil
+}
+
+func (s *Schemas) Schema(version *APIVersion, name string) *Schema {
+ var (
+ path string
+ )
+
+ if strings.Contains(name, "/") {
+ idx := strings.LastIndex(name, "/")
+ path = name[0:idx]
+ name = name[idx+1:]
+ } else if version != nil {
+ path = version.Path
+ } else {
+ path = "core"
+ }
+
+ schemas, ok := s.schemasByPath[path]
+ if !ok {
+ return nil
+ }
+
+ schema := schemas[name]
+ if schema != nil {
+ return schema
+ }
+
+ for _, check := range schemas {
+ if strings.EqualFold(check.ID, name) || strings.EqualFold(check.PluralName, name) {
+ return check
+ }
+ }
+
+ return nil
+}
+
+type multiErrors struct {
+ errors []error
+}
+
+func NewErrors(errors []error) error {
+ if len(errors) == 0 {
+ return nil
+ } else if len(errors) == 1 {
+ return errors[0]
+ }
+ return &multiErrors{
+ errors: errors,
+ }
+}
+
+func (m *multiErrors) Error() string {
+ buf := bytes.NewBuffer(nil)
+ for _, err := range m.errors {
+ if buf.Len() > 0 {
+ buf.WriteString(", ")
+ }
+ buf.WriteString(err.Error())
+ }
+
+ return buf.String()
+}
diff --git a/types/server_types.go b/types/server_types.go
new file mode 100644
index 00000000..c719d965
--- /dev/null
+++ b/types/server_types.go
@@ -0,0 +1,120 @@
+package types
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+type ValuesMap struct {
+ Foo map[string]interface{}
+}
+
+type RawResource struct {
+ ID string `json:"id,omitempty" yaml:"id,omitempty"`
+ Type string `json:"type,omitempty" yaml:"type,omitempty"`
+ Schema *Schema `json:"-" yaml:"-"`
+ Links map[string]string `json:"links" yaml:"links"`
+ Actions map[string]string `json:"actions" yaml:"actions"`
+ Values map[string]interface{} `json:",inline"`
+ ActionLinks bool `json:"-"`
+}
+
+func (r *RawResource) MarshalJSON() ([]byte, error) {
+ data := map[string]interface{}{}
+ for k, v := range r.Values {
+ data[k] = v
+ }
+ if r.ID != "" {
+ data["id"] = r.ID
+ }
+ data["type"] = r.Type
+ data["links"] = r.Links
+ if r.ActionLinks {
+ data["actionLinks"] = r.Actions
+ } else {
+ data["action"] = r.Actions
+ }
+ return json.Marshal(data)
+}
+
+type ActionHandler func(actionName string, action *Action, request *APIContext) error
+
+type RequestHandler func(request *APIContext) error
+
+type Validator func(request *APIContext, data map[string]interface{}) error
+
+type Formatter func(request *APIContext, resource *RawResource)
+
+type ErrorHandler func(request *APIContext, err error)
+
+type ResponseWriter interface {
+ Write(apiContext *APIContext, code int, obj interface{})
+}
+
+type AccessControl interface {
+ CanCreate(schema *Schema) bool
+ CanList(schema *Schema) bool
+}
+
+type APIContext struct {
+ Action string
+ ID string
+ Type string
+ Link string
+ Method string
+ Schema *Schema
+ Schemas *Schemas
+ Version *APIVersion
+ ResponseFormat string
+ ReferenceValidator ReferenceValidator
+ ResponseWriter ResponseWriter
+ QueryOptions *QueryOptions
+ Body map[string]interface{}
+ URLBuilder URLBuilder
+ AccessControl AccessControl
+ SubContext map[string]interface{}
+
+ Request *http.Request
+ Response http.ResponseWriter
+}
+
+func (r *APIContext) WriteResponse(code int, obj interface{}) {
+ r.ResponseWriter.Write(r, code, obj)
+}
+
+var (
+ ASC = SortOrder("asc")
+ DESC = SortOrder("desc")
+)
+
+type QueryOptions struct {
+ Sort Sort
+ Pagination *Pagination
+ Conditions []*QueryCondition
+}
+
+type ReferenceValidator interface {
+ Validate(resourceType, resourceID string) bool
+ Lookup(resourceType, resourceID string) *RawResource
+}
+
+type URLBuilder interface {
+ Current() string
+ Collection(schema *Schema) string
+ ResourceLink(resource *RawResource) string
+ RelativeToRoot(path string) string
+ //Link(resource Resource, name string) string
+ //ReferenceLink(resource Resource) string
+ //ReferenceByIdLink(resourceType string, id string) string
+ Version(version string) string
+ ReverseSort(order SortOrder) string
+ SetSubContext(subContext string)
+}
+
+type Store interface {
+ ByID(apiContext *APIContext, schema *Schema, id string) (map[string]interface{}, error)
+ List(apiContext *APIContext, schema *Schema, opt *QueryOptions) ([]map[string]interface{}, error)
+ Create(apiContext *APIContext, schema *Schema, data map[string]interface{}) (map[string]interface{}, error)
+ Update(apiContext *APIContext, schema *Schema, data map[string]interface{}, id string) (map[string]interface{}, error)
+ Delete(apiContext *APIContext, schema *Schema, id string) error
+}
diff --git a/types/types.go b/types/types.go
new file mode 100644
index 00000000..488bef73
--- /dev/null
+++ b/types/types.go
@@ -0,0 +1,121 @@
+package types
+
+type Collection struct {
+ Type string `json:"type,omitempty"`
+ Links map[string]string `json:"links"`
+ CreateTypes map[string]string `json:"createTypes,omitempty"`
+ Actions map[string]string `json:"actions"`
+ Pagination *Pagination `json:"pagination,omitempty"`
+ Sort *Sort `json:"sort,omitempty"`
+ Filters map[string][]Condition `json:"filters,omitempty"`
+ ResourceType string `json:"resourceType"`
+}
+
+type GenericCollection struct {
+ Collection
+ Data []interface{} `json:"data"`
+}
+
+type ResourceCollection struct {
+ Collection
+ Data []Resource `json:"data,omitempty"`
+}
+
+type SortOrder string
+
+type Sort struct {
+ Name string `json:"name,omitempty"`
+ Order SortOrder `json:"order,omitempty"`
+ Reverse string `json:"reverse,omitempty"`
+ Links map[string]string `json:"sortLinks,omitempty"`
+}
+
+type Condition struct {
+ Modifier string `json:"modifier,omitempty"`
+ Value interface{} `json:"value,omitempty"`
+}
+
+type Pagination struct {
+ Marker string `json:"marker,omitempty"`
+ First string `json:"first,omitempty"`
+ Previous string `json:"previous,omitempty"`
+ Next string `json:"next,omitempty"`
+ Limit *int64 `json:"limit,omitempty"`
+ Total *int64 `json:"total,omitempty"`
+ Partial bool `json:"partial,omitempty"`
+}
+
+type Resource struct {
+ ID string `json:"id,omitempty"`
+ Type string `json:"type,omitempty"`
+ Links map[string]string `json:"links"`
+ Actions map[string]string `json:"actions"`
+}
+
+type APIVersion struct {
+ Group string `json:"group,omitempty"`
+ Version string `json:"version,omitempty"`
+ Path string `json:"path,omitempty"`
+ SubContexts map[string]bool `json:"subContext,omitempty"`
+}
+
+type Schema struct {
+ ID string `json:"id,omitempty"`
+ CodeName string `json:"-"`
+ Type string `json:"type,omitempty"`
+ Links map[string]string `json:"links"`
+ Version APIVersion `json:"version"`
+ PluralName string `json:"pluralName,omitempty"`
+ ResourceMethods []string `json:"resourceMethods,omitempty"`
+ ResourceFields map[string]Field `json:"resourceFields,omitempty"`
+ ResourceActions map[string]Action `json:"resourceActions,omitempty"`
+ CollectionMethods []string `json:"collectionMethods,omitempty"`
+ CollectionFields map[string]Field `json:"collectionFields,omitempty"`
+ CollectionActions map[string]Action `json:"collectionActions,omitempty"`
+ CollectionFilters map[string]Filter `json:"collectionFilters,omitempty"`
+
+ InternalSchema *Schema `json:"-"`
+ Mapper Mapper `json:"-"`
+ ActionHandler ActionHandler `json:"-"`
+ LinkHandler RequestHandler `json:"-"`
+ ListHandler RequestHandler `json:"-"`
+ CreateHandler RequestHandler `json:"-"`
+ DeleteHandler RequestHandler `json:"-"`
+ UpdateHandler RequestHandler `json:"-"`
+ Formatter Formatter `json:"-"`
+ ErrorHandler ErrorHandler `json:"-"`
+ Validator Validator `json:"-"`
+ Store Store `json:"-"`
+}
+
+type Field struct {
+ Type string `json:"type,omitempty"`
+ Default interface{} `json:"default,omitempty"`
+ Nullable bool `json:"nullable,omitempty"`
+ Create bool `json:"create,omitempty"`
+ WriteOnly bool `json:"writeOnly,omitempty"`
+ Required bool `json:"required,omitempty"`
+ Update bool `json:"update,omitempty"`
+ MinLength *int64 `json:"minLength,omitempty"`
+ MaxLength *int64 `json:"maxLength,omitempty"`
+ Min *int64 `json:"min,omitempty"`
+ Max *int64 `json:"max,omitempty"`
+ Options []string `json:"options,omitempty"`
+ ValidChars string `json:"validChars,omitempty"`
+ InvalidChars string `json:"invalidChars,omitempty"`
+ Description string `json:"description,omitempty"`
+ CodeName string `json:"-"`
+}
+
+type Action struct {
+ Input string `json:"input,omitempty"`
+ Output string `json:"output,omitempty"`
+}
+
+type Filter struct {
+ Modifiers []string `json:"modifiers,omitempty"`
+}
+
+type ListOpts struct {
+ Filters map[string]interface{}
+}
diff --git a/urlbuilder/url.go b/urlbuilder/url.go
new file mode 100644
index 00000000..eabfda1d
--- /dev/null
+++ b/urlbuilder/url.go
@@ -0,0 +1,212 @@
+package urlbuilder
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/rancher/norman/name"
+ "github.com/rancher/norman/types"
+)
+
+const (
+ DEFAULT_OVERRIDE_URL_HEADER = "X-API-request-url"
+ FORWARDED_HOST_HEADER = "X-Forwarded-Host"
+ FORWARDED_PROTO_HEADER = "X-Forwarded-Proto"
+ FORWARDED_PORT_HEADER = "X-Forwarded-Port"
+)
+
+func New(r *http.Request, version types.APIVersion, schemas *types.Schemas) (types.URLBuilder, error) {
+ requestUrl := parseRequestUrl(r)
+ responseUrlBase, err := parseResponseUrlBase(requestUrl, r)
+ if err != nil {
+ return nil, err
+ }
+
+ builder := &urlBuilder{
+ schemas: schemas,
+ requestUrl: requestUrl,
+ responseUrlBase: responseUrlBase,
+ apiVersion: version,
+ query: r.URL.Query(),
+ }
+
+ return builder, nil
+}
+
+type urlBuilder struct {
+ schemas *types.Schemas
+ requestUrl string
+ responseUrlBase string
+ apiVersion types.APIVersion
+ subContext string
+ query url.Values
+}
+
+func (u *urlBuilder) SetSubContext(subContext string) {
+ u.subContext = subContext
+}
+
+func (u *urlBuilder) ResourceLink(resource *types.RawResource) string {
+ if resource.ID == "" {
+ return ""
+ }
+
+ return u.constructBasicUrl(resource.Schema.Version, resource.Schema.PluralName, resource.ID)
+}
+
+func (u *urlBuilder) ReverseSort(order types.SortOrder) string {
+ newValues := url.Values{}
+ for k, v := range u.query {
+ newValues[k] = v
+ }
+ newValues.Del("order")
+ newValues.Del("marker")
+ if order == types.ASC {
+ newValues.Add("order", string(types.DESC))
+ } else {
+ newValues.Add("order", string(types.ASC))
+ }
+
+ return u.requestUrl + "?" + newValues.Encode()
+}
+
+func (u *urlBuilder) Current() string {
+ return u.requestUrl
+}
+
+func (u *urlBuilder) RelativeToRoot(path string) string {
+ return u.responseUrlBase + path
+}
+
+func (u *urlBuilder) Collection(schema *types.Schema) string {
+ plural := u.getPluralName(schema)
+ return u.constructBasicUrl(schema.Version, plural)
+}
+
+func (u *urlBuilder) Version(version string) string {
+ return fmt.Sprintf("%s/%s", u.responseUrlBase, version)
+}
+
+func (u *urlBuilder) constructBasicUrl(version types.APIVersion, parts ...string) string {
+ buffer := bytes.Buffer{}
+
+ buffer.WriteString(u.responseUrlBase)
+ if version.Path == "" {
+ buffer.WriteString(u.apiVersion.Path)
+ } else {
+ buffer.WriteString(version.Path)
+ }
+ buffer.WriteString(u.subContext)
+
+ for _, part := range parts {
+ if part == "" {
+ return ""
+ }
+ buffer.WriteString("/")
+ buffer.WriteString(part)
+ }
+
+ return buffer.String()
+}
+
+func (u *urlBuilder) getPluralName(schema *types.Schema) string {
+ if schema.PluralName == "" {
+ return strings.ToLower(name.GuessPluralName(schema.ID))
+ }
+ return strings.ToLower(schema.PluralName)
+}
+
+// Constructs the request URL based off of standard headers in the request, falling back to the HttpServletRequest.getRequestURL()
+// if the headers aren't available. Here is the ordered list of how we'll attempt to construct the URL:
+// - x-api-request-url
+// - x-forwarded-proto://x-forwarded-host:x-forwarded-port/HttpServletRequest.getRequestURI()
+// - x-forwarded-proto://x-forwarded-host/HttpServletRequest.getRequestURI()
+// - x-forwarded-proto://host:x-forwarded-port/HttpServletRequest.getRequestURI()
+// - x-forwarded-proto://host/HttpServletRequest.getRequestURI() request.getRequestURL()
+//
+// Additional notes:
+// - With x-api-request-url, the query string is passed, it will be dropped to match the other formats.
+// - If the x-forwarded-host/host header has a port and x-forwarded-port has been passed, x-forwarded-port will be used.
+func parseRequestUrl(r *http.Request) string {
+ // Get url from custom x-api-request-url header
+ requestUrl := getOverrideHeader(r, DEFAULT_OVERRIDE_URL_HEADER, "")
+ if requestUrl != "" {
+ return strings.SplitN(requestUrl, "?", 2)[0]
+ }
+
+ // Get url from standard headers
+ requestUrl = getUrlFromStandardHeaders(r)
+ if requestUrl != "" {
+ return requestUrl
+ }
+
+ // Use incoming url
+ return fmt.Sprintf("http://%s%s", r.Host, r.URL.Path)
+}
+
+func getUrlFromStandardHeaders(r *http.Request) string {
+ xForwardedProto := getOverrideHeader(r, FORWARDED_PROTO_HEADER, "")
+ if xForwardedProto == "" {
+ return ""
+ }
+
+ host := getOverrideHeader(r, FORWARDED_HOST_HEADER, "")
+ if host == "" {
+ host = r.Host
+ }
+
+ if host == "" {
+ return ""
+ }
+
+ port := getOverrideHeader(r, FORWARDED_PORT_HEADER, "")
+ if port == "443" || port == "80" {
+ port = "" // Don't include default ports in url
+ }
+
+ if port != "" && strings.Contains(host, ":") {
+ // Have to strip the port that is in the host. Handle IPv6, which has this format: [::1]:8080
+ if (strings.HasPrefix(host, "[") && strings.Contains(host, "]:")) || !strings.HasPrefix(host, "[") {
+ host = host[0:strings.LastIndex(host, ":")]
+ }
+ }
+
+ if port != "" {
+ port = ":" + port
+ }
+
+ return fmt.Sprintf("%s://%s%s%s", xForwardedProto, host, port, r.URL.Path)
+}
+
+func getOverrideHeader(r *http.Request, header string, defaultValue string) string {
+ // Need to handle comma separated hosts in X-Forwarded-For
+ value := r.Header.Get(header)
+ if value != "" {
+ return strings.TrimSpace(strings.Split(value, ",")[0])
+ }
+ return defaultValue
+}
+
+func parseResponseUrlBase(requestUrl string, r *http.Request) (string, error) {
+ path := r.URL.Path
+
+ index := strings.LastIndex(requestUrl, path)
+ if index == -1 {
+ // Fallback, if we can't find path in requestUrl, then we just assume the base is the root of the web request
+ u, err := url.Parse(requestUrl)
+ if err != nil {
+ return "", err
+ }
+
+ buffer := bytes.Buffer{}
+ buffer.WriteString(u.Scheme)
+ buffer.WriteString("://")
+ buffer.WriteString(u.Host)
+ return buffer.String(), nil
+ } else {
+ return requestUrl[0:index], nil
+ }
+}