diff --git a/cmd/api.go b/cmd/api.go index a054c86b3e7..03f64c72946 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -22,6 +22,7 @@ import ( "log" "os" "os/exec" + "strings" "github.com/spf13/cobra" flag "github.com/spf13/pflag" @@ -29,6 +30,7 @@ import ( "sigs.k8s.io/kubebuilder/cmd/util" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" + "sigs.k8s.io/kubebuilder/plugins/addon" ) type apiOptions struct { @@ -37,6 +39,9 @@ type apiOptions struct { // runMake indicates whether to run make or not after scaffolding APIs runMake bool + + // pattern indicates that we should use a plugin to build according to a pattern + pattern string } func (o *apiOptions) bindCmdFlags(cmd *cobra.Command) { @@ -48,6 +53,10 @@ func (o *apiOptions) bindCmdFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.apiScaffolder.DoController, "controller", true, "if set, generate the controller without prompting the user") o.controllerFlag = cmd.Flag("controller") + if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { + cmd.Flags().StringVar(&o.pattern, "pattern", "", + "generates an API following an extension pattern (addon)") + } o.apiScaffolder.Resource = resourceForFlags(cmd.Flags()) } @@ -67,6 +76,17 @@ func resourceForFlags(f *flag.FlagSet) *resource.Resource { func (o *apiOptions) runAddAPI() { dieIfNoProject() + switch strings.ToLower(o.pattern) { + case "": + // Default pattern + + case "addon": + o.apiScaffolder.Plugins = append(o.apiScaffolder.Plugins, &addon.Plugin{}) + + default: + log.Fatalf("unknown pattern %q", o.pattern) + } + reader := bufio.NewReader(os.Stdin) if !o.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") diff --git a/cmd/vendor_update.go b/cmd/vendor_update.go index ccc95a747c1..33a7b3cc5dd 100644 --- a/cmd/vendor_update.go +++ b/cmd/vendor_update.go @@ -20,6 +20,7 @@ import ( "log" "github.com/spf13/cobra" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" @@ -35,7 +36,9 @@ kubebuilder update vendor `, Run: func(cmd *cobra.Command, args []string) { dieIfNoProject() - err := (&scaffold.Scaffold{}).Execute(input.Options{}, + err := (&scaffold.Scaffold{}).Execute( + &model.Universe{}, + input.Options{}, &project.GopkgToml{}) if err != nil { log.Fatalf("error updating vendor dependecies %v", err) diff --git a/cmd/webhook_v1.go b/cmd/webhook_v1.go index 7fd415a5c40..8362f2afb86 100644 --- a/cmd/webhook_v1.go +++ b/cmd/webhook_v1.go @@ -27,6 +27,7 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" @@ -68,7 +69,9 @@ This command is only available for v1 scaffolding project. o.res.Resource = flect.Pluralize(strings.ToLower(o.res.Kind)) } - err = (&scaffold.Scaffold{}).Execute(input.Options{}, + err = (&scaffold.Scaffold{}).Execute( + &model.Universe{}, + input.Options{}, &manager.Webhook{}, &webhook.AdmissionHandler{Resource: o.res, Config: webhook.Config{Server: o.server, Type: o.webhookType, Operations: o.operations}}, &webhook.AdmissionWebhookBuilder{Resource: o.res, Config: webhook.Config{Server: o.server, Type: o.webhookType, Operations: o.operations}}, diff --git a/cmd/webhook_v2.go b/cmd/webhook_v2.go index 7eeb58d0ea2..88ae9501832 100644 --- a/cmd/webhook_v2.go +++ b/cmd/webhook_v2.go @@ -26,6 +26,7 @@ import ( "github.com/gobuffalo/flect" "github.com/spf13/cobra" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" @@ -82,6 +83,7 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f Validating: o.validation, } err = (&scaffold.Scaffold{}).Execute( + &model.Universe{}, input.Options{}, webhookScaffolder, ) diff --git a/pkg/model/types.go b/pkg/model/types.go new file mode 100644 index 00000000000..0c5211fe7cb --- /dev/null +++ b/pkg/model/types.go @@ -0,0 +1,55 @@ +package model + +import ( + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" +) + +// Universe describes the entire state of file generation +type Universe struct { + Boilerplate string `json:"boilerplate,omitempty"` + + Resource *Resource `json:"resource,omitempty"` + + Files []*File `json:"files,omitempty"` +} + +// Resource describes the resource currently being generated +// TODO: Just use the resource type? +type Resource struct { + // Namespaced is true if the resource is namespaced + Namespaced bool `json:"namespaces,omitempty"` + + // Group is the API Group. Does not contain the domain. + Group string `json:"group,omitempty"` + + // Version is the API version - e.g. v1beta1 + Version string `json:"version,omitempty"` + + // Kind is the API Kind. + Kind string `json:"kind,omitempty"` + + // Plural is the plural lowercase of Kind. + Plural string `json:"plural,omitempty"` + + // Resource is the API Resource. + Resource string `json:"resource,omitempty"` + + // ResourcePackage is the go package of the Resource + GoPackage string `json:"goPackage,omitempty"` + + // GroupDomain is the Group + "." + Domain for the Resource + GroupDomain string `json:"groupDomain,omitempty"` +} + +// File describes a file that will be written +type File struct { + // Path is the file to write + Path string `json:"path,omitempty"` + + // Contents is the generated output + Contents string `json:"contents,omitempty"` + + // TODO: Move input.IfExistsAction into model + // IfExistsAction determines what to do if the file exists + IfExistsAction input.IfExistsAction `json:"ifExistsAction,omitempty"` +} diff --git a/pkg/scaffold/api.go b/pkg/scaffold/api.go index 716e17f8aab..cf2ee108a09 100644 --- a/pkg/scaffold/api.go +++ b/pkg/scaffold/api.go @@ -21,8 +21,11 @@ import ( "path/filepath" "strings" + "github.com/gobuffalo/flect" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/controller" resourcev1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" resourcev2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" @@ -34,6 +37,9 @@ import ( type API struct { scaffold *Scaffold + // Plugins is the list of plugins we should allow to transform our generated scaffolding + Plugins []Plugin + Resource *resourcev1.Resource project *input.ProjectFile @@ -60,6 +66,7 @@ func (api *API) Validate() error { if api.Resource.Kind == "" { return fmt.Errorf("missing kind information for resource") } + return nil } @@ -89,6 +96,23 @@ func (api *API) Scaffold() error { } } +func (api *API) buildUniverse() *model.Universe { + resource := &model.Resource{ + Namespaced: api.Resource.Namespaced, + Group: api.Resource.Group, + Version: api.Resource.Version, + Kind: api.Resource.Kind, + Resource: api.Resource.Resource, + Plural: flect.Pluralize(strings.ToLower(api.Resource.Kind)), + } + + resource.GoPackage, resource.GroupDomain = util.GetResourceInfo(api.Resource, api.project.Repo, api.project.Domain) + + return &model.Universe{ + Resource: resource, + } +} + func (api *API) scaffoldV1() error { r := api.Resource @@ -98,7 +122,7 @@ func (api *API) scaffoldV1() error { fmt.Println(filepath.Join("pkg", "apis", r.Group, r.Version, fmt.Sprintf("%s_types_test.go", strings.ToLower(r.Kind)))) - err := (&Scaffold{}).Execute(input.Options{}, + err := (&Scaffold{}).Execute(api.buildUniverse(), input.Options{}, &resourcev1.Register{Resource: r}, &resourcev1.Types{Resource: r}, &resourcev1.VersionSuiteTest{Resource: r}, @@ -125,7 +149,7 @@ func (api *API) scaffoldV1() error { fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(r.Kind), fmt.Sprintf("%s_controller_test.go", strings.ToLower(r.Kind)))) - err := (&Scaffold{}).Execute(input.Options{}, + err := (&Scaffold{}).Execute(api.buildUniverse(), input.Options{}, &controller.Controller{Resource: r}, &controller.AddController{Resource: r}, &controller.Test{Resource: r}, @@ -150,8 +174,7 @@ func (api *API) scaffoldV2() error { fmt.Println(filepath.Join("api", r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind)))) - err := (&Scaffold{}).Execute( - input.Options{}, + files := []input.File{ &resourcev2.Types{ Input: input.Input{ Path: filepath.Join("api", r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))), @@ -161,13 +184,18 @@ func (api *API) scaffoldV2() error { &resourcev2.CRDSample{Resource: r}, &crdv2.EnableWebhookPatch{Resource: r}, &crdv2.EnableCAInjectionPatch{Resource: r}, - ) - if err != nil { + } + + scaffold := &Scaffold{ + Plugins: api.Plugins, + } + + if err := scaffold.Execute(api.buildUniverse(), input.Options{}, files...); err != nil { return fmt.Errorf("error scaffolding APIs: %v", err) } crdKustomization := &crdv2.Kustomization{Resource: r} - err = (&Scaffold{}).Execute( + err := (&Scaffold{}).Execute(api.buildUniverse(), input.Options{}, crdKustomization, &crdv2.KustomizeConfig{}, @@ -203,6 +231,7 @@ func (api *API) scaffoldV2() error { ctrlScaffolder := &resourcev2.Controller{Resource: r} testsuiteScaffolder := &resourcev2.ControllerSuiteTest{Resource: r} err := (&Scaffold{}).Execute( + api.buildUniverse(), input.Options{}, testsuiteScaffolder, ctrlScaffolder, diff --git a/pkg/scaffold/project.go b/pkg/scaffold/project.go index 7e7bfd1ad2b..bee9063c16a 100644 --- a/pkg/scaffold/project.go +++ b/pkg/scaffold/project.go @@ -24,6 +24,7 @@ import ( "strings" "sigs.k8s.io/kubebuilder/cmd/util" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" scaffoldv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1" @@ -77,6 +78,10 @@ func (p *V1Project) EnsureDependencies() (bool, error) { return true, c.Run() } +func (p *V1Project) buildUniverse() *model.Universe { + return &model.Universe{} +} + func (p *V1Project) Scaffold() error { p.Project.Version = project.Version1 @@ -96,6 +101,7 @@ func (p *V1Project) Scaffold() error { } err = s.Execute( + p.buildUniverse(), input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, &p.Project, &p.Boilerplate, @@ -109,6 +115,7 @@ func (p *V1Project) Scaffold() error { s = &Scaffold{} return s.Execute( + p.buildUniverse(), input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, &project.GitIgnore{}, &project.KustomizeRBAC{}, @@ -147,6 +154,10 @@ func (p *V2Project) EnsureDependencies() (bool, error) { return true, c.Run() } +func (p *V2Project) buildUniverse() *model.Universe { + return &model.Universe{} +} + func (p *V2Project) Scaffold() error { p.Project.Version = project.Version2 @@ -166,6 +177,7 @@ func (p *V2Project) Scaffold() error { } err = s.Execute( + p.buildUniverse(), input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, &p.Project, &p.Boilerplate, @@ -179,6 +191,7 @@ func (p *V2Project) Scaffold() error { s = &Scaffold{} return s.Execute( + p.buildUniverse(), input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, &project.GitIgnore{}, &metricsauthv2.KustomizePrometheusMetricsPatch{}, diff --git a/pkg/scaffold/project/project_test.go b/pkg/scaffold/project/project_test.go index 4a1f33dca00..ba44f9ebae7 100644 --- a/pkg/scaffold/project/project_test.go +++ b/pkg/scaffold/project/project_test.go @@ -11,11 +11,12 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" - scaffoldv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1" "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" + scaffoldv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/metricsauth" ) @@ -40,7 +41,7 @@ var _ = Describe("Project", func() { It("should match the golden file", func() { instance := &project.Boilerplate{Year: year, License: "apache2", Owner: "The Kubernetes authors"} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) }) @@ -53,7 +54,7 @@ var _ = Describe("Project", func() { Context("for apache2", func() { It("should write the apache2 boilerplate with specified owners", func() { instance := &project.Boilerplate{Year: year, Owner: "Example Owners"} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) e := strings.Replace( result.Golden, "The Kubernetes authors", "Example Owners", -1) Expect(result.Actual.String()).To(BeEquivalentTo(e)) @@ -61,7 +62,7 @@ var _ = Describe("Project", func() { It("should use apache2 as the default", func() { instance := &project.Boilerplate{Year: year, Owner: "The Kubernetes authors"} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) }) }) @@ -70,7 +71,7 @@ var _ = Describe("Project", func() { It("should write the empty boilerplate", func() { // Scaffold a boilerplate file instance := &project.Boilerplate{Year: year, License: "none", Owner: "Example Owners"} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) Expect(result.Actual.String()).To(BeEquivalentTo(fmt.Sprintf(`/* Copyright %s Example Owners. */`, year))) @@ -82,7 +83,7 @@ Copyright %s Example Owners. instance := &project.Boilerplate{} instance.Boilerplate = `/* Hello World */` - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) Expect(result.Actual.String()).To(BeEquivalentTo(`/* Hello World */`)) }) }) @@ -96,7 +97,7 @@ Copyright %s Example Owners. Context("with defaults ", func() { It("should match the golden file", func() { instance := &project.GopkgToml{} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -121,7 +122,7 @@ Copyright %s Example Owners. instance := &project.GopkgToml{} instance.Input.Path = f.Name() - err = s.Execute(input.Options{}, instance) + err = s.Execute(&model.Universe{}, input.Options{}, instance) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( "skipping modifying Gopkg.toml - file already exists and is unmanaged")) @@ -148,7 +149,7 @@ Copyright %s Example Owners. instance := &project.GopkgToml{} instance.Input.Path = f.Name() - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) Expect(result.Actual.String()).To(BeEquivalentTo(e)) }) }) @@ -167,7 +168,7 @@ Copyright %s Example Owners. instance := &project.GopkgToml{} instance.Input.Path = writeToPath - err = s.Execute(input.Options{}, instance) + err = s.Execute(&model.Universe{}, input.Options{}, instance) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) }) @@ -183,7 +184,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &project.Makefile{Image: "controller:latest"} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -200,7 +201,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &project.Kustomize{Prefix: "project"} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -217,7 +218,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &project.KustomizeRBAC{} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -234,7 +235,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &project.KustomizeManager{} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -251,7 +252,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &scaffoldv1.KustomizeImagePatch{} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -268,7 +269,7 @@ Copyright %s Example Owners. It("should match the golden file", func() { instance := &metricsauth.KustomizePrometheusMetricsPatch{} instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -284,7 +285,7 @@ Copyright %s Example Owners. Context("with defaults ", func() { It("should match the golden file", func() { instance := &project.GitIgnore{} - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) @@ -303,7 +304,7 @@ Copyright %s Example Owners. instance.Version = "1" instance.Domain = "testproject.org" instance.Repo = "project" - Expect(s.Execute(input.Options{}, instance)).NotTo(HaveOccurred()) + Expect(s.Execute(&model.Universe{}, input.Options{}, instance)).NotTo(HaveOccurred()) // Verify the contents matches the golden file. Expect(result.Actual.String()).To(BeEquivalentTo(result.Golden)) diff --git a/pkg/scaffold/scaffold.go b/pkg/scaffold/scaffold.go index e73d8398837..8494c6a674b 100644 --- a/pkg/scaffold/scaffold.go +++ b/pkg/scaffold/scaffold.go @@ -29,6 +29,7 @@ import ( "golang.org/x/tools/imports" yaml "gopkg.in/yaml.v2" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/project" ) @@ -54,6 +55,16 @@ type Scaffold struct { GetWriter func(path string) (io.Writer, error) FileExists func(path string) bool + + // Plugins is the list of plugins we should allow to transform our generated scaffolding + Plugins []Plugin +} + +// Plugin is the interface that a plugin must implement +// We will (later) have an ExecPlugin that implements this by exec-ing a binary +type Plugin interface { + // Pipe is the core plugin interface, that transforms a UniverseModel + Pipe(u *model.Universe) error } func (s *Scaffold) setFieldsAndValidate(t input.File) error { @@ -151,8 +162,8 @@ func (s *Scaffold) defaultOptions(options *input.Options) error { return nil } -// Execute executes scaffolding the Files -func (s *Scaffold) Execute(options input.Options, files ...input.File) error { +// Execute executes scaffolding the for files +func (s *Scaffold) Execute(u *model.Universe, options input.Options, files ...input.File) error { if s.GetWriter == nil { s.GetWriter = (&FileWriter{}).WriteCloser } @@ -163,14 +174,33 @@ func (s *Scaffold) Execute(options input.Options, files ...input.File) error { } } + if u.Boilerplate == "" { + u.Boilerplate = s.Boilerplate + } + if err := s.defaultOptions(&options); err != nil { return err } for _, f := range files { - if err := s.doFile(f); err != nil { + m, err := s.buildFileModel(f) + if err != nil { return err } + u.Files = append(u.Files, m) } + + for _, plugin := range s.Plugins { + if err := plugin.Pipe(u); err != nil { + return err + } + } + + for _, f := range u.Files { + if err := s.writeFile(f); err != nil { + return err + } + } + return nil } @@ -188,43 +218,45 @@ func isAlreadyExistsError(e error) bool { } // doFile scaffolds a single file -func (s *Scaffold) doFile(e input.File) error { +func (s *Scaffold) buildFileModel(e input.File) (*model.File, error) { // Set common fields err := s.setFieldsAndValidate(e) if err != nil { - return err + return nil, err } // Get the template input params i, err := e.GetInput() if err != nil { - return err + return nil, err } + m := &model.File{ + Path: i.Path, + } + + if b, err := s.doTemplate(i, e); err != nil { + return nil, err + } else { + m.Contents = string(b) + } + + return m, nil +} + +func (s *Scaffold) writeFile(file *model.File) error { // Check if the file to write already exists - if s.FileExists(i.Path) { - switch i.IfExistsAction { + if s.FileExists(file.Path) { + switch file.IfExistsAction { case input.Overwrite: case input.Skip: return nil case input.Error: - return &errorAlreadyExists{path: i.Path} + return &errorAlreadyExists{path: file.Path} } } - if err := s.doTemplate(i, e); err != nil { - return err - } - return nil -} - -// doTemplate executes the template for a file using the input -func (s *Scaffold) doTemplate(i input.Input, e input.File) error { - temp, err := newTemplate(e).Parse(i.TemplateBody) - if err != nil { - return err - } - f, err := s.GetWriter(i.Path) + f, err := s.GetWriter(file.Path) if err != nil { return err } @@ -236,10 +268,22 @@ func (s *Scaffold) doTemplate(i input.Input, e input.File) error { }() } + _, err = f.Write([]byte(file.Contents)) + + return err +} + +// doTemplate executes the template for a file using the input +func (s *Scaffold) doTemplate(i input.Input, e input.File) ([]byte, error) { + temp, err := newTemplate(e).Parse(i.TemplateBody) + if err != nil { + return nil, err + } + out := &bytes.Buffer{} err = temp.Execute(out, e) if err != nil { - return err + return nil, err } b := out.Bytes() @@ -248,12 +292,11 @@ func (s *Scaffold) doTemplate(i input.Input, e input.File) error { b, err = imports.Process(i.Path, b, nil) if err != nil { fmt.Printf("%s\n", out.Bytes()) - return err + return nil, err } } - _, err = f.Write(b) - return err + return b, nil } // newTemplate a new template with common functions diff --git a/pkg/scaffold/util/util.go b/pkg/scaffold/util/util.go index dc220b70d45..2133345d375 100644 --- a/pkg/scaffold/util/util.go +++ b/pkg/scaffold/util/util.go @@ -23,11 +23,10 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) -func GetResourceInfo(r *resource.Resource, in input.Input) (resourcePackage, groupDomain string) { +func GetResourceInfo(r *resource.Resource, repo, domain string) (resourcePackage, groupDomain string) { // Use the k8s.io/api package for core resources coreGroups := map[string]string{ "apps": "", @@ -69,5 +68,5 @@ func GetResourceInfo(r *resource.Resource, in input.Input) (resourcePackage, gro } // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath } - return path.Join(in.Repo, "api"), r.Group + "." + in.Domain + return path.Join(repo, "api"), r.Group + "." + domain } diff --git a/pkg/scaffold/v1/manager/manager_test.go b/pkg/scaffold/v1/manager/manager_test.go index ef22ce41a1a..cda10746078 100644 --- a/pkg/scaffold/v1/manager/manager_test.go +++ b/pkg/scaffold/v1/manager/manager_test.go @@ -6,6 +6,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" @@ -48,7 +49,7 @@ var _ = Describe("Manager", func() { Context(f.file, func() { It("should write a file matching the golden file", func() { s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(scaffoldtest.Options(), f.instance)).To(Succeed()) + Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) @@ -61,7 +62,7 @@ var _ = Describe("Manager", func() { instance := &manager.APIs{} s, _ := scaffoldtest.NewTestScaffold(filepath.Join("pkg", "apis", "apis.go"), "") s.ProjectPath = "." - err := s.Execute(scaffoldtest.Options(), instance) + err := s.Execute(&model.Universe{}, scaffoldtest.Options(), instance) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("Rel: can't make")) }) diff --git a/pkg/scaffold/v1/resource/resource_test.go b/pkg/scaffold/v1/resource/resource_test.go index c207e28cb9c..b11e023cf31 100644 --- a/pkg/scaffold/v1/resource/resource_test.go +++ b/pkg/scaffold/v1/resource/resource_test.go @@ -9,6 +9,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" @@ -181,7 +182,7 @@ var _ = Describe("Resource", func() { Context(f.file, func() { It("should write a file matching the golden file", func() { s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(scaffoldtest.Options(), f.instance)).To(Succeed()) + Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) diff --git a/pkg/scaffold/v1/webhook/webhook_test.go b/pkg/scaffold/v1/webhook/webhook_test.go index bd286164f0b..8e53befd6e7 100644 --- a/pkg/scaffold/v1/webhook/webhook_test.go +++ b/pkg/scaffold/v1/webhook/webhook_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" @@ -136,7 +137,7 @@ var _ = Describe("Webhook", func() { Context(f.file, func() { It("should write a file matching the golden file", func() { s, result := scaffoldtest.NewTestScaffold(f.file, f.file) - Expect(s.Execute(scaffoldtest.Options(), f.instance)).To(Succeed()) + Expect(s.Execute(&model.Universe{}, scaffoldtest.Options(), f.instance)).To(Succeed()) Expect(result.Actual.String()).To(Equal(result.Golden), result.Actual.String()) }) }) diff --git a/pkg/scaffold/v2/controller.go b/pkg/scaffold/v2/controller.go index 7eca90ab147..c5be3faa391 100644 --- a/pkg/scaffold/v2/controller.go +++ b/pkg/scaffold/v2/controller.go @@ -47,7 +47,7 @@ type Controller struct { // GetInput implements input.File func (a *Controller) GetInput() (input.Input, error) { - a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Repo, a.Domain) if a.Plural == "" { a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) @@ -57,7 +57,9 @@ func (a *Controller) GetInput() (input.Input, error) { a.Path = filepath.Join("controllers", strings.ToLower(a.Resource.Kind)+"_controller.go") } + a.TemplateBody = controllerTemplate + a.Input.IfExistsAction = input.Error return a.Input, nil } @@ -73,7 +75,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/go-logr/logr" - {{ .Resource.Group}}{{ .Resource.Version }} "{{ .ResourcePackage }}/{{ .Resource.Version }}" + {{ .Resource.Group }}{{ .Resource.Version }} "{{ .ResourcePackage }}/{{ .Resource.Version }}" ) // {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object @@ -96,7 +98,7 @@ func (r *{{ .Resource.Kind }}Reconciler) Reconcile(req ctrl.Request) (ctrl.Resul func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). - For(&{{ .Resource.Group}}{{ .Resource.Version }}.{{ .Resource.Kind }}{}). + For(&{{ .Resource.Group }}{{ .Resource.Version }}.{{ .Resource.Kind }}{}). Complete(r) } ` diff --git a/pkg/scaffold/v2/controller_suitetest.go b/pkg/scaffold/v2/controller_suitetest.go index 9a0caecbfbd..3e5c102966f 100644 --- a/pkg/scaffold/v2/controller_suitetest.go +++ b/pkg/scaffold/v2/controller_suitetest.go @@ -131,7 +131,7 @@ var _ = AfterSuite(func() { // adding import paths and code setup for new types. func (a *ControllerSuiteTest) Update() error { - a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Repo, a.Domain) if a.Plural == "" { a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) } diff --git a/pkg/scaffold/v2/main.go b/pkg/scaffold/v2/main.go index 6bd6cb6e188..3716ed2175b 100644 --- a/pkg/scaffold/v2/main.go +++ b/pkg/scaffold/v2/main.go @@ -53,10 +53,7 @@ func (m *Main) GetInput() (input.Input, error) { func (m *Main) Update(opts *MainUpdateOptions) error { path := "main.go" - resPkg, _ := util.GetResourceInfo(opts.Resource, input.Input{ - Domain: opts.Project.Domain, - Repo: opts.Project.Repo, - }) + resPkg, _ := util.GetResourceInfo(opts.Resource, opts.Project.Repo, opts.Project.Domain) // generate all the code fragments apiImportCodeFragment := fmt.Sprintf(`%s%s "%s/%s" diff --git a/pkg/scaffold/v2/webhook/webhook.go b/pkg/scaffold/v2/webhook/webhook.go index a25d4d395df..dfb1b5ef1b2 100644 --- a/pkg/scaffold/v2/webhook/webhook.go +++ b/pkg/scaffold/v2/webhook/webhook.go @@ -56,7 +56,7 @@ type Webhook struct { // GetInput implements input.File func (a *Webhook) GetInput() (input.Input, error) { - a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Repo, a.Domain) a.GroupDomainWithDash = strings.Replace(a.GroupDomain, ".", "-", -1) diff --git a/plugins/addon/channel.go b/plugins/addon/channel.go new file mode 100644 index 00000000000..f07a6100af1 --- /dev/null +++ b/plugins/addon/channel.go @@ -0,0 +1,39 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package addon + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" +) + +const exampleChannel = `# Versions for the stable channel +manifests: +- version: 0.0.1 +` + +func ExampleChannel(u *model.Universe) error { + m := &model.File{ + Path: filepath.Join("channels", "stable"), + Contents: exampleChannel, + IfExistsAction: input.Error, + } + + return AddFile(u, m) +} diff --git a/plugins/addon/controller.go b/plugins/addon/controller.go new file mode 100644 index 00000000000..e249d7ab703 --- /dev/null +++ b/plugins/addon/controller.go @@ -0,0 +1,99 @@ +package addon + +import ( + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" +) + +func ReplaceController(u *model.Universe) error { + templateBody := controllerTemplate + + funcs := DefaultTemplateFunctions() + contents, err := RunTemplate("controller", templateBody, u, funcs) + if err != nil { + return err + } + + m := &model.File{ + Path: filepath.Join("controllers", strings.ToLower(u.Resource.Kind)+"_controller.go"), + Contents: contents, + IfExistsAction: input.Error, + } + + ReplaceFileIfExists(u, m) + + return nil +} + +var controllerTemplate = `{{ .Boilerplate }} + +package controllers + +import ( + "context" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/go-logr/logr" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" + + api "{{ .Resource.GoPackage }}/{{ .Resource.Version }}" +) + +// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object +type {{ .Resource.Kind }}Reconciler struct { + client.Client + Log logr.Logger + + declarative.Reconciler +} + +// +kubebuilder:rbac:groups={{.Resource.GroupDomain}},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups={{.Resource.GroupDomain}},resources={{ .Resource.Plural }}/status,verbs=get;update;patch + +func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { + addon.Init() + + labels := map[string]string{ + "k8s-app": "{{ .Resource.Kind | lower }}", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &api.{{ .Resource.Kind }}{}, + declarative.WithObjectTransform(declarative.AddLabels(labels)), + declarative.WithOwner(declarative.SourceAsOwner), + declarative.WithLabels(watchLabels), + declarative.WithStatus(status.NewBasic(mgr.GetClient())), + // TODO: add an application to your manifest: declarative.WithObjectTransform(addon.TransformApplicationFromStatus), + // TODO: add an application to your manifest: declarative.WithManagedApplication(watchLabels), + declarative.WithObjectTransform(addon.ApplyPatches), + ); err != nil { + return err + } + + c, err := controller.New("{{ .Resource.Kind | lower }}-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to {{ .Resource.Kind }} + err = c.Watch(&source.Kind{Type: &api.{{ .Resource.Kind }}{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to deployed objects + _, err = declarative.WatchAll(mgr.GetConfig(), c, r, watchLabels) + if err != nil { + return err + } + + return nil +} +` diff --git a/plugins/addon/helpers.go b/plugins/addon/helpers.go new file mode 100644 index 00000000000..e016f169f6d --- /dev/null +++ b/plugins/addon/helpers.go @@ -0,0 +1,84 @@ +package addon + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/gobuffalo/flect" + "sigs.k8s.io/kubebuilder/pkg/model" +) + +// This file gathers functions that are likely to be useful to other +// plugins. Once we have validated they are used in more than one +// place, we can promote them to a shared location. + +type PluginFunc func(u *model.Universe) error + +// AddFile adds the specified file to the model, returning a file if the file already exists +func AddFile(u *model.Universe, add *model.File) error { + p := add.Path + if p == "" { + return fmt.Errorf("path must be set") + } + + for _, f := range u.Files { + if f.Path == p { + return fmt.Errorf("file already exists at path %q", p) + } + } + + u.Files = append(u.Files, add) + return nil +} + +// ReplaceFileIfExists replaces the specified file in the model by path +// Returns true iff the file was replaced. +func ReplaceFileIfExists(u *model.Universe, add *model.File) bool { + p := add.Path + if p == "" { + panic("path must be set") + } + + for i, f := range u.Files { + if f.Path == p { + u.Files[i] = add + return true + } + } + + return false +} + +// ReplaceFile replaces the specified file in the model by path +// If the file does not exist, it returns an error +func ReplaceFile(u *model.Universe, add *model.File) error { + found := ReplaceFileIfExists(u, add) + if !found { + return fmt.Errorf("file not found %q", add.Path) + } + return nil +} + +func DefaultTemplateFunctions() template.FuncMap { + return template.FuncMap{ + "title": strings.Title, + "lower": strings.ToLower, + "plural": flect.Pluralize, + } +} + +func RunTemplate(templateName, templateValue string, data interface{}, funcMap template.FuncMap) (string, error) { + t, err := template.New(templateName).Funcs(funcMap).Parse(templateValue) + if err != nil { + return "", fmt.Errorf("error building template %s: %v", templateName, err) + } + + var b bytes.Buffer + if err := t.Execute(&b, data); err != nil { + return "", fmt.Errorf("error rending template %s: %v", templateName, err) + } + + return b.String(), nil +} diff --git a/plugins/addon/manifest.go b/plugins/addon/manifest.go new file mode 100644 index 00000000000..12dd6d71e39 --- /dev/null +++ b/plugins/addon/manifest.go @@ -0,0 +1,47 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package addon + +import ( + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" +) + +const exampleManifestVersion = "0.0.1" + +const exampleManifestContents = `# Placeholder manifest - replace with the manifest for your addon +` + +func ExampleManifest(u *model.Universe) error { + packageName := getPackageName(u) + + m := &model.File{ + Path: filepath.Join("channels", "packages", packageName, exampleManifestVersion, "manifest.yaml"), + Contents: exampleManifestContents, + IfExistsAction: input.Error, + } + + return AddFile(u, m) +} + +// getPackageName returns the (default) name of the declarative package +func getPackageName(u *model.Universe) string { + return strings.ToLower(u.Resource.Kind) +} diff --git a/plugins/addon/plugin.go b/plugins/addon/plugin.go new file mode 100644 index 00000000000..75d3d2b960b --- /dev/null +++ b/plugins/addon/plugin.go @@ -0,0 +1,26 @@ +package addon + +import ( + "sigs.k8s.io/kubebuilder/pkg/model" +) + +type Plugin struct { +} + +func (p *Plugin) Pipe(u *model.Universe) error { + functions := []PluginFunc{ + ExampleManifest, + ExampleChannel, + ReplaceController, + ReplaceTypes, + } + + for _, fn := range functions { + if err := fn(u); err != nil { + return err + } + + } + + return nil +} diff --git a/plugins/addon/type.go b/plugins/addon/type.go new file mode 100644 index 00000000000..de59ae58fda --- /dev/null +++ b/plugins/addon/type.go @@ -0,0 +1,114 @@ +package addon + +import ( + "fmt" + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" +) + +func ReplaceTypes(u *model.Universe) error { + funcs := DefaultTemplateFunctions() + funcs["JSONTag"] = JSONTag + + contents, err := RunTemplate("types", typesTemplate, u, funcs) + if err != nil { + return err + } + + m := &model.File{ + Path: filepath.Join("controllers", strings.ToLower(u.Resource.Kind)+"_controller.go"), + Contents: contents, + IfExistsAction: input.Error, + } + + ReplaceFileIfExists(u, m) + + return nil +} + +// JSONTag is a helper to build the json tag for a struct +// It works around escaping problems for the json tag syntax +func JSONTag(tag string) string { + return fmt.Sprintf("`json:\"%s\"`", tag) +} + +// Resource.Resource + +var typesTemplate = `{{ .Boilerplate }} + +package {{ .Resource.Version }} + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// {{.Resource.Kind}}Spec defines the desired state of {{.Resource.Kind}} +type {{.Resource.Kind}}Spec struct { + addonv1alpha1.CommonSpec {{ JSONTag ",inline" }} + addonv1alpha1.PatchSpec {{ JSONTag ",inline" }} + + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// {{.Resource.Kind}}Status defines the observed state of {{.Resource.Kind}} +type {{.Resource.Kind}}Status struct { + addonv1alpha1.CommonStatus {{ JSONTag ",inline" }} + + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true + +// {{.Resource.Kind}} is the Schema for the {{ .Resource.Resource }} API +type {{.Resource.Kind}} struct { + metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` + metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` + + Spec {{.Resource.Kind}}Spec ` + "`" + `json:"spec,omitempty"` + "`" + ` + Status {{.Resource.Kind}}Status ` + "`" + `json:"status,omitempty"` + "`" + ` +} + +var _ addonv1alpha1.CommonObject = &{{.Resource.Kind}}{} + +func (o *{{.Resource.Kind}}) ComponentName() string { + return "{{ .Resource.Kind | lower }}" +} + +func (o *{{.Resource.Kind}}) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *{{.Resource.Kind}}) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *{{.Resource.Kind}}) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *{{.Resource.Kind}}) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + +// +kubebuilder:object:root=true + +// {{.Resource.Kind}}List contains a list of {{.Resource.Kind}} +type {{.Resource.Kind}}List struct { + metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` + metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` + Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + ` +} + +func init() { + SchemeBuilder.Register(&{{.Resource.Kind}}{}, &{{.Resource.Kind}}List{}) +} +`