From 18d3f69aa84ed39326e731ebfa509cf104bf9ad1 Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 5 Dec 2017 09:21:12 -0700 Subject: [PATCH] Updates --- api/builtin/api_root.go | 2 +- api/builtin/schema.go | 2 +- api/server.go | 4 +- api/writer/json.go | 9 +- clientbase/object_client.go | 10 ++ generator/generator.go | 37 +++++++ generator/lifecycle_template.go | 43 +++++++++ httperror/handler.go | 2 +- lifecycle/object.go | 119 +++++++++++++++++++++++ parse/builder/builder.go | 8 +- parse/parse.go | 165 +++++++++++++++++--------------- types/mapper/metadata.go | 2 +- types/schema_funcs.go | 8 ++ types/schemas.go | 2 +- types/server_types.go | 3 +- types/values/values.go | 26 +++++ urlbuilder/url.go | 8 ++ 17 files changed, 362 insertions(+), 88 deletions(-) create mode 100644 generator/lifecycle_template.go create mode 100644 lifecycle/object.go diff --git a/api/builtin/api_root.go b/api/builtin/api_root.go index 7ff1b366..eae13fb1 100644 --- a/api/builtin/api_root.go +++ b/api/builtin/api_root.go @@ -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, diff --git a/api/builtin/schema.go b/api/builtin/schema.go index c49f9f29..379c74ab 100644 --- a/api/builtin/schema.go +++ b/api/builtin/schema.go @@ -11,7 +11,7 @@ var ( Version = types.APIVersion{ Group: "meta.cattle.io", Version: "v1", - Path: "/v1-meta", + Path: "/meta", } Schema = types.Schema{ diff --git a/api/server.go b/api/server.go index 130e97fb..4f9b2794 100644 --- a/api/server.go +++ b/api/server.go @@ -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"] diff --git a/api/writer/json.go b/api/writer/json.go index 6ad1b517..156113b8 100644 --- a/api/writer/json.go +++ b/api/writer/json.go @@ -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 + } } } diff --git a/clientbase/object_client.go b/clientbase/object_client.go index edef0dc1..ceb9333a 100644 --- a/clientbase/object_client.go +++ b/clientbase/object_client.go @@ -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). diff --git a/generator/generator.go b/generator/generator.go index dd9ecb1c..4ed33b26 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -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) diff --git a/generator/lifecycle_template.go b/generator/lifecycle_template.go new file mode 100644 index 00000000..0be8bc8e --- /dev/null +++ b/generator/lifecycle_template.go @@ -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) + } +} +` diff --git a/httperror/handler.go b/httperror/handler.go index b74aab17..e5ad766f 100644 --- a/httperror/handler.go +++ b/httperror/handler.go @@ -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, } diff --git a/lifecycle/object.go b/lifecycle/object.go new file mode 100644 index 00000000..ca973017 --- /dev/null +++ b/lifecycle/object.go @@ -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 +} diff --git a/parse/builder/builder.go b/parse/builder/builder.go index 762b7591..0bcfaec2 100644 --- a/parse/builder/builder.go +++ b/parse/builder/builder.go @@ -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 diff --git a/parse/parse.go b/parse/parse.go index 0f9936b7..a2b2514e 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -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 == "" { diff --git a/types/mapper/metadata.go b/types/mapper/metadata.go index ddbfa5f8..2e06845d 100644 --- a/types/mapper/metadata.go +++ b/types/mapper/metadata.go @@ -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{ diff --git a/types/schema_funcs.go b/types/schema_funcs.go index df6f6ff6..b4543df1 100644 --- a/types/schema_funcs.go +++ b/types/schema_funcs.go @@ -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) +} diff --git a/types/schemas.go b/types/schemas.go index 0732d31b..43613cac 100644 --- a/types/schemas.go +++ b/types/schemas.go @@ -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 diff --git a/types/server_types.go b/types/server_types.go index d00f8295..4d4c3870 100644 --- a/types/server_types.go +++ b/types/server_types.go @@ -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 diff --git a/types/values/values.go b/types/values/values.go index 634957b0..0b325435 100644 --- a/types/values/values.go +++ b/types/values/values.go @@ -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 { diff --git a/urlbuilder/url.go b/urlbuilder/url.go index ea33ead5..bda28045 100644 --- a/urlbuilder/url.go +++ b/urlbuilder/url.go @@ -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 ""