1
0
mirror of https://github.com/rancher/norman.git synced 2025-09-01 15:18:20 +00:00
This commit is contained in:
Darren Shepherd
2017-12-05 09:21:12 -07:00
parent 5ec6a8d719
commit 18d3f69aa8
17 changed files with 362 additions and 88 deletions

View File

@@ -112,7 +112,7 @@ func (a *APIRootStore) List(apiContext *types.APIContext, schema *types.Schema,
func apiVersionToAPIRootMap(version types.APIVersion) map[string]interface{} {
return map[string]interface{}{
"type": "/v1-meta/schemas/apiRoot",
"type": "/meta/schemas/apiRoot",
"apiVersion": map[string]interface{}{
"version": version.Version,
"group": version.Group,

View File

@@ -11,7 +11,7 @@ var (
Version = types.APIVersion{
Group: "meta.cattle.io",
Version: "v1",
Path: "/v1-meta",
Path: "/meta",
}
Schema = types.Schema{

View File

@@ -29,6 +29,7 @@ type Server struct {
schemas *types.Schemas
QueryFilter types.QueryFilter
StoreWrapper StoreWrapper
URLParser parse.URLParser
Defaults Defaults
}
@@ -63,6 +64,7 @@ func NewAPIServer() *Server {
ErrorHandler: httperror.ErrorHandler,
},
StoreWrapper: wrapper.Wrap,
URLParser: parse.DefaultURLParser,
QueryFilter: handler.QueryFilter,
}
@@ -71,7 +73,7 @@ func NewAPIServer() *Server {
}
func (s *Server) parser(rw http.ResponseWriter, req *http.Request) (*types.APIContext, error) {
ctx, err := parse.Parse(rw, req, s.schemas, s.Resolver)
ctx, err := parse.Parse(rw, req, s.schemas, s.URLParser, s.Resolver)
ctx.ResponseWriter = s.ResponseWriters[ctx.ResponseFormat]
if ctx.ResponseWriter == nil {
ctx.ResponseWriter = s.ResponseWriters["json"]

View File

@@ -122,7 +122,14 @@ func (j *JSONResponseWriter) convert(b *builder.Builder, context *types.APIConte
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)
self := context.URLBuilder.ResourceLink(rawResource)
rawResource.Links["self"] = self
if schema.CanUpdate() {
rawResource.Links["update"] = self
}
if schema.CanDelete() {
rawResource.Links["remove"] = self
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -47,6 +48,15 @@ func (p *ObjectClient) Create(o runtime.Object) (runtime.Object, error) {
if obj, ok := o.(metav1.Object); ok && obj.GetNamespace() != "" {
ns = obj.GetNamespace()
}
if t, err := meta.TypeAccessor(o); err == nil {
if t.GetKind() == "" {
t.SetKind(p.gvk.Kind)
}
if t.GetAPIVersion() == "" {
apiVersion, _ := p.gvk.ToAPIVersionAndKind()
t.SetAPIVersion(apiVersion)
}
}
result := p.Factory.Object()
err := p.restClient.Post().
Prefix(p.getAPIPrefix(), p.gvk.Group, p.gvk.Version).

View File

@@ -135,6 +135,36 @@ func generateType(outputDir string, schema *types.Schema, schemas *types.Schemas
})
}
func generateLifecycle(external bool, outputDir string, schema *types.Schema, schemas *types.Schemas) error {
filePath := strings.ToLower("zz_generated_" + addUnderscore(schema.ID) + "_lifecycle_adapter.go")
output, err := os.Create(path.Join(outputDir, filePath))
if err != nil {
return err
}
defer output.Close()
typeTemplate, err := template.New("lifecycle.template").
Funcs(funcs()).
Parse(strings.Replace(lifecycleTemplate, "%BACK%", "`", -1))
if err != nil {
return err
}
importPackage := ""
prefix := ""
if external {
parts := strings.Split(schema.PkgName, "/vendor/")
importPackage = fmt.Sprintf("\"%s\"", parts[len(parts)-1])
prefix = schema.Version.Version + "."
}
return typeTemplate.Execute(output, map[string]interface{}{
"schema": schema,
"importPackage": importPackage,
"prefix": prefix,
})
}
func generateController(external bool, 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))
@@ -226,6 +256,10 @@ func GenerateControllerForTypes(version *types.APIVersion, k8sOutputPackage stri
if err := generateController(true, k8sDir, schema, schemas); err != nil {
return err
}
if err := generateLifecycle(true, k8sDir, schema, schemas); err != nil {
return err
}
}
if err := deepCopyGen(baseDir, k8sOutputPackage); err != nil {
@@ -267,6 +301,9 @@ func Generate(schemas *types.Schemas, cattleOutputPackage, k8sOutputPackage stri
if err := generateController(false, k8sDir, schema, schemas); err != nil {
return err
}
if err := generateLifecycle(false, k8sDir, schema, schemas); err != nil {
return err
}
}
generated = append(generated, schema)

View File

@@ -0,0 +1,43 @@
package generator
var lifecycleTemplate = `package {{.schema.Version.Version}}
import (
{{.importPackage}}
"k8s.io/apimachinery/pkg/runtime"
"github.com/rancher/norman/lifecycle"
)
type {{.schema.CodeName}}Lifecycle interface {
Initialize(obj *{{.prefix}}{{.schema.CodeName}}) error
Remove(obj *{{.prefix}}{{.schema.CodeName}}) error
Updated(obj *{{.prefix}}{{.schema.CodeName}}) error
}
type {{.schema.ID}}LifecycleAdapter struct {
lifecycle {{.schema.CodeName}}Lifecycle
}
func (w *{{.schema.ID}}LifecycleAdapter) Initialize(obj runtime.Object) error {
return w.lifecycle.Initialize(obj.(*{{.prefix}}{{.schema.CodeName}}))
}
func (w *{{.schema.ID}}LifecycleAdapter) Finalize(obj runtime.Object) error {
return w.lifecycle.Remove(obj.(*{{.prefix}}{{.schema.CodeName}}))
}
func (w *{{.schema.ID}}LifecycleAdapter) Updated(obj runtime.Object) error {
return w.lifecycle.Updated(obj.(*{{.prefix}}{{.schema.CodeName}}))
}
func New{{.schema.CodeName}}LifecycleAdapter(name string, client {{.schema.CodeName}}Interface, l {{.schema.CodeName}}Lifecycle) {{.schema.CodeName}}HandlerFunc {
adapter := &{{.schema.ID}}LifecycleAdapter{lifecycle: l}
syncFn := lifecycle.NewObjectLifecycleAdapter(name, adapter, client.ObjectClient())
return func(key string, obj *{{.prefix}}{{.schema.CodeName}}) error {
if obj == nil {
return syncFn(key, nil)
}
return syncFn(key, obj)
}
}
`

View File

@@ -23,7 +23,7 @@ func ErrorHandler(request *types.APIContext, err error) {
func toError(apiError *APIError) map[string]interface{} {
e := map[string]interface{}{
"type": "/v1-meta/schemas/error",
"type": "/meta/schemas/error",
"code": apiError.code.code,
"message": apiError.message,
}

119
lifecycle/object.go Normal file
View File

@@ -0,0 +1,119 @@
package lifecycle
import (
"github.com/rancher/norman/clientbase"
"github.com/rancher/norman/types/slice"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
var (
initialized = "io.cattle.lifecycle.initialized"
)
type ObjectLifecycle interface {
Initialize(obj runtime.Object) error
Finalize(obj runtime.Object) error
Updated(obj runtime.Object) error
}
type objectLifecycleAdapter struct {
name string
lifecycle ObjectLifecycle
objectClient *clientbase.ObjectClient
}
func NewObjectLifecycleAdapter(name string, lifecycle ObjectLifecycle, objectClient *clientbase.ObjectClient) func(key string, obj runtime.Object) error {
o := objectLifecycleAdapter{
name: name,
lifecycle: lifecycle,
objectClient: objectClient,
}
return o.sync
}
func (o *objectLifecycleAdapter) sync(key string, obj runtime.Object) error {
if obj == nil {
return nil
}
metadata, err := meta.Accessor(obj)
if err != nil {
return err
}
if cont, err := o.finalize(metadata, obj); err != nil || !cont {
return err
}
if cont, err := o.initialize(metadata, obj); err != nil || !cont {
return err
}
return o.lifecycle.Updated(obj.DeepCopyObject())
}
func (o *objectLifecycleAdapter) finalize(metadata metav1.Object, obj runtime.Object) (bool, error) {
// Check finalize
if metadata.GetDeletionTimestamp() == nil {
return true, nil
}
if !slice.ContainsString(metadata.GetFinalizers(), o.name) {
return false, nil
}
obj = obj.DeepCopyObject()
metadata, err := meta.Accessor(obj)
if err != nil {
return false, err
}
var finalizers []string
for _, finalizer := range metadata.GetFinalizers() {
if finalizer == o.name {
continue
}
finalizers = append(finalizers, finalizer)
}
metadata.SetFinalizers(finalizers)
if err := o.lifecycle.Finalize(obj); err != nil {
return false, err
}
_, err = o.objectClient.Update(metadata.GetName(), obj)
return false, err
}
func (o *objectLifecycleAdapter) initializeKey() string {
return initialized + "." + o.name
}
func (o *objectLifecycleAdapter) initialize(metadata metav1.Object, obj runtime.Object) (bool, error) {
initialized := o.initializeKey()
if metadata.GetLabels()[initialized] == "true" {
return true, nil
}
obj = obj.DeepCopyObject()
metadata, err := meta.Accessor(obj)
if err != nil {
return false, err
}
if metadata.GetLabels() == nil {
metadata.SetLabels(map[string]string{})
}
metadata.SetFinalizers(append(metadata.GetFinalizers(), o.name))
metadata.GetLabels()[initialized] = "true"
if err := o.lifecycle.Initialize(obj); err != nil {
return false, err
}
_, err = o.objectClient.Update(metadata.GetName(), obj)
return false, err
}

View File

@@ -83,8 +83,12 @@ func (b *Builder) copyInputs(schema *types.Schema, input map[string]interface{},
}
if op == List {
result["type"] = input["type"]
result["id"] = input["id"]
if !convert.IsEmpty(input["type"]) {
result["type"] = input["type"]
}
if !convert.IsEmpty(input["id"]) {
result["id"] = input["id"]
}
}
return nil

View File

@@ -2,11 +2,11 @@ package parse
import (
"net/http"
"net/url"
"regexp"
"strings"
"github.com/rancher/norman/api/builtin"
"github.com/rancher/norman/httperror"
"github.com/rancher/norman/types"
"github.com/rancher/norman/urlbuilder"
)
@@ -23,20 +23,83 @@ var (
}
)
type ParsedURL struct {
Version string
Type string
ID string
Link string
Method string
Action string
SubContext map[string]string
SubContextPrefix string
}
type ResolverFunc func(typeName string, context *types.APIContext) error
func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, resolverFunc ResolverFunc) (*types.APIContext, error) {
type URLParser func(schema *types.Schemas, url *url.URL) (ParsedURL, error)
func DefaultURLParser(schemas *types.Schemas, url *url.URL) (ParsedURL, error) {
result := ParsedURL{}
version := Version(schemas, url.Path)
if version == nil {
return result, nil
}
path := url.Path
path = multiSlashRegexp.ReplaceAllString(path, "/")
parts := strings.SplitN(path[len(version.Path):], "/", 4)
prefix, parts, subContext := parseSubContext(version, parts)
result.Version = version.Path
result.SubContext = subContext
result.SubContextPrefix = prefix
result.Action, result.Method = parseAction(url)
result.Type = safeIndex(parts, 1)
result.ID = safeIndex(parts, 2)
result.Link = safeIndex(parts, 3)
return result, nil
}
func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, urlParser URLParser, resolverFunc ResolverFunc) (*types.APIContext, error) {
var err error
result := &types.APIContext{
Request: req,
Response: rw,
Schemas: schemas,
Request: req,
Response: rw,
Method: parseMethod(req),
ResponseFormat: parseResponseFormat(req),
}
result.URLBuilder, _ = urlbuilder.New(req, types.APIVersion{}, schemas)
// The response format is guarenteed to be set even in the event of an error
result.ResponseFormat = parseResponseFormat(req)
result.Version = parseVersion(schemas, req.URL.Path)
result.Schemas = schemas
parsedURL, err := urlParser(schemas, req.URL)
// wait to check error, want to set as much as possible
result.SubContext = parsedURL.SubContext
result.Type = parsedURL.Type
result.ID = parsedURL.ID
result.Link = parsedURL.Link
result.Action = parsedURL.Action
if parsedURL.Method != "" {
result.Method = parsedURL.Method
}
for i, version := range schemas.Versions() {
if version.Path == parsedURL.Version {
result.Version = &schemas.Versions()[i]
break
}
}
if err != nil {
return result, err
}
if result.Version == nil {
result.Method = http.MethodGet
@@ -46,15 +109,16 @@ func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, re
return result, nil
}
result.Method = parseMethod(req)
result.Action, result.Method = parseAction(req, result.Method)
result.URLBuilder, err = urlbuilder.New(req, *result.Version, result.Schemas)
if err != nil {
return result, err
}
if err := parsePath(result, req, resolverFunc); err != nil {
if parsedURL.SubContextPrefix != "" {
result.URLBuilder.SetSubContext(parsedURL.SubContextPrefix)
}
if err := resolverFunc(result.Type, result); err != nil {
return result, err
}
@@ -66,6 +130,8 @@ func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, re
return result, nil
}
result.Type = result.Schema.ID
if err := ValidateMethod(result); err != nil {
return result, err
}
@@ -73,33 +139,24 @@ func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, re
return result, nil
}
func parseSubContext(parts []string, apiRequest *types.APIContext) []string {
func parseSubContext(version *types.APIVersion, parts []string) (string, []string, map[string]string) {
subContext := ""
apiRequest.SubContext = map[string]string{}
apiRequest.Attributes = map[string]interface{}{}
result := map[string]string{}
for len(parts) > 3 && apiRequest.Version != nil && parts[3] != "" {
for len(parts) > 3 && version != nil && parts[3] != "" {
resourceType := parts[1]
resourceID := parts[2]
if !apiRequest.Version.SubContexts[resourceType] {
if !version.SubContexts[resourceType] {
break
}
if apiRequest.ReferenceValidator != nil && !apiRequest.ReferenceValidator.Validate(resourceType, resourceID) {
return parts
}
apiRequest.SubContext[resourceType] = resourceID
result[resourceType] = resourceID
subContext = subContext + "/" + resourceType + "/" + resourceID
parts = append(parts[:1], parts[3:]...)
}
if subContext != "" {
apiRequest.URLBuilder.SetSubContext(subContext)
}
return parts
return subContext, parts, result
}
func DefaultResolver(typeName string, apiContext *types.APIContext) error {
@@ -120,50 +177,6 @@ func DefaultResolver(typeName string, apiContext *types.APIContext) error {
return nil
}
func parsePath(apiRequest *types.APIContext, request *http.Request, resolverFunc ResolverFunc) error {
if apiRequest.Version == nil {
return nil
}
path := request.URL.Path
path = multiSlashRegexp.ReplaceAllString(path, "/")
versionPrefix := apiRequest.Version.Path
if !strings.HasPrefix(path, versionPrefix) {
return nil
}
parts := strings.Split(path[len(versionPrefix):], "/")
parts = parseSubContext(parts, apiRequest)
if len(parts) > 4 {
return httperror.NewAPIError(httperror.NotFound, "No handler for path")
}
typeName := safeIndex(parts, 1)
id := safeIndex(parts, 2)
link := safeIndex(parts, 3)
if err := resolverFunc(typeName, apiRequest); err != nil {
return err
}
if apiRequest.Schema == nil {
return nil
}
apiRequest.Type = apiRequest.Schema.ID
if id == "" {
return nil
}
apiRequest.ID = id
apiRequest.Link = link
return nil
}
func safeIndex(slice []string, index int) string {
if index >= len(slice) {
return ""
@@ -198,20 +211,16 @@ func parseMethod(req *http.Request) string {
return method
}
func parseAction(req *http.Request, method string) (string, string) {
if req.Method != http.MethodPost {
return "", method
}
action := req.URL.Query().Get("action")
func parseAction(url *url.URL) (string, string) {
action := url.Query().Get("action")
if action == "remove" {
return "", http.MethodDelete
}
return action, method
return action, ""
}
func parseVersion(schemas *types.Schemas, path string) *types.APIVersion {
func Version(schemas *types.Schemas, path string) *types.APIVersion {
path = multiSlashRegexp.ReplaceAllString(path, "/")
for _, version := range schemas.Versions() {
if version.Path == "" {

View File

@@ -15,7 +15,7 @@ func NewMetadataMapper() types.Mapper {
Move{From: "deletionTimestamp", To: "removed"},
Drop{"deletionGracePeriodSeconds"},
Drop{"initializers"},
Drop{"finalizers"},
//Drop{"finalizers"},
Drop{"clusterName"},
ReadOnly{Field: "*"},
Access{

View File

@@ -24,3 +24,11 @@ func (v *APIVersion) Equals(other *APIVersion) bool {
func (s *Schema) CanList() bool {
return slice.ContainsString(s.CollectionMethods, http.MethodGet)
}
func (s *Schema) CanUpdate() bool {
return slice.ContainsString(s.ResourceMethods, http.MethodPut)
}
func (s *Schema) CanDelete() bool {
return slice.ContainsString(s.ResourceMethods, http.MethodDelete)
}

View File

@@ -60,7 +60,7 @@ func (s *Schemas) AddSchemas(schema *Schemas) *Schemas {
}
func (s *Schemas) AddSchema(schema *Schema) *Schemas {
schema.Type = "/v1-meta/schemas/schema"
schema.Type = "/meta/schemas/schema"
if schema.ID == "" {
s.errors = append(s.errors, fmt.Errorf("ID is not set on schema: %v", schema))
return s

View File

@@ -83,7 +83,7 @@ type APIContext struct {
URLBuilder URLBuilder
AccessControl AccessControl
SubContext map[string]string
Attributes map[string]interface{}
//Attributes map[string]interface{}
Request *http.Request
Response http.ResponseWriter
@@ -140,6 +140,7 @@ type URLBuilder interface {
SubContextCollection(subContext *Schema, contextName string, schema *Schema) string
SchemaLink(schema *Schema) string
ResourceLink(resource *RawResource) string
Link(linkName string, resource *RawResource) string
RelativeToRoot(path string) string
Version(version APIVersion) string
Marker(marker string) string

View File

@@ -1,5 +1,7 @@
package values
import "github.com/rancher/norman/types/convert"
func RemoveValue(data map[string]interface{}, keys ...string) (interface{}, bool) {
for i, key := range keys {
if i == len(keys)-1 {
@@ -13,6 +15,30 @@ func RemoveValue(data map[string]interface{}, keys ...string) (interface{}, bool
return nil, false
}
func GetStringSlice(data map[string]interface{}, keys ...string) ([]string, bool) {
val, ok := GetValue(data, keys...)
if !ok {
return nil, ok
}
slice, typeOk := val.([]string)
if typeOk {
return slice, typeOk
}
sliceNext, typeOk := val.([]interface{})
if !typeOk {
return nil, typeOk
}
var result []string
for _, item := range sliceNext {
result = append(result, convert.ToString(item))
}
return result, true
}
func GetSlice(data map[string]interface{}, keys ...string) ([]map[string]interface{}, bool) {
val, ok := GetValue(data, keys...)
if !ok {

View File

@@ -53,6 +53,14 @@ func (u *urlBuilder) SchemaLink(schema *types.Schema) string {
return u.constructBasicURL(schema.Version, "schemas", schema.ID)
}
func (u *urlBuilder) Link(linkName string, resource *types.RawResource) string {
if resource.ID == "" || linkName == "" {
return ""
}
return u.constructBasicURL(resource.Schema.Version, resource.Schema.PluralName, resource.ID, strings.ToLower(linkName))
}
func (u *urlBuilder) ResourceLink(resource *types.RawResource) string {
if resource.ID == "" {
return ""