From d2590b3d6f680f9c9e546d03d90d4df3c76198e7 Mon Sep 17 00:00:00 2001 From: Kubernetes Publisher Date: Fri, 22 Sep 2017 12:06:25 +0000 Subject: [PATCH] code-generator: allow to customize generated verbs and add custom verb Kubernetes-commit: b1a3235fd46042a17d4ee864e0ada5aeea76ed1b --- .../fake/generator_fake_for_type.go | 177 ++++++++++-- .../generators/generator_for_type.go | 258 ++++++++++++++++-- cmd/client-gen/generators/util/tags.go | 192 ++++++++++++- cmd/client-gen/generators/util/tags_test.go | 64 +++++ 4 files changed, 639 insertions(+), 52 deletions(-) diff --git a/cmd/client-gen/generators/fake/generator_fake_for_type.go b/cmd/client-gen/generators/fake/generator_fake_for_type.go index d606434d..f3deb368 100644 --- a/cmd/client-gen/generators/fake/generator_fake_for_type.go +++ b/cmd/client-gen/generators/fake/generator_fake_for_type.go @@ -19,6 +19,7 @@ package fake import ( "io" "path/filepath" + "strings" "k8s.io/gengo/generator" "k8s.io/gengo/namer" @@ -108,6 +109,9 @@ func (g *genFakeForType) GenerateType(c *generator.Context, t *types.Type, w io. const pkgClientGoTesting = "k8s.io/client-go/testing" m := map[string]interface{}{ "type": t, + "inputType": t, + "resultType": t, + "subresourcePath": "", "package": pkg, "Package": namer.IC(pkg), "namespaced": !tags.NonNamespaced, @@ -139,7 +143,10 @@ func (g *genFakeForType) GenerateType(c *generator.Context, t *types.Type, w io. "NewCreateAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewCreateAction"}), "NewRootWatchAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewRootWatchAction"}), "NewWatchAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewWatchAction"}), + "NewCreateSubresourceAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewCreateSubresourceAction"}), "NewUpdateSubresourceAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewUpdateSubresourceAction"}), + "NewGetSubresourceAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewGetSubresourceAction"}), + "NewListSubresourceAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewListSubresourceAction"}), "NewRootUpdateSubresourceAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewRootUpdateSubresourceAction"}), "NewRootPatchAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewRootPatchAction"}), "NewPatchAction": c.Universe.Function(types.Name{Package: pkgClientGoTesting, Name: "NewPatchAction"}), @@ -193,9 +200,88 @@ func (g *genFakeForType) GenerateType(c *generator.Context, t *types.Type, w io. sw.Do(patchTemplate, m) } + // generate extended client methods + for _, e := range tags.Extensions { + inputType := *t + resultType := *t + if len(e.InputTypeOverride) > 0 { + if name, pkg := e.Input(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + inputType = *newType + } else { + inputType.Name.Name = e.InputTypeOverride + } + } + if len(e.ResultTypeOverride) > 0 { + if name, pkg := e.Result(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + resultType = *newType + } else { + resultType.Name.Name = e.ResultTypeOverride + } + } + m["inputType"] = &inputType + m["resultType"] = &resultType + m["subresourcePath"] = e.SubResourcePath + + if e.HasVerb("get") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, getSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, getTemplate), m) + } + } + + if e.HasVerb("list") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, listSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, listTemplate), m) + } + } + + // TODO: Figure out schemantic for watching a sub-resource. + if e.HasVerb("watch") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, watchTemplate), m) + } + + if e.HasVerb("create") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, createSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, createTemplate), m) + } + } + + if e.HasVerb("update") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, updateSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, updateTemplate), m) + } + } + + // TODO: Figure out schemantic for deleting a sub-resource (what arguments + // are passed, does it need two names? etc. + if e.HasVerb("delete") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, deleteTemplate), m) + } + + if e.HasVerb("patch") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, patchTemplate), m) + } + } + return sw.Error() } +// adjustTemplate adjust the origin verb template using the expansion name. +// TODO: Make the verbs in templates parametrized so the strings.Replace() is +// not needed. +func adjustTemplate(name, verbType, template string) string { + return strings.Replace(template, " "+strings.Title(verbType), " "+name, -1) +} + // template for the struct that implements the type's interface var structNamespaced = ` // Fake$.type|publicPlural$ implements $.type|public$Interface @@ -234,6 +320,19 @@ func (c *Fake$.type|publicPlural$) List(opts $.ListOptions|raw$) (result *$.type } ` +var listSubresourceTemplate = ` +// List takes label and field selectors, and returns the list of $.resultType|publicPlural$ that match those selectors. +func (c *Fake$.type|publicPlural$) List($.type|private$Name string, opts $.ListOptions|raw$) (result *$.resultType|raw$List, err error) { + obj, err := c.Fake. + $if .namespaced$Invokes($.NewListSubresourceAction|raw$($.type|allLowercasePlural$Resource, $.type|private$Name, "$.subresourcePath$", $.type|allLowercasePlural$Kind, c.ns, opts), &$.resultType|raw$List{}) + $else$Invokes($.NewRootListAction|raw$($.type|allLowercasePlural$Resource, $.type|allLowercasePlural$Kind, opts), &$.resultType|raw$List{})$end$ + if obj == nil { + return nil, err + } + return obj.(*$.resultType|raw$List), err +} +` + var listUsingOptionsTemplate = ` // List takes label and field selectors, and returns the list of $.type|publicPlural$ that match those selectors. func (c *Fake$.type|publicPlural$) List(opts $.ListOptions|raw$) (result *$.type|raw$List, err error) { @@ -259,15 +358,28 @@ func (c *Fake$.type|publicPlural$) List(opts $.ListOptions|raw$) (result *$.type ` var getTemplate = ` -// Get takes name of the $.type|private$, and returns the corresponding $.type|private$ object, and an error if there is any. -func (c *Fake$.type|publicPlural$) Get(name string, options $.GetOptions|raw$) (result *$.type|raw$, err error) { +// Get takes name of the $.type|private$, and returns the corresponding $.resultType|private$ object, and an error if there is any. +func (c *Fake$.type|publicPlural$) Get(name string, options $.GetOptions|raw$) (result *$.resultType|raw$, err error) { obj, err := c.Fake. - $if .namespaced$Invokes($.NewGetAction|raw$($.type|allLowercasePlural$Resource, c.ns, name), &$.type|raw${}) - $else$Invokes($.NewRootGetAction|raw$($.type|allLowercasePlural$Resource, name), &$.type|raw${})$end$ + $if .namespaced$Invokes($.NewGetAction|raw$($.type|allLowercasePlural$Resource, c.ns, name), &$.resultType|raw${}) + $else$Invokes($.NewRootGetAction|raw$($.type|allLowercasePlural$Resource, name), &$.resultType|raw${})$end$ if obj == nil { return nil, err } - return obj.(*$.type|raw$), err + return obj.(*$.resultType|raw$), err +} +` + +var getSubresourceTemplate = ` +// Get takes name of the $.type|private$, and returns the corresponding $.resultType|private$ object, and an error if there is any. +func (c *Fake$.type|publicPlural$) Get($.type|private$Name string, options $.GetOptions|raw$) (result *$.resultType|raw$, err error) { + obj, err := c.Fake. + $if .namespaced$Invokes($.NewGetSubresourceAction|raw$($.type|allLowercasePlural$Resource, c.ns, "$.subresourcePath$", $.type|private$Name), &$.resultType|raw${}) + $else$Invokes($.NewRootGetAction|raw$($.type|allLowercasePlural$Resource, $.type|private$Name), &$.resultType|raw${})$end$ + if obj == nil { + return nil, err + } + return obj.(*$.resultType|raw$), err } ` @@ -291,30 +403,55 @@ func (c *Fake$.type|publicPlural$) DeleteCollection(options *$.DeleteOptions|raw return err } ` - var createTemplate = ` -// Create takes the representation of a $.type|private$ and creates it. Returns the server's representation of the $.type|private$, and an error, if there is any. -func (c *Fake$.type|publicPlural$) Create($.type|private$ *$.type|raw$) (result *$.type|raw$, err error) { +// Create takes the representation of a $.inputType|private$ and creates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *Fake$.type|publicPlural$) Create($.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { obj, err := c.Fake. - $if .namespaced$Invokes($.NewCreateAction|raw$($.type|allLowercasePlural$Resource, c.ns, $.type|private$), &$.type|raw${}) - $else$Invokes($.NewRootCreateAction|raw$($.type|allLowercasePlural$Resource, $.type|private$), &$.type|raw${})$end$ + $if .namespaced$Invokes($.NewCreateAction|raw$($.inputType|allLowercasePlural$Resource, c.ns, $.inputType|private$), &$.resultType|raw${}) + $else$Invokes($.NewRootCreateAction|raw$($.inputType|allLowercasePlural$Resource, $.inputType|private$), &$.resultType|raw${})$end$ if obj == nil { return nil, err } - return obj.(*$.type|raw$), err + return obj.(*$.resultType|raw$), err +} +` + +var createSubresourceTemplate = ` +// Create takes the representation of a $.inputType|private$ and creates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *Fake$.type|publicPlural$) Create($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + obj, err := c.Fake. + $if .namespaced$Invokes($.NewCreateSubresourceAction|raw$($.type|allLowercasePlural$Resource, $.type|private$Name, "$.subresourcePath$", c.ns, $.inputType|private$), &$.resultType|raw${}) + $else$Invokes($.NewRootCreateAction|raw$($.inputType|allLowercasePlural$Resource, $.inputType|private$), &$.resultType|raw${})$end$ + if obj == nil { + return nil, err + } + return obj.(*$.resultType|raw$), err } ` var updateTemplate = ` -// Update takes the representation of a $.type|private$ and updates it. Returns the server's representation of the $.type|private$, and an error, if there is any. -func (c *Fake$.type|publicPlural$) Update($.type|private$ *$.type|raw$) (result *$.type|raw$, err error) { +// Update takes the representation of a $.inputType|private$ and updates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *Fake$.type|publicPlural$) Update($.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { obj, err := c.Fake. - $if .namespaced$Invokes($.NewUpdateAction|raw$($.type|allLowercasePlural$Resource, c.ns, $.type|private$), &$.type|raw${}) + $if .namespaced$Invokes($.NewUpdateAction|raw$($.inputType|allLowercasePlural$Resource, c.ns, $.inputType|private$), &$.resultType|raw${}) + $else$Invokes($.NewRootUpdateAction|raw$($.inputType|allLowercasePlural$Resource, $.inputType|private$), &$.resultType|raw${})$end$ + if obj == nil { + return nil, err + } + return obj.(*$.resultType|raw$), err +} +` + +var updateSubresourceTemplate = ` +// Update takes the representation of a $.inputType|private$ and updates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *Fake$.type|publicPlural$) Update($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + obj, err := c.Fake. + $if .namespaced$Invokes($.NewUpdateSubresourceAction|raw$($.type|allLowercasePlural$Resource, "$.subresourcePath$", c.ns, $.inputType|private$), &$.inputType|raw${}) $else$Invokes($.NewRootUpdateAction|raw$($.type|allLowercasePlural$Resource, $.type|private$), &$.type|raw${})$end$ if obj == nil { return nil, err } - return obj.(*$.type|raw$), err + return obj.(*$.resultType|raw$), err } ` @@ -342,14 +479,14 @@ func (c *Fake$.type|publicPlural$) Watch(opts $.ListOptions|raw$) ($.watchInterf ` var patchTemplate = ` -// Patch applies the patch and returns the patched $.type|private$. -func (c *Fake$.type|publicPlural$) Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.type|raw$, err error) { +// Patch applies the patch and returns the patched $.resultType|private$. +func (c *Fake$.type|publicPlural$) Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.resultType|raw$, err error) { obj, err := c.Fake. - $if .namespaced$Invokes($.NewPatchSubresourceAction|raw$($.type|allLowercasePlural$Resource, c.ns, name, data, subresources... ), &$.type|raw${}) - $else$Invokes($.NewRootPatchSubresourceAction|raw$($.type|allLowercasePlural$Resource, name, data, subresources...), &$.type|raw${})$end$ + $if .namespaced$Invokes($.NewPatchSubresourceAction|raw$($.type|allLowercasePlural$Resource, c.ns, name, data, subresources... ), &$.resultType|raw${}) + $else$Invokes($.NewRootPatchSubresourceAction|raw$($.type|allLowercasePlural$Resource, name, data, subresources...), &$.resultType|raw${})$end$ if obj == nil { return nil, err } - return obj.(*$.type|raw$), err + return obj.(*$.resultType|raw$), err } ` diff --git a/cmd/client-gen/generators/generator_for_type.go b/cmd/client-gen/generators/generator_for_type.go index 17eea6c2..51f39bdb 100644 --- a/cmd/client-gen/generators/generator_for_type.go +++ b/cmd/client-gen/generators/generator_for_type.go @@ -77,12 +77,61 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i if err != nil { return err } + type extendedInterfaceMethod struct { + template string + args map[string]interface{} + } + extendedMethods := []extendedInterfaceMethod{} + for _, e := range tags.Extensions { + inputType := *t + resultType := *t + // TODO: Extract this to some helper method as this code is copied into + // 2 other places. + if len(e.InputTypeOverride) > 0 { + if name, pkg := e.Input(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + inputType = *newType + } else { + inputType.Name.Name = e.InputTypeOverride + } + } + if len(e.ResultTypeOverride) > 0 { + if name, pkg := e.Result(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + resultType = *newType + } else { + resultType.Name.Name = e.ResultTypeOverride + } + } + var updatedVerbtemplate string + if _, exists := subresourceDefaultVerbTemplates[e.VerbType]; e.IsSubresource() && exists { + updatedVerbtemplate = e.VerbName + "(" + strings.TrimPrefix(subresourceDefaultVerbTemplates[e.VerbType], strings.Title(e.VerbType)+"(") + } else { + updatedVerbtemplate = e.VerbName + "(" + strings.TrimPrefix(defaultVerbTemplates[e.VerbType], strings.Title(e.VerbType)+"(") + } + extendedMethods = append(extendedMethods, extendedInterfaceMethod{ + template: updatedVerbtemplate, + args: map[string]interface{}{ + "type": t, + "inputType": &inputType, + "resultType": &resultType, + "DeleteOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "DeleteOptions"}), + "ListOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ListOptions"}), + "GetOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "GetOptions"}), + "PatchType": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/types", Name: "PatchType"}), + }, + }) + } m := map[string]interface{}{ "type": t, + "inputType": t, + "resultType": t, "package": pkg, "Package": namer.IC(pkg), "namespaced": !tags.NonNamespaced, "Group": namer.IC(g.group), + "subresource": false, + "subresourcePath": "", "GroupVersion": namer.IC(g.group) + namer.IC(g.version), "DeleteOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "DeleteOptions"}), "ListOptions": c.Universe.Type(types.Name{Package: "k8s.io/apimachinery/pkg/apis/meta/v1", Name: "ListOptions"}), @@ -105,7 +154,16 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i if !genStatus(t) { tags.SkipVerbs = append(tags.SkipVerbs, "updateStatus") } - sw.Do(generateInterface(tags), m) + interfaceSuffix := "" + if len(extendedMethods) > 0 { + interfaceSuffix = "\n" + } + sw.Do("\n"+generateInterface(tags)+interfaceSuffix, m) + // add extended verbs into interface + for _, v := range extendedMethods { + sw.Do(v.template+interfaceSuffix, v.args) + } + } sw.Do(interfaceTemplate4, m) @@ -150,9 +208,88 @@ func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w i sw.Do(patchTemplate, m) } + // generate expansion methods + for _, e := range tags.Extensions { + inputType := *t + resultType := *t + if len(e.InputTypeOverride) > 0 { + if name, pkg := e.Input(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + inputType = *newType + } else { + inputType.Name.Name = e.InputTypeOverride + } + } + if len(e.ResultTypeOverride) > 0 { + if name, pkg := e.Result(); len(pkg) > 0 { + newType := c.Universe.Type(types.Name{Package: pkg, Name: name}) + resultType = *newType + } else { + resultType.Name.Name = e.ResultTypeOverride + } + } + m["inputType"] = &inputType + m["resultType"] = &resultType + m["subresourcePath"] = e.SubResourcePath + + if e.HasVerb("get") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, getSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, getTemplate), m) + } + } + + if e.HasVerb("list") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, listSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, listTemplate), m) + } + } + + // TODO: Figure out schemantic for watching a sub-resource. + if e.HasVerb("watch") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, watchTemplate), m) + } + + if e.HasVerb("create") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, createSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, createTemplate), m) + } + } + + if e.HasVerb("update") { + if e.IsSubresource() { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, updateSubresourceTemplate), m) + } else { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, updateTemplate), m) + } + } + + // TODO: Figure out schemantic for deleting a sub-resource (what arguments + // are passed, does it need two names? etc. + if e.HasVerb("delete") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, deleteTemplate), m) + } + + if e.HasVerb("patch") { + sw.Do(adjustTemplate(e.VerbName, e.VerbType, patchTemplate), m) + } + } + return sw.Error() } +// adjustTemplate adjust the origin verb template using the expansion name. +// TODO: Make the verbs in templates parametrized so the strings.Replace() is +// not needed. +func adjustTemplate(name, verbType, template string) string { + return strings.Replace(template, " "+strings.Title(verbType), " "+name, -1) +} + func generateInterface(tags util.Tags) string { // need an ordered list here to guarantee order of generated methods. out := []string{} @@ -164,16 +301,23 @@ func generateInterface(tags util.Tags) string { return strings.Join(out, "\n") } +var subresourceDefaultVerbTemplates = map[string]string{ + "create": `Create($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (*$.resultType|raw$, error)`, + "list": `List($.type|private$Name string, opts $.ListOptions|raw$) (*$.resultType|raw$List, error)`, + "update": `Update($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (*$.resultType|raw$, error)`, + "get": `Get($.type|private$Name string, options $.GetOptions|raw$) (*$.resultType|raw$, error)`, +} + var defaultVerbTemplates = map[string]string{ - "create": `Create(*$.type|raw$) (*$.type|raw$, error)`, - "update": `Update(*$.type|raw$) (*$.type|raw$, error)`, + "create": `Create(*$.inputType|raw$) (*$.resultType|raw$, error)`, + "update": `Update(*$.inputType|raw$) (*$.resultType|raw$, error)`, "updateStatus": `UpdateStatus(*$.type|raw$) (*$.type|raw$, error)`, "delete": `Delete(name string, options *$.DeleteOptions|raw$) error`, "deleteCollection": `DeleteCollection(options *$.DeleteOptions|raw$, listOptions $.ListOptions|raw$) error`, - "get": `Get(name string, options $.GetOptions|raw$) (*$.type|raw$, error)`, - "list": `List(opts $.ListOptions|raw$) (*$.type|raw$List, error)`, + "get": `Get(name string, options $.GetOptions|raw$) (*$.resultType|raw$, error)`, + "list": `List(opts $.ListOptions|raw$) (*$.resultType|raw$List, error)`, "watch": `Watch(opts $.ListOptions|raw$) ($.watchInterface|raw$, error)`, - "patch": `Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.type|raw$, err error)`, + "patch": `Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.resultType|raw$, err error)`, } // group client will implement this interface. @@ -238,24 +382,40 @@ func new$.type|publicPlural$(c *$.GroupVersion$Client) *$.type|privatePlural$ { } } ` - var listTemplate = ` -// List takes label and field selectors, and returns the list of $.type|publicPlural$ that match those selectors. -func (c *$.type|privatePlural$) List(opts $.ListOptions|raw$) (result *$.type|raw$List, err error) { - result = &$.type|raw$List{} +// List takes label and field selectors, and returns the list of $.resultType|publicPlural$ that match those selectors. +func (c *$.type|privatePlural$) List(opts $.ListOptions|raw$) (result *$.resultType|raw$List, err error) { + result = &$.resultType|raw$List{} + err = c.client.Get(). + $if .namespaced$Namespace(c.ns).$end$ + Resource("$.type|resource$"). + VersionedParams(&opts, $.schemeParameterCodec|raw$). + Do(). + Into(result) + return +} +` + +var listSubresourceTemplate = ` +// List takes $.type|raw$ name, label and field selectors, and returns the list of $.resultType|publicPlural$ that match those selectors. +func (c *$.type|privatePlural$) List($.type|private$Name string, opts $.ListOptions|raw$) (result *$.resultType|raw$List, err error) { + result = &$.resultType|raw$List{} err = c.client.Get(). $if .namespaced$Namespace(c.ns).$end$ Resource("$.type|resource$"). + Name($.type|private$Name). + SubResource("$.subresourcePath$"). VersionedParams(&opts, $.schemeParameterCodec|raw$). Do(). Into(result) return } ` + var getTemplate = ` -// Get takes name of the $.type|private$, and returns the corresponding $.type|private$ object, and an error if there is any. -func (c *$.type|privatePlural$) Get(name string, options $.GetOptions|raw$) (result *$.type|raw$, err error) { - result = &$.type|raw${} +// Get takes name of the $.type|private$, and returns the corresponding $.resultType|private$ object, and an error if there is any. +func (c *$.type|privatePlural$) Get(name string, options $.GetOptions|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} err = c.client.Get(). $if .namespaced$Namespace(c.ns).$end$ Resource("$.type|resource$"). @@ -267,6 +427,22 @@ func (c *$.type|privatePlural$) Get(name string, options $.GetOptions|raw$) (res } ` +var getSubresourceTemplate = ` +// Get takes name of the $.type|private$, and returns the corresponding $.resultType|raw$ object, and an error if there is any. +func (c *$.type|privatePlural$) Get($.type|private$Name string, options $.GetOptions|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} + err = c.client.Get(). + $if .namespaced$Namespace(c.ns).$end$ + Resource("$.type|resource$"). + Name($.type|private$Name). + SubResource("$.subresourcePath$"). + VersionedParams(&options, $.schemeParameterCodec|raw$). + Do(). + Into(result) + return +} +` + var deleteTemplate = ` // Delete takes name of the $.type|private$ and deletes it. Returns an error if one occurs. func (c *$.type|privatePlural$) Delete(name string, options *$.DeleteOptions|raw$) error { @@ -293,14 +469,46 @@ func (c *$.type|privatePlural$) DeleteCollection(options *$.DeleteOptions|raw$, } ` +var createSubresourceTemplate = ` +// Create takes the representation of a $.inputType|private$ and creates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *$.type|privatePlural$) Create($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} + err = c.client.Post(). + $if .namespaced$Namespace(c.ns).$end$ + Resource("$.type|resource$"). + Name($.type|private$Name). + SubResource("$.subresourcePath$"). + Body($.inputType|private$). + Do(). + Into(result) + return +} +` + var createTemplate = ` -// Create takes the representation of a $.type|private$ and creates it. Returns the server's representation of the $.type|private$, and an error, if there is any. -func (c *$.type|privatePlural$) Create($.type|private$ *$.type|raw$) (result *$.type|raw$, err error) { - result = &$.type|raw${} +// Create takes the representation of a $.inputType|private$ and creates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *$.type|privatePlural$) Create($.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} err = c.client.Post(). $if .namespaced$Namespace(c.ns).$end$ Resource("$.type|resource$"). - Body($.type|private$). + Body($.inputType|private$). + Do(). + Into(result) + return +} +` + +var updateSubresourceTemplate = ` +// Update takes the top resource name and the representation of a $.inputType|private$ and updates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *$.type|privatePlural$) Update($.type|private$Name string, $.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} + err = c.client.Put(). + $if .namespaced$Namespace(c.ns).$end$ + Resource("$.type|resource$"). + Name($.type|private$Name). + SubResource("$.subresourcePath$"). + Body($.inputType|private$). Do(). Into(result) return @@ -308,14 +516,14 @@ func (c *$.type|privatePlural$) Create($.type|private$ *$.type|raw$) (result *$. ` var updateTemplate = ` -// Update takes the representation of a $.type|private$ and updates it. Returns the server's representation of the $.type|private$, and an error, if there is any. -func (c *$.type|privatePlural$) Update($.type|private$ *$.type|raw$) (result *$.type|raw$, err error) { - result = &$.type|raw${} +// Update takes the representation of a $.inputType|private$ and updates it. Returns the server's representation of the $.resultType|private$, and an error, if there is any. +func (c *$.type|privatePlural$) Update($.inputType|private$ *$.inputType|raw$) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} err = c.client.Put(). $if .namespaced$Namespace(c.ns).$end$ Resource("$.type|resource$"). - Name($.type|private$.Name). - Body($.type|private$). + Name($.inputType|private$.Name). + Body($.inputType|private$). Do(). Into(result) return @@ -353,9 +561,9 @@ func (c *$.type|privatePlural$) Watch(opts $.ListOptions|raw$) ($.watchInterface ` var patchTemplate = ` -// Patch applies the patch and returns the patched $.type|private$. -func (c *$.type|privatePlural$) Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.type|raw$, err error) { - result = &$.type|raw${} +// Patch applies the patch and returns the patched $.resultType|private$. +func (c *$.type|privatePlural$) Patch(name string, pt $.PatchType|raw$, data []byte, subresources ...string) (result *$.resultType|raw$, err error) { + result = &$.resultType|raw${} err = c.client.Patch(pt). $if .namespaced$Namespace(c.ns).$end$ Resource("$.type|resource$"). diff --git a/cmd/client-gen/generators/util/tags.go b/cmd/client-gen/generators/util/tags.go index 38747ec2..0b7d68ca 100644 --- a/cmd/client-gen/generators/util/tags.go +++ b/cmd/client-gen/generators/util/tags.go @@ -32,6 +32,7 @@ var supportedTags = []string{ "genclient:skipVerbs", "genclient:noStatus", "genclient:readonly", + "genclient:method", } // SupportedVerbs is a list of supported verbs for +onlyVerbs and +skipVerbs. @@ -54,6 +55,96 @@ var ReadonlyVerbs = []string{ "watch", } +// genClientPrefix is the default prefix for all genclient tags. +const genClientPrefix = "genclient:" + +// unsupportedExtensionVerbs is a list of verbs we don't support generating +// extension client functions for. +var unsupportedExtensionVerbs = []string{ + "updateStatus", + "deleteCollection", + "watch", + "delete", +} + +// inputTypeSupportedVerbs is a list of verb types that supports overriding the +// input argument type. +var inputTypeSupportedVerbs = []string{ + "create", + "update", +} + +// resultTypeSupportedVerbs is a list of verb types that supports overriding the +// resulting type. +var resultTypeSupportedVerbs = []string{ + "create", + "update", + "get", + "list", + "patch", +} + +// Extensions allows to extend the default set of client verbs +// (CRUD+watch+patch+list+deleteCollection) for a given type with custom defined +// verbs. Custom verbs can have custom input and result types and also allow to +// use a sub-resource in a request instead of top-level resource type. +// +// Example: +// +// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale +// +// type ReplicaSet struct { ... } +// +// The 'method=UpdateScale' is the name of the client function. +// The 'verb=update' here means the client function will use 'PUT' action. +// The 'subresource=scale' means we will use SubResource template to generate this client function. +// The 'input' is the input type used for creation (function argument). +// The 'result' (not needed in this case) is the result type returned from the +// client function. +// +type extension struct { + // VerbName is the name of the custom verb (Scale, Instantiate, etc..) + VerbName string + // VerbType is the type of the verb (only verbs from SupportedVerbs are + // supported) + VerbType string + // SubResourcePath defines a path to a sub-resource to use in the request. + // (optional) + SubResourcePath string + // InputTypeOverride overrides the input parameter type for the verb. By + // default the original type is used. Overriding the input type only works for + // "create" and "update" verb types. The given type must exists in the same + // package as the original type. + // (optional) + InputTypeOverride string + // ResultTypeOverride overrides the resulting object type for the verb. By + // default the original type is used. Overriding the result type works. + // (optional) + ResultTypeOverride string +} + +// IsSubresource indicates if this extension should generate the sub-resource. +func (e *extension) IsSubresource() bool { + return len(e.SubResourcePath) > 0 +} + +// HasVerb checks if the extension matches the given verb. +func (e *extension) HasVerb(verb string) bool { + return e.VerbType == verb +} + +// Input returns the input override package path and the type. +func (e *extension) Input() (string, string) { + parts := strings.Split(e.InputTypeOverride, ".") + return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".") +} + +// Result returns the result override package path and the type. +func (e *extension) Result() (string, string) { + parts := strings.Split(e.ResultTypeOverride, ".") + return parts[len(parts)-1], strings.Join(parts[0:len(parts)-1], ".") +} + // Tags represents a genclient configuration for a single type. type Tags struct { // +genclient @@ -67,6 +158,8 @@ type Tags struct { // +genclient:skipVerbs=get,update // +genclient:onlyVerbs=create,delete SkipVerbs []string + // +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale + Extensions []extension } // HasVerb returns true if we should include the given verb in final client interface and @@ -103,25 +196,25 @@ func ParseClientGenTags(lines []string) (Tags, error) { if len(value) > 0 && len(value[0]) > 0 { return ret, fmt.Errorf("+genclient=%s is invalid, use //+genclient if you want to generate client or omit it when you want to disable generation", value) } - _, ret.NonNamespaced = values["genclient:nonNamespaced"] + _, ret.NonNamespaced = values[genClientPrefix+"nonNamespaced"] // Check the old format and error when used if value := values["nonNamespaced"]; len(value) > 0 && len(value[0]) > 0 { return ret, fmt.Errorf("+nonNamespaced=%s is invalid, use //+genclient:nonNamespaced instead", value[0]) } - _, ret.NoVerbs = values["genclient:noVerbs"] - _, ret.NoStatus = values["genclient:noStatus"] + _, ret.NoVerbs = values[genClientPrefix+"noVerbs"] + _, ret.NoStatus = values[genClientPrefix+"noStatus"] onlyVerbs := []string{} - if _, isReadonly := values["genclient:readonly"]; isReadonly { + if _, isReadonly := values[genClientPrefix+"readonly"]; isReadonly { onlyVerbs = ReadonlyVerbs } // Check the old format and error when used if value := values["readonly"]; len(value) > 0 && len(value[0]) > 0 { return ret, fmt.Errorf("+readonly=%s is invalid, use //+genclient:readonly instead", value[0]) } - if v, exists := values["genclient:skipVerbs"]; exists { + if v, exists := values[genClientPrefix+"skipVerbs"]; exists { ret.SkipVerbs = strings.Split(v[0], ",") } - if v, exists := values["genclient:onlyVerbs"]; exists || len(onlyVerbs) > 0 { + if v, exists := values[genClientPrefix+"onlyVerbs"]; exists || len(onlyVerbs) > 0 { if len(v) > 0 { onlyVerbs = append(onlyVerbs, strings.Split(v[0], ",")...) } @@ -146,16 +239,101 @@ func ParseClientGenTags(lines []string) (Tags, error) { } ret.SkipVerbs = skipVerbs } + var err error + if ret.Extensions, err = parseClientExtensions(values); err != nil { + return ret, err + } return ret, validateClientGenTags(values) } +func parseClientExtensions(tags map[string][]string) ([]extension, error) { + var ret []extension + for name, values := range tags { + if !strings.HasPrefix(name, genClientPrefix+"method") { + continue + } + for _, value := range values { + // the value comes in this form: "Foo,verb=create" + ext := extension{} + parts := strings.Split(value, ",") + if len(parts) == 0 { + return nil, fmt.Errorf("invalid of empty extension verb name: %q", value) + } + // The first part represents the name of the extension + ext.VerbName = parts[0] + if len(ext.VerbName) == 0 { + return nil, fmt.Errorf("must specify a verb name (// +genclient:method=Foo,verb=create)") + } + // Parse rest of the arguments + params := parts[1:] + for _, p := range params { + parts := strings.Split(p, "=") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid extension tag specification %q", p) + } + key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + if len(val) == 0 { + return nil, fmt.Errorf("empty value of %q for %q extension", key, ext.VerbName) + } + switch key { + case "verb": + ext.VerbType = val + case "subresource": + ext.SubResourcePath = val + case "input": + ext.InputTypeOverride = val + case "result": + ext.ResultTypeOverride = val + default: + return nil, fmt.Errorf("unknown extension configuration key %q", key) + } + } + // Validate resulting extension configuration + if len(ext.VerbType) == 0 { + return nil, fmt.Errorf("verb type must be specified (use '// +genclient:method=%s,verb=create')", ext.VerbName) + } + if len(ext.ResultTypeOverride) > 0 { + supported := false + for _, v := range resultTypeSupportedVerbs { + if ext.VerbType == v { + supported = true + break + } + } + if !supported { + return nil, fmt.Errorf("%s: result type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, resultTypeSupportedVerbs) + } + } + if len(ext.InputTypeOverride) > 0 { + supported := false + for _, v := range inputTypeSupportedVerbs { + if ext.VerbType == v { + supported = true + break + } + } + if !supported { + return nil, fmt.Errorf("%s: input type is not supported for %q verbs (supported verbs: %#v)", ext.VerbName, ext.VerbType, inputTypeSupportedVerbs) + } + } + for _, t := range unsupportedExtensionVerbs { + if ext.VerbType == t { + return nil, fmt.Errorf("verb %q is not supported by extension generator", ext.VerbType) + } + } + ret = append(ret, ext) + } + } + return ret, nil +} + // validateTags validates that only supported genclient tags were provided. func validateClientGenTags(values map[string][]string) error { for _, k := range supportedTags { delete(values, k) } for key := range values { - if strings.HasPrefix(key, "genclient") { + if strings.HasPrefix(key, strings.TrimSuffix(genClientPrefix, ":")) { return errors.New("unknown tag detected: " + key) } } diff --git a/cmd/client-gen/generators/util/tags_test.go b/cmd/client-gen/generators/util/tags_test.go index 531b5848..a5c93875 100644 --- a/cmd/client-gen/generators/util/tags_test.go +++ b/cmd/client-gen/generators/util/tags_test.go @@ -82,3 +82,67 @@ func TestParseTags(t *testing.T) { } } } + +func TestParseTagsExtension(t *testing.T) { + testCases := map[string]struct { + lines []string + expectedExtensions []extension + expectError bool + }{ + "simplest extension": { + lines: []string{`+genclient:method=Foo,verb=create`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}}, + }, + "multiple extensions": { + lines: []string{`+genclient:method=Foo,verb=create`, `+genclient:method=Bar,verb=get`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create"}, {VerbName: "Bar", VerbType: "get"}}, + }, + "extension without verb": { + lines: []string{`+genclient:method`}, + expectError: true, + }, + "extension without verb type": { + lines: []string{`+genclient:method=Foo`}, + expectError: true, + }, + "sub-resource extension": { + lines: []string{`+genclient:method=Foo,verb=create,subresource=bar`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "create", SubResourcePath: "bar"}}, + }, + "output type extension": { + lines: []string{`+genclient:method=Foos,verb=list,result=Bars`}, + expectedExtensions: []extension{{VerbName: "Foos", VerbType: "list", ResultTypeOverride: "Bars"}}, + }, + "input type extension": { + lines: []string{`+genclient:method=Foo,verb=update,input=Bar`}, + expectedExtensions: []extension{{VerbName: "Foo", VerbType: "update", InputTypeOverride: "Bar"}}, + }, + "unknown verb type extension": { + lines: []string{`+genclient:method=Foo,verb=explode`}, + expectedExtensions: nil, + expectError: true, + }, + "invalid verb extension": { + lines: []string{`+genclient:method=Foo,unknown=bar`}, + expectedExtensions: nil, + expectError: true, + }, + "empty verb extension subresource": { + lines: []string{`+genclient:method=Foo,verb=get,subresource=`}, + expectedExtensions: nil, + expectError: true, + }, + } + for key, c := range testCases { + result, err := ParseClientGenTags(c.lines) + if err != nil && !c.expectError { + t.Fatalf("[%s] unexpected error: %v", key, err) + } + if err != nil && c.expectError { + t.Logf("[%s] got expected error: %+v", key, err) + } + if !c.expectError && !reflect.DeepEqual(result.Extensions, c.expectedExtensions) { + t.Errorf("[%s] expected %#+v to be %#+v", key, result.Extensions, c.expectedExtensions) + } + } +}