From 6d5e8bb2361b80510fff179dcd9d923c488dad69 Mon Sep 17 00:00:00 2001 From: Adrian Orive Date: Thu, 23 Jan 2020 15:41:29 +0100 Subject: [PATCH] Homogenize commands through Scaffolder interface Signed-off-by: Adrian Orive --- cmd/alpha.go | 15 +- cmd/api.go | 242 ++++++++++++-------- cmd/create.go | 20 +- cmd/edit.go | 91 +++++--- cmd/init.go | 229 +++++++++++++++++++ cmd/init_project.go | 278 ---------------------- cmd/internal/config.go | 38 ++-- cmd/internal/exec.go | 32 +++ cmd/internal/go_version.go | 75 ++++++ cmd/{ => internal}/go_version_test.go | 3 +- cmd/internal/repository.go | 93 ++++++++ cmd/{util => internal}/stdin.go | 2 +- cmd/{util => internal}/validations.go | 29 ++- cmd/main.go | 211 +++++++---------- cmd/root.go | 50 ++++ cmd/update.go | 90 ++++++++ cmd/vendor_update.go | 60 ----- cmd/webhook.go | 210 +++++++++++++++++ cmd/webhook_v1.go | 136 ----------- cmd/webhook_v2.go | 145 ------------ internal/config/config.go | 22 +- pkg/model/universe.go | 14 -- pkg/scaffold/api.go | 291 +++++++++--------------- pkg/scaffold/edit.go | 39 ++++ pkg/scaffold/init.go | 185 +++++++++++++++ pkg/scaffold/interface.go | 21 ++ pkg/scaffold/project.go | 270 ---------------------- pkg/scaffold/project/project.go | 64 ------ pkg/scaffold/project/project_test.go | 19 -- pkg/scaffold/update.go | 50 ++++ pkg/scaffold/v1/webhook/webhook_test.go | 3 +- pkg/scaffold/webhook.go | 164 +++++++++++++ 32 files changed, 1677 insertions(+), 1514 deletions(-) create mode 100644 cmd/init.go delete mode 100644 cmd/init_project.go create mode 100644 cmd/internal/exec.go create mode 100644 cmd/internal/go_version.go rename cmd/{ => internal}/go_version_test.go (97%) create mode 100644 cmd/internal/repository.go rename cmd/{util => internal}/stdin.go (98%) rename cmd/{util => internal}/validations.go (60%) create mode 100644 cmd/root.go create mode 100644 cmd/update.go delete mode 100644 cmd/vendor_update.go create mode 100644 cmd/webhook.go delete mode 100644 cmd/webhook_v1.go delete mode 100644 cmd/webhook_v2.go create mode 100644 pkg/scaffold/edit.go create mode 100644 pkg/scaffold/init.go create mode 100644 pkg/scaffold/interface.go delete mode 100644 pkg/scaffold/project.go delete mode 100644 pkg/scaffold/project/project.go create mode 100644 pkg/scaffold/update.go create mode 100644 pkg/scaffold/webhook.go diff --git a/cmd/alpha.go b/cmd/alpha.go index 5d0026fd8ce..336eb8a54fd 100644 --- a/cmd/alpha.go +++ b/cmd/alpha.go @@ -20,21 +20,10 @@ import ( "github.com/spf13/cobra" ) -// newAlphaCommand returns alpha subcommand which will be mounted -// at the root command by the caller. -func newAlphaCommand() *cobra.Command { - cmd := &cobra.Command{ +func newAlphaCmd() *cobra.Command { + return &cobra.Command{ Use: "alpha", Short: "Expose commands which are in experimental or early stages of development", Long: `Command group for commands which are either experimental or in early stages of development`, - Example: ` -# scaffolds webhook server -kubebuilder alpha webhook -`, } - - cmd.AddCommand( - newWebhookCmd(), - ) - return cmd } diff --git a/cmd/api.go b/cmd/api.go index 050abc8a735..ae4a8af689f 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -18,157 +18,205 @@ package main import ( "bufio" + "errors" "fmt" "log" "os" - "os/exec" "strings" "github.com/spf13/cobra" flag "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/cmd/internal" - "sigs.k8s.io/kubebuilder/cmd/util" + "sigs.k8s.io/kubebuilder/internal/config" "sigs.k8s.io/kubebuilder/pkg/scaffold" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" "sigs.k8s.io/kubebuilder/plugins/addon" ) -type apiOptions struct { - apiScaffolder scaffold.API - resourceFlag, controllerFlag *flag.Flag +type apiError struct { + err error +} - // runMake indicates whether to run make or not after scaffolding APIs - runMake bool +func (e apiError) Error() string { + return fmt.Sprintf("failed to create API: %v", e.err) +} + +func newAPICmd() *cobra.Command { + options := &apiOptions{} + + cmd := &cobra.Command{ + Use: "api", + Short: "Scaffold a Kubernetes API", + Long: `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. + +kubebuilder create api will prompt the user asking if it should scaffold the Resource and / or Controller. To only +scaffold a Controller for an existing Resource, select "n" for Resource. To only define +the schema for a Resource without writing a Controller, select "n" for Controller. + +After the scaffold is written, api will run make on the project. +`, + Example: ` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + kubebuilder create api --group ship --version v1beta1 --kind Frigate + + # Edit the API Scheme + nano api/v1beta1/frigate_types.go + + # Edit the Controller + nano controllers/frigate/frigate_controller.go + + # Edit the Controller Test + nano controllers/frigate/frigate_controller_test.go + + # Install CRDs into the Kubernetes cluster using kubectl apply + make install + + # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config + make run +`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(apiError{err}) + } + }, + } + + options.bindFlags(cmd) + + return cmd +} +var _ commandOptions = &apiOptions{} + +type apiOptions struct { // pattern indicates that we should use a plugin to build according to a pattern pattern string + + resource *resource.Resource + + // Check if we have to scaffold resource and/or controller + resourceFlag *flag.Flag + controllerFlag *flag.Flag + doResource bool + doController bool + + // force indicates that the resource should be created even if it already exists + force bool + + // runMake indicates whether to run make or not after scaffolding APIs + runMake bool } -func (o *apiOptions) bindCmdFlags(cmd *cobra.Command) { - cmd.Flags().BoolVar(&o.runMake, "make", true, - "if true, run make after generating files") - cmd.Flags().BoolVar(&o.apiScaffolder.DoResource, "resource", true, +func (o *apiOptions) bindFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.runMake, "make", true, "if true, run make after generating files") + + cmd.Flags().BoolVar(&o.doResource, "resource", true, "if set, generate the resource without prompting the user") o.resourceFlag = cmd.Flag("resource") - cmd.Flags().BoolVar(&o.apiScaffolder.DoController, "controller", true, + cmd.Flags().BoolVar(&o.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)") } - cmd.Flags().BoolVar(&o.apiScaffolder.Force, "force", false, + + cmd.Flags().BoolVar(&o.force, "force", false, "attempt to create resource even if it already exists") - o.apiScaffolder.Resource = resourceForFlags(cmd.Flags()) -} -// resourceForFlags registers flags for Resource fields and returns the Resource -func resourceForFlags(f *flag.FlagSet) *resource.Resource { - r := &resource.Resource{} - f.StringVar(&r.Kind, "kind", "", "resource Kind") - f.StringVar(&r.Group, "group", "", "resource Group") - f.StringVar(&r.Version, "version", "", "resource Version") - f.BoolVar(&r.Namespaced, "namespaced", true, "resource is namespaced") - f.BoolVar(&r.CreateExampleReconcileBody, "example", true, + o.resource = &resource.Resource{} + cmd.Flags().StringVar(&o.resource.Kind, "kind", "", "resource Kind") + cmd.Flags().StringVar(&o.resource.Group, "group", "", "resource Group") + cmd.Flags().StringVar(&o.resource.Version, "version", "", "resource Version") + cmd.Flags().BoolVar(&o.resource.Namespaced, "namespaced", true, "resource is namespaced") + cmd.Flags().BoolVar(&o.resource.CreateExampleReconcileBody, "example", true, "if true an example reconcile body should be written while scaffolding a resource.") - return r } -// APICmd represents the resource command -func (o *apiOptions) runAddAPI() { - internal.DieIfNotConfigured() - - 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) +func (o *apiOptions) loadConfig() (*config.Config, error) { + projectConfig, err := config.Load() + if os.IsNotExist(err) { + return nil, errors.New("unable to find configuration file, project must be initialized") } - if err := o.apiScaffolder.Validate(); err != nil { - log.Fatalln(err) + return projectConfig, err +} + +func (o *apiOptions) validate(c *config.Config) error { + if err := o.resource.Validate(); err != nil { + return err } reader := bufio.NewReader(os.Stdin) if !o.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") - o.apiScaffolder.DoResource = util.YesNo(reader) + o.doResource = internal.YesNo(reader) } - if !o.controllerFlag.Changed { fmt.Println("Create Controller [y/n]") - o.apiScaffolder.DoController = util.YesNo(reader) - } - - fmt.Println("Writing scaffold for you to edit...") - - if err := o.apiScaffolder.Scaffold(); err != nil { - log.Fatal(err) + o.doController = internal.YesNo(reader) } - if err := o.postScaffold(); err != nil { - log.Fatal(err) - } -} + // In case we want to scaffold a resource API we need to do some checks + if o.doResource { + // Skip the following check for v1 as resources aren't tracked + if !c.IsV1() { + // Check that resource doesn't exist or flag force was set + if !o.force { + resourceExists := false + for _, r := range c.Resources { + if r.Group == o.resource.Group && + r.Version == o.resource.Version && + r.Kind == o.resource.Kind { + resourceExists = true + break + } + } + if resourceExists { + return errors.New("API resource already exists") + } + } + } -func (o *apiOptions) postScaffold() error { - if o.runMake { - fmt.Println("Running make...") - cm := exec.Command("make") // #nosec - cm.Stderr = os.Stderr - cm.Stdout = os.Stdout - if err := cm.Run(); err != nil { - return fmt.Errorf("error running make: %v", err) + // The following check is v2 specific as multi-group isn't enabled by default + if c.IsV2() { + // Check the group is the same for single-group projects + if !c.MultiGroup { + validGroup := true + for _, existingGroup := range c.ResourceGroups() { + if !strings.EqualFold(o.resource.Group, existingGroup) { + validGroup = false + break + } + } + if !validGroup { + return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s", + "kubebuilder.io/migration/multi-group.html") + } + } } } + return nil } -func newAPICommand() *cobra.Command { - options := apiOptions{ - apiScaffolder: scaffold.API{}, - } - - apiCmd := &cobra.Command{ - Use: "api", - Short: "Scaffold a Kubernetes API", - Long: `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. - -create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only -scaffold a Controller for an existing Resource, select "n" for Resource. To only define -the schema for a Resource without writing a Controller, select "n" for Controller. - -After the scaffold is written, api will run make on the project. -`, - Example: ` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate - kubebuilder create api --group ship --version v1beta1 --kind Frigate - - # Edit the API Scheme - nano api/v1beta1/frigate_types.go - - # Edit the Controller - nano controllers/frigate/frigate_controller.go - - # Edit the Controller Test - nano controllers/frigate/frigate_controller_test.go +func (o *apiOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { + plugins := make([]scaffold.Plugin, 0) + switch strings.ToLower(o.pattern) { + case "": + // Default pattern - # Install CRDs into the Kubernetes cluster using kubectl apply - make install + case "addon": + plugins = append(plugins, &addon.Plugin{}) - # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config - make run -`, - Run: func(cmd *cobra.Command, args []string) { - options.runAddAPI() - }, + default: + return nil, fmt.Errorf("unknown pattern %q", o.pattern) } - options.bindCmdFlags(apiCmd) + return scaffold.NewAPIScaffolder(c, o.resource, o.doResource, o.doController, plugins), nil +} - return apiCmd +func (o *apiOptions) postScaffold(_ *config.Config) error { + return internal.RunCmd("Running make", "make") } diff --git a/cmd/create.go b/cmd/create.go index e229ee4b824..5cb48839ae2 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -17,31 +17,13 @@ limitations under the License. package main import ( - "fmt" - "github.com/spf13/cobra" - - "sigs.k8s.io/kubebuilder/cmd/internal" ) func newCreateCmd() *cobra.Command { - cmd := &cobra.Command{ + return &cobra.Command{ Use: "create", Short: "Scaffold a Kubernetes API or webhook.", Long: `Scaffold a Kubernetes API or webhook.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Coming soon.") - }, } - cmd.AddCommand( - newAPICommand(), - ) - - if !internal.ConfiguredAndV1() { - cmd.AddCommand( - newWebhookV2Cmd(), - ) - } - - return cmd } diff --git a/cmd/edit.go b/cmd/edit.go index be4184f2caa..e0cf30fedf7 100644 --- a/cmd/edit.go +++ b/cmd/edit.go @@ -17,59 +17,82 @@ limitations under the License. package main import ( + "errors" + "fmt" "log" + "os" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/cmd/internal" "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold" ) -func newEditProjectCmd() *cobra.Command { +type editError struct { + err error +} + +func (e editError) Error() string { + return fmt.Sprintf("failed to edit configuration: %v", e.err) +} - opts := editProjectCmdOptions{} +func newEditCmd() *cobra.Command { + options := &editOptions{} - editProjectCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "edit", Short: "This command will edit the project configuration", Long: `This command will edit the project configuration`, - Example: ` - # To enable the multigroup layout/support - kubebuilder edit --multigroup - - # To disable the multigroup layout/support - kubebuilder edit --multigroup=false`, - Run: func(cmd *cobra.Command, args []string) { - internal.DieIfNotConfigured() - - projectConfig, err := config.Load() - if err != nil { - log.Fatalf("failed to read the configuration file: %v", err) + Example: ` # Enable the multigroup layout + kubebuilder edit --multigroup + + # Disable the multigroup layout + kubebuilder edit --multigroup=false`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(editError{err}) } + }, + } - if opts.multigroup { - if !projectConfig.IsV2() { - log.Fatalf("kubebuilder multigroup is for project version: 2,"+ - " the version of this project is: %s \n", projectConfig.Version) - } + options.bindFlags(cmd) - // Set MultiGroup Option - projectConfig.MultiGroup = true - } + return cmd +} - err = projectConfig.Save() - if err != nil { - log.Fatalf("error updating project file with resource information : %v", err) - } - }, +var _ commandOptions = &editOptions{} + +type editOptions struct { + multigroup bool +} + +func (o *editOptions) bindFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.multigroup, "multigroup", false, "enable or disable multigroup layout") +} + +func (o *editOptions) loadConfig() (*config.Config, error) { + projectConfig, err := config.Load() + if os.IsNotExist(err) { + return nil, errors.New("unable to find configuration file, project must be initialized") + } + + return projectConfig, err +} + +func (o *editOptions) validate(c *config.Config) error { + if !c.IsV2() { + if c.MultiGroup { + return fmt.Errorf("multiple group support can't be enabled for version %s", c.Version) + } } - editProjectCmd.Flags().BoolVar(&opts.multigroup, "multigroup", false, - "if set as true, then the tool will generate the project files with multigroup layout") + return nil +} - return editProjectCmd +func (o *editOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { // nolint:unparam + return scaffold.NewEditScaffolder(c, o.multigroup), nil } -type editProjectCmdOptions struct { - multigroup bool +func (o *editOptions) postScaffold(_ *config.Config) error { + return nil } diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 00000000000..089b3d3cd34 --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,229 @@ +/* +Copyright 2017 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 main + +import ( + "bufio" + "errors" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/cmd/internal" + "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold" +) + +type initError struct { + err error +} + +func (e initError) Error() string { + return fmt.Sprintf("failed to initialize project: %v", e.err) +} + +func newInitCmd() *cobra.Command { + options := &initOptions{} + + cmd := &cobra.Command{ + Use: "init", + Short: "Initialize a new project", + Long: `Initialize a new project including vendor/ directory and Go package directories. + +Writes the following files: +- a boilerplate license file +- a PROJECT file with the project configuration +- a Makefile to build the project +- a go.mod with project dependencies +- a Kustomization.yaml for customizating manifests +- a Patch file for customizing image for manager manifests +- a Patch file for enabling prometheus metrics +- a cmd/manager/main.go to run + +project will prompt the user to run 'dep ensure' after writing the project files. +`, + Example: `# Scaffold a project using the apache2 license with "The Kubernetes authors" as owners +kubebuilder init --domain example.org --license apache2 --owner "The Kubernetes authors" +`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(initError{err}) + } + }, + } + + options.bindFlags(cmd) + + return cmd +} + +var _ commandOptions = &initOptions{} + +type initOptions struct { + config *config.Config + + // boilerplate options + license string + owner string + + // deprecated flags + depFlag *flag.Flag + depArgs []string + dep bool + + // flags + fetchDeps bool + skipGoVersionCheck bool +} + +func (o *initOptions) bindFlags(cmd *cobra.Command) { + cmd.Flags().BoolVar(&o.skipGoVersionCheck, "skip-go-version-check", + false, "if specified, skip checking the Go version") + + // dependency args + cmd.Flags().BoolVar(&o.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded") + + // deprecated dependency args + cmd.Flags().BoolVar(&o.dep, "dep", true, "if specified, determines whether dep will be used.") + o.depFlag = cmd.Flag("dep") + cmd.Flags().StringArrayVar(&o.depArgs, "depArgs", nil, "additional arguments for dep") + + if err := cmd.Flags().MarkDeprecated("dep", "use the fetch-deps flag instead"); err != nil { + log.Printf("error to mark dep flag as deprecated: %v", err) + } + if err := cmd.Flags().MarkDeprecated("depArgs", "will be removed with version 1 scaffolding"); err != nil { + log.Printf("error to mark dep flag as deprecated: %v", err) + } + + // boilerplate args + cmd.Flags().StringVar(&o.license, "license", "apache2", + "license to use to boilerplate, may be one of 'apache2', 'none'") + cmd.Flags().StringVar(&o.owner, "owner", "", "owner to add to the copyright") + + // project args + o.config = config.New(config.DefaultPath) + cmd.Flags().StringVar(&o.config.Repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+ + "defaults to the go package of the current working directory.") + cmd.Flags().StringVar(&o.config.Domain, "domain", "my.domain", "domain for groups") + cmd.Flags().StringVar(&o.config.Version, "project-version", config.DefaultVersion, "project version") +} + +func (o *initOptions) loadConfig() (*config.Config, error) { + _, err := config.Read() + if err == nil || os.IsExist(err) { + return nil, errors.New("already initialized") + } + + return o.config, nil +} + +func (o *initOptions) validate(c *config.Config) error { + // Requires go1.11+ + if !o.skipGoVersionCheck { + if err := internal.ValidateGoVersion(); err != nil { + return err + } + } + + // Check if the project name is a valid namespace according to k8s + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("error to get the current path: %v", err) + } + projectName := filepath.Base(dir) + if err := internal.IsDNS1123Label(strings.ToLower(projectName)); err != nil { + return fmt.Errorf("project name (%s) is invalid: %v", projectName, err) + } + + // Try to guess repository if flag is not set + if c.Repo == "" { + repoPath, err := internal.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) + } + c.Repo = repoPath + } + + // v1 only checks + if c.IsV1() { + // v1 is deprecated + internal.PrintV1DeprecationWarning() + + // Verify dep is installed + if _, err := exec.LookPath("dep"); err != nil { + return fmt.Errorf("dep is not installed: %v\n"+ + "Follow steps at: https://golang.github.io/dep/docs/installation.html", err) + } + } + + return nil +} + +func (o *initOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { // nolint:unparam + return scaffold.NewInitScaffolder(c, o.license, o.owner), nil +} + +func (o *initOptions) postScaffold(c *config.Config) error { + switch { + case c.IsV1(): + if !o.depFlag.Changed { + reader := bufio.NewReader(os.Stdin) + fmt.Println("Run `dep ensure` to fetch dependencies (Recommended) [y/n]?") + o.dep = internal.YesNo(reader) + } + if !o.dep { + fmt.Println("Skipping fetching dependencies.") + return nil + } + + err := internal.RunCmd("Fetching dependencies", "dep", append([]string{"ensure"}, o.depArgs...)...) + if err != nil { + return err + } + + case c.IsV2(): + // Ensure that we are pinning controller-runtime version + // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 + err := internal.RunCmd("Get controller runtime", "go", "get", + "sigs.k8s.io/controller-runtime@"+scaffold.ControllerRuntimeVersion) + if err != nil { + return err + } + + err = internal.RunCmd("Update go.mod", "go", "mod", "tidy") + if err != nil { + return err + } + + default: + return fmt.Errorf("unknown project version %v", c.Version) + } + + err := internal.RunCmd("Running make", "make") + if err != nil { + return err + } + + fmt.Println("Next: define a resource with:\n$ kubebuilder create api") + return nil +} diff --git a/cmd/init_project.go b/cmd/init_project.go deleted file mode 100644 index 6c6e4493e6f..00000000000 --- a/cmd/init_project.go +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright 2017 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 main - -import ( - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/spf13/cobra" - flag "github.com/spf13/pflag" - - "sigs.k8s.io/kubebuilder/cmd/internal" - "sigs.k8s.io/kubebuilder/cmd/util" - "sigs.k8s.io/kubebuilder/pkg/model/config" - "sigs.k8s.io/kubebuilder/pkg/scaffold" - "sigs.k8s.io/kubebuilder/pkg/scaffold/project" -) - -func newInitProjectCmd() *cobra.Command { - o := projectOptions{} - - initCmd := &cobra.Command{ - Use: "init", - Short: "Initialize a new project", - Long: `Initialize a new project including vendor/ directory and Go package directories. - -Writes the following files: -- a boilerplate license file -- a PROJECT file with the project configuration -- a Makefile to build the project -- a go.mod with project dependencies -- a Kustomization.yaml for customizating manifests -- a Patch file for customizing image for manager manifests -- a Patch file for enabling prometheus metrics -- a cmd/manager/main.go to run - -project will prompt the user to run 'dep ensure' after writing the project files. -`, - Example: `# Scaffold a project using the apache2 license with "The Kubernetes authors" as owners -kubebuilder init --domain example.org --license apache2 --owner "The Kubernetes authors" -`, - Run: func(cmd *cobra.Command, args []string) { - o.initializeProject() - }, - } - - o.bindCmdlineFlags(initCmd) - - return initCmd -} - -type projectOptions struct { - boilerplate project.Boilerplate - project project.Project - - // final result - scaffolder scaffold.ProjectScaffolder - - // deprecated flags - depFlag *flag.Flag - depArgs []string - - // flags - fetchDeps bool - skipGoVersionCheck bool - - // deprecated flags - dep bool -} - -func (o *projectOptions) bindCmdlineFlags(cmd *cobra.Command) { - - cmd.Flags().BoolVar(&o.skipGoVersionCheck, "skip-go-version-check", - false, "if specified, skip checking the Go version") - - // dependency args - cmd.Flags().BoolVar(&o.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded") - - // deprecated dependency args - cmd.Flags().BoolVar(&o.dep, "dep", true, "if specified, determines whether dep will be used.") - o.depFlag = cmd.Flag("dep") - cmd.Flags().StringArrayVar(&o.depArgs, "depArgs", nil, "Additional arguments for dep") - - if err := cmd.Flags().MarkDeprecated("dep", "use the fetch-deps flag instead"); err != nil { - log.Printf("error to mark dep flag as deprecated: %v", err) - } - if err := cmd.Flags().MarkDeprecated("depArgs", "will be removed with version 1 scaffolding"); err != nil { - log.Printf("error to mark dep flag as deprecated: %v", err) - } - - // boilerplate args - cmd.Flags().StringVar(&o.boilerplate.Path, "path", "", "path for boilerplate") - cmd.Flags().StringVar(&o.boilerplate.License, "license", "apache2", - "license to use to boilerplate. May be one of apache2,none") - cmd.Flags().StringVar(&o.boilerplate.Owner, "owner", "", "Owner to add to the copyright") - - // project args - cmd.Flags().StringVar(&o.project.Repo, "repo", "", - "name to use for go module, e.g. github.com/user/repo. "+ - "defaults to the go package of the current working directory.") - cmd.Flags().StringVar(&o.project.Domain, "domain", "my.domain", "domain for groups") - cmd.Flags().StringVar(&o.project.Version, "project-version", config.Version2, "project version") -} - -func (o *projectOptions) initializeProject() { - internal.DieIfConfigured() - - if err := o.validate(); err != nil { - log.Fatal(err) - } - - if o.project.IsV1() { - printV1DeprecationWarning() - } - - if err := o.scaffolder.Scaffold(); err != nil { - log.Fatalf("error scaffolding project: %v", err) - } - - if err := o.postScaffold(); err != nil { - log.Fatal(err) - } - - fmt.Printf("Next: Define a resource with:\n" + - "$ kubebuilder create api\n") -} - -func (o *projectOptions) validate() error { - if !o.skipGoVersionCheck { - if err := validateGoVersion(); err != nil { - return err - } - } - - // use directory name as prefix - dir, err := os.Getwd() - if err != nil { - return fmt.Errorf("error to get the current path: %v", err) - } - - // check if the name of th project pass is a valid name for k8s objects - // it will be used to create the namespace - projectName := filepath.Base(dir) - if err := util.IsValidName(strings.ToLower(projectName)); err != nil { - return fmt.Errorf("project name (%v) is invalid: (%v)", projectName, err) - } - - if o.project.Repo == "" { - repoPath, err := findCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) - } - o.project.Repo = repoPath - } - - switch { - case o.project.IsV1(): - var defEnsure *bool - if o.depFlag.Changed { - defEnsure = &o.dep - } - o.scaffolder = &scaffold.V1Project{ - Project: o.project, - Boilerplate: o.boilerplate, - - DepArgs: o.depArgs, - DefinitelyEnsure: defEnsure, - } - case o.project.IsV2(): - o.scaffolder = &scaffold.V2Project{ - Project: o.project, - Boilerplate: o.boilerplate, - } - default: - return fmt.Errorf("unknown project version %v", o.project.Version) - } - - if err := o.scaffolder.Validate(); err != nil { - return err - } - - return nil -} - -func validateGoVersion() error { - err := fetchAndCheckGoVersion() - if err != nil { - return fmt.Errorf("%s. You can skip this check using the --skip-go-version-check flag", err) - } - return nil -} - -func fetchAndCheckGoVersion() error { - cmd := exec.Command("go", "version") - out, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to retrieve 'go version': %v", string(out)) - } - - split := strings.Split(string(out), " ") - if len(split) < 3 { - return fmt.Errorf("found invalid Go version: %q", string(out)) - } - goVer := split[2] - if err := checkGoVersion(goVer); err != nil { - return fmt.Errorf("go version '%s' is incompatible because '%s'", goVer, err) - } - return nil -} - -func checkGoVersion(verStr string) error { - goVerRegex := `^go?([0-9]+)\.([0-9]+)([\.0-9A-Za-z\-]+)?$` - m := regexp.MustCompile(goVerRegex).FindStringSubmatch(verStr) - if m == nil { - return fmt.Errorf("invalid version string") - } - - major, err := strconv.Atoi(m[1]) - if err != nil { - return fmt.Errorf("error parsing major version '%s': %s", m[1], err) - } - - minor, err := strconv.Atoi(m[2]) - if err != nil { - return fmt.Errorf("error parsing minor version '%s': %s", m[2], err) - } - - if major < 1 || minor < 11 { - return fmt.Errorf("requires version >= 1.11") - } - - return nil -} - -func (o *projectOptions) postScaffold() error { - // preserve old "ask if not explicitly set" behavior for the `--dep` flag - // (asking is handled by the v1 scaffolder) - if (o.depFlag.Changed && !o.dep) || !o.fetchDeps { - fmt.Println("Skipping fetching dependencies.") - return nil - } - - ensured, err := o.scaffolder.EnsureDependencies() - if err != nil { - return err - } - - if !ensured { - return nil - } - - fmt.Println("Running make...") - c := exec.Command("make") // #nosec - c.Stderr = os.Stderr - c.Stdout = os.Stdout - fmt.Println(strings.Join(c.Args, " ")) - return c.Run() -} diff --git a/cmd/internal/config.go b/cmd/internal/config.go index 4b830d3909b..c2249b955b8 100644 --- a/cmd/internal/config.go +++ b/cmd/internal/config.go @@ -17,45 +17,33 @@ limitations under the License. package internal import ( + "fmt" "log" + "os" "sigs.k8s.io/kubebuilder/internal/config" ) -// isProjectConfigured checks for the existence of the configuration file -func isProjectConfigured() bool { - exists, err := config.Exists() - if err != nil { - log.Fatalf("Unable to check if configuration file exists: %v", err) - } - - return exists -} - -// DieIfConfigured exists if a configuration file was found -func DieIfConfigured() { - if isProjectConfigured() { - log.Fatalf("Project is already initialized") - } -} - -// DieIfNotConfigured exists if no configuration file was found -func DieIfNotConfigured() { - if !isProjectConfigured() { - log.Fatalf("Command must be run after `kubebuilder init ...`") - } -} +const ( + noticeColor = "\033[1;36m%s\033[0m" +) // ConfiguredAndV1 returns true if the project is already configured and it is v1 func ConfiguredAndV1() bool { - if !isProjectConfigured() { + projectConfig, err := config.Read() + + if os.IsNotExist(err) { return false } - projectConfig, err := config.Read() if err != nil { log.Fatalf("failed to read the configuration file: %v", err) } return projectConfig.IsV1() } + +func PrintV1DeprecationWarning() { + fmt.Printf(noticeColor, "[Deprecation Notice] The v1 projects are deprecated and will not be supported beyond "+ + "Feb 1, 2020.\nSee how to upgrade your project to v2: https://book.kubebuilder.io/migration/guide.html\n") +} diff --git a/cmd/internal/exec.go b/cmd/internal/exec.go new file mode 100644 index 00000000000..89affd5c241 --- /dev/null +++ b/cmd/internal/exec.go @@ -0,0 +1,32 @@ +/* +Copyright 2020 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 internal + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func RunCmd(msg, cmd string, args ...string) error { + c := exec.Command(cmd, args...) // #nolint:gosec + c.Stdout = os.Stdout + c.Stderr = os.Stderr + fmt.Println(msg + ":\n$ " + strings.Join(c.Args, " ")) + return c.Run() +} diff --git a/cmd/internal/go_version.go b/cmd/internal/go_version.go new file mode 100644 index 00000000000..aaa00723759 --- /dev/null +++ b/cmd/internal/go_version.go @@ -0,0 +1,75 @@ +/* +Copyright 2018 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 internal + +import ( + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" +) + +func ValidateGoVersion() error { + err := fetchAndCheckGoVersion() + if err != nil { + return fmt.Errorf("%s. You can skip this check using the --skip-go-version-check flag", err) + } + return nil +} + +func fetchAndCheckGoVersion() error { + cmd := exec.Command("go", "version") + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to retrieve 'go version': %v", string(out)) + } + + split := strings.Split(string(out), " ") + if len(split) < 3 { + return fmt.Errorf("found invalid Go version: %q", string(out)) + } + goVer := split[2] + if err := checkGoVersion(goVer); err != nil { + return fmt.Errorf("go version '%s' is incompatible because '%s'", goVer, err) + } + return nil +} + +func checkGoVersion(verStr string) error { + goVerRegex := `^go?([0-9]+)\.([0-9]+)([\.0-9A-Za-z\-]+)?$` + m := regexp.MustCompile(goVerRegex).FindStringSubmatch(verStr) + if m == nil { + return fmt.Errorf("invalid version string") + } + + major, err := strconv.Atoi(m[1]) + if err != nil { + return fmt.Errorf("error parsing major version '%s': %s", m[1], err) + } + + minor, err := strconv.Atoi(m[2]) + if err != nil { + return fmt.Errorf("error parsing minor version '%s': %s", m[2], err) + } + + if major < 1 || minor < 11 { + return fmt.Errorf("requires version >= 1.11") + } + + return nil +} diff --git a/cmd/go_version_test.go b/cmd/internal/go_version_test.go similarity index 97% rename from cmd/go_version_test.go rename to cmd/internal/go_version_test.go index f9d2ac5efe5..44a27511d86 100644 --- a/cmd/go_version_test.go +++ b/cmd/internal/go_version_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package internal import ( "testing" @@ -33,6 +33,7 @@ func TestCheckGoVersion(t *testing.T) { {"go1.11rc", false}, {"go1.11.1", false}, {"go1.12rc2", false}, + {"go1.13", false}, } for _, test := range tests { diff --git a/cmd/internal/repository.go b/cmd/internal/repository.go new file mode 100644 index 00000000000..c33f86afd05 --- /dev/null +++ b/cmd/internal/repository.go @@ -0,0 +1,93 @@ +/* +Copyright 2017 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 internal + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + + "golang.org/x/tools/go/packages" +) + +// module and goMod arg just enough of the output of `go mod edit -json` for our purposes +type goMod struct { + Module module +} +type module struct { + Path string +} + +// findGoModulePath finds the path of the current module, if present. +func findGoModulePath(forceModules bool) (string, error) { + cmd := exec.Command("go", "mod", "edit", "-json") + cmd.Env = append(cmd.Env, os.Environ()...) + if forceModules { + cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */) + } + out, err := cmd.Output() + if err != nil { + if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { + err = fmt.Errorf("%s", string(exitErr.Stderr)) + } + return "", err + } + mod := goMod{} + if err := json.Unmarshal(out, &mod); err != nil { + return "", err + } + return mod.Module.Path, nil +} + +// FindCurrentRepo attempts to determine the current repository +// though a combination of go/packages and `go mod` commands/tricks. +func FindCurrentRepo() (string, error) { + // easiest case: existing go module + path, err := findGoModulePath(false) + if err == nil { + return path, nil + } + + // next, check if we've got a package in the current directory + pkgCfg := &packages.Config{ + Mode: packages.NeedName, // name gives us path as well + } + pkgs, err := packages.Load(pkgCfg, ".") + // NB(directxman12): when go modules are off and we're outside GOPATH and + // we don't otherwise have a good guess packages.Load will fabricate a path + // that consists of `_/absolute/path/to/current/directory`. We shouldn't + // use that when it happens. + if err == nil && len(pkgs) > 0 && len(pkgs[0].PkgPath) > 0 && pkgs[0].PkgPath[0] != '_' { + return pkgs[0].PkgPath, nil + } + + // otherwise, try to get `go mod init` to guess for us -- it's pretty good + cmd := exec.Command("go", "mod", "init") + cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */) + if _, err := cmd.Output(); err != nil { + if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { + err = fmt.Errorf("%s", string(exitErr.Stderr)) + } + // give up, let the user figure it out + return "", fmt.Errorf("could not determine repository path from module data, "+ + "package data, or by initializing a module: %v", err) + } + defer os.Remove("go.mod") // clean up after ourselves + return findGoModulePath(true) +} diff --git a/cmd/util/stdin.go b/cmd/internal/stdin.go similarity index 98% rename from cmd/util/stdin.go rename to cmd/internal/stdin.go index 5b1f6be6971..9bb01acfdfb 100644 --- a/cmd/util/stdin.go +++ b/cmd/internal/stdin.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package internal import ( "bufio" diff --git a/cmd/util/validations.go b/cmd/internal/validations.go similarity index 60% rename from cmd/util/validations.go rename to cmd/internal/validations.go index 5bef558ff15..05d613f0eff 100644 --- a/cmd/util/validations.go +++ b/cmd/internal/validations.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package internal import ( "fmt" @@ -26,27 +26,26 @@ import ( // --------------------------------------- const ( - qnameCharFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" - // The value is 56 because it will be contact with "-system" = 63 - qualifiedNameMaxLength int = 56 + dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?" + dns1123LabelMaxLength int = 56 // = 63 - len("-system") ) -var qualifiedNameRegexp = regexp.MustCompile("^" + qnameCharFmt + "$") +var dns1123LabelRegexp = regexp.MustCompile("^" + dns1123LabelFmt + "$") -//IsValidName used to check the name of the project -func IsValidName(value string) []string { +//IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). +func IsDNS1123Label(value string) []string { var errs []string - if len(value) > qualifiedNameMaxLength { - errs = append(errs, MaxLenError(qualifiedNameMaxLength)) + if len(value) > dns1123LabelMaxLength { + errs = append(errs, maxLenError(dns1123LabelMaxLength)) } - if !qualifiedNameRegexp.MatchString(value) { - errs = append(errs, RegexError("invalid value for project name", qnameCharFmt)) + if !dns1123LabelRegexp.MatchString(value) { + errs = append(errs, regexError("invalid value for project name", dns1123LabelFmt)) } return errs } -// RegexError returns a string explanation of a regex validation failure. -func RegexError(msg string, fmt string, examples ...string) string { +// regexError returns a string explanation of a regex validation failure. +func regexError(msg string, fmt string, examples ...string) string { if len(examples) == 0 { return msg + " (regex used for validation is '" + fmt + "')" } @@ -61,8 +60,8 @@ func RegexError(msg string, fmt string, examples ...string) string { return msg } -// MaxLenError returns a string explanation of a "string too long" validation +// maxLenError returns a string explanation of a "string too long" validation // failure. -func MaxLenError(length int) string { +func maxLenError(length int) string { return fmt.Sprintf("must be no more than %d characters", length) } diff --git a/cmd/main.go b/cmd/main.go index 120800acb9d..f90a75ea848 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,170 +17,117 @@ limitations under the License. package main import ( - "encoding/json" - "fmt" "log" - "os" - "os/exec" "github.com/spf13/cobra" - "golang.org/x/tools/go/packages" "sigs.k8s.io/kubebuilder/cmd/internal" "sigs.k8s.io/kubebuilder/cmd/version" + "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold" ) -const ( - NoticeColor = "\033[1;36m%s\033[0m" -) - -// module and goMod arg just enough of the output of `go mod edit -json` for our purposes -type goMod struct { - Module module -} -type module struct { - Path string +// commandOptions represent the types used to implement the different commands +type commandOptions interface { + // bindFlags binds the command flags to the fields in the options struct + bindFlags(command *cobra.Command) + + // The following steps define a generic logic to follow when developing new commands. Some steps may be no-ops. + // - Step 1: load the config failing if expected but not found or if not expected but found + loadConfig() (*config.Config, error) + // - Step 2: verify that the command can be run (e.g., go version, project version, arguments, ...) + validate(*config.Config) error + // - Step 3: create the Scaffolder instance + scaffolder(*config.Config) (scaffold.Scaffolder, error) + // - Step 4: call the Scaffold method of the Scaffolder instance + // Doesn't need any method + // - Step 5: finish the command execution + postScaffold(*config.Config) error } -// findGoModulePath finds the path of the current module, if present. -func findGoModulePath(forceModules bool) (string, error) { - cmd := exec.Command("go", "mod", "edit", "-json") - cmd.Env = append(cmd.Env, os.Environ()...) - if forceModules { - cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */) - } - out, err := cmd.Output() +// run executes a command +func run(options commandOptions) error { + // Step 1: load config + projectConfig, err := options.loadConfig() if err != nil { - if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { - err = fmt.Errorf("%s", string(exitErr.Stderr)) - } - return "", err - } - mod := goMod{} - if err := json.Unmarshal(out, &mod); err != nil { - return "", err + return err } - return mod.Module.Path, nil -} -// findCurrentRepo attempts to determine the current repository -// though a combination of go/packages and `go mod` commands/tricks. -func findCurrentRepo() (string, error) { - // easiest case: existing go module - path, err := findGoModulePath(false) - if err == nil { - return path, nil + // Step 2: validate + if err := options.validate(projectConfig); err != nil { + return err } - // next, check if we've got a package in the current directory - pkgCfg := &packages.Config{ - Mode: packages.NeedName, // name gives us path as well + // Step 3: create scaffolder + scaffolder, err := options.scaffolder(projectConfig) + if err != nil { + return err } - pkgs, err := packages.Load(pkgCfg, ".") - // NB(directxman12): when go modules are off and we're outside GOPATH and - // we don't otherwise have a good guess packages.Load will fabricate a path - // that consists of `_/absolute/path/to/current/directory`. We shouldn't - // use that when it happens. - if err == nil && len(pkgs) > 0 && len(pkgs[0].PkgPath) > 0 && pkgs[0].PkgPath[0] != '_' { - return pkgs[0].PkgPath, nil + + // Step 4: scaffold + if err := scaffolder.Scaffold(); err != nil { + return err } - // otherwise, try to get `go mod init` to guess for us -- it's pretty good - cmd := exec.Command("go", "mod", "init") - cmd.Env = append(cmd.Env, os.Environ()...) - cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */) - if _, err := cmd.Output(); err != nil { - if exitErr, isExitErr := err.(*exec.ExitError); isExitErr { - err = fmt.Errorf("%s", string(exitErr.Stderr)) - } - // give up, let the user figure it out - return "", fmt.Errorf("could not determine repository path from module data, "+ - "package data, or by initializing a module: %v", err) + // Step 5: finish + if err := options.postScaffold(projectConfig); err != nil { + return err } - defer os.Remove("go.mod") // clean up after ourselves - return findGoModulePath(true) + + return nil } -func main() { - rootCmd := defaultCommand() +func buildCmdTree() *cobra.Command { + if internal.ConfiguredAndV1() { + internal.PrintV1DeprecationWarning() + } - rootCmd.AddCommand( - newInitProjectCmd(), - newEditProjectCmd(), - newCreateCmd(), - version.NewVersionCmd(), - ) + // kubebuilder + rootCmd := newRootCmd() + // kubebuilder alpha + alphaCmd := newAlphaCmd() + // kubebuilder alpha webhook (v1 only) if internal.ConfiguredAndV1() { - printV1DeprecationWarning() - - rootCmd.AddCommand( - newAlphaCommand(), - newVendorUpdateCmd(), - ) + alphaCmd.AddCommand(newWebhookCmd()) } - - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) + // Only add alpha group if it has subcommands + if alphaCmd.HasSubCommands() { + rootCmd.AddCommand(alphaCmd) } -} - -func defaultCommand() *cobra.Command { - return &cobra.Command{ - Use: "kubebuilder", - Short: "Development kit for building Kubernetes extensions and tools.", - Long: ` -Development kit for building Kubernetes extensions and tools. -Provides libraries and tools to create new projects, APIs and controllers. -Includes tools for packaging artifacts into an installer container. - -Typical project lifecycle: - -- initialize a project: - - kubebuilder init --domain example.com --license apache2 --owner "The Kubernetes authors" - -- create one or more a new resource APIs and add your code to them: - - kubebuilder create api --group --version --kind - -Create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only -scaffold a Controller for an existing Resource, select "n" for Resource. To only define -the schema for a Resource without writing a Controller, select "n" for Controller. - -After the scaffold is written, api will run make on the project. -`, - Example: ` - # Initialize your project - kubebuilder init --domain example.com --license apache2 --owner "The Kubernetes authors" - - # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate - kubebuilder create api --group ship --version v1beta1 --kind Frigate + // kubebuilder create + createCmd := newCreateCmd() + // kubebuilder create api + createCmd.AddCommand(newAPICmd()) + // kubebuilder create webhook (v2 only) + if !internal.ConfiguredAndV1() { + createCmd.AddCommand(newWebhookV2Cmd()) + } + // Only add create group if it has subcommands + if createCmd.HasSubCommands() { + rootCmd.AddCommand(createCmd) + } - # Edit the API Scheme - nano api/v1beta1/frigate_types.go + // kubebuilder edit + rootCmd.AddCommand(newEditCmd()) - # Edit the Controller - nano controllers/frigate_controller.go + // kubebuilder init + rootCmd.AddCommand(newInitCmd()) - # Install CRDs into the Kubernetes cluster using kubectl apply - make install + // kubebuilder update (v1 only) + if internal.ConfiguredAndV1() { + rootCmd.AddCommand(newUpdateCmd()) + } - # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config - make run -`, + // kubebuilder version + rootCmd.AddCommand(version.NewVersionCmd()) - Run: func(cmd *cobra.Command, args []string) { - if err := cmd.Help(); err != nil { - log.Fatalf("failed to call the help: %v", err) - } - }, - } + return rootCmd } -func printV1DeprecationWarning() { - fmt.Printf(NoticeColor, "[Deprecation Notice] The v1 projects are deprecated and will not be supported "+ - "beyond Feb 1, 2020.\nSee how to upgrade your project to v2:"+ - " https://book.kubebuilder.io/migration/guide.html\n") +func main() { + if err := buildCmdTree().Execute(); err != nil { + log.Fatal(err) + } } diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 00000000000..ae5436d760f --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,50 @@ +/* +Copyright 2017 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 main + +import ( + "github.com/spf13/cobra" +) + +func newRootCmd() *cobra.Command { + return &cobra.Command{ + Use: "kubebuilder", + Short: "Development kit for building Kubernetes extensions and tools.", + Long: ` +Development kit for building Kubernetes extensions and tools. + +Provides libraries and tools to create new projects, APIs and controllers. +Includes tools for packaging artifacts into an installer container. + +Typical project lifecycle: + +- initialize a project: + + kubebuilder init --domain example.com --license apache2 --owner "The Kubernetes authors" + +- create one or more a new resource APIs and add your code to them: + + kubebuilder create api --group --version --kind + +Create resource will prompt the user for if it should scaffold the Resource and / or Controller. To only +scaffold a Controller for an existing Resource, select "n" for Resource. To only define +the schema for a Resource without writing a Controller, select "n" for Controller. + +After the scaffold is written, api will run make on the project. +`, + } +} diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 00000000000..9ca35873590 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,90 @@ +/* +Copyright 2017 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 main + +import ( + "errors" + "fmt" + "log" + "os" + + "github.com/spf13/cobra" + + "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold" +) + +type updateError struct { + err error +} + +func (e updateError) Error() string { + return fmt.Sprintf("failed to update vendor dependencies: %v", e.err) +} + +func newUpdateCmd() *cobra.Command { + options := &updateOptions{} + + cmd := &cobra.Command{ + Use: "update", + Short: "Update vendor dependencies", + Long: `Update vendor dependencies`, + Example: `Update the vendor dependencies: +kubebuilder update +`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(updateError{err}) + } + }, + } + + options.bindFlags(cmd) + + return cmd +} + +var _ commandOptions = &updateOptions{} + +type updateOptions struct{} + +func (o *updateOptions) bindFlags(_ *cobra.Command) {} + +func (o *updateOptions) loadConfig() (*config.Config, error) { + projectConfig, err := config.Load() + if os.IsNotExist(err) { + return nil, errors.New("unable to find configuration file, project must be initialized") + } + + return projectConfig, err +} + +func (o *updateOptions) validate(c *config.Config) error { + if !c.IsV1() { + return fmt.Errorf("vendor was only used for v1, this project is %s", c.Version) + } + + return nil +} + +func (o *updateOptions) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { // nolint:unparam + return scaffold.NewUpdateScaffolder(&c.Config), nil +} + +func (o *updateOptions) postScaffold(_ *config.Config) error { + return nil +} diff --git a/cmd/vendor_update.go b/cmd/vendor_update.go deleted file mode 100644 index 55da634b908..00000000000 --- a/cmd/vendor_update.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2017 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 main - -import ( - "log" - - "github.com/spf13/cobra" - - "sigs.k8s.io/kubebuilder/cmd/internal" - "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" -) - -func newVendorUpdateCmd() *cobra.Command { - return &cobra.Command{ - Use: "update", - Short: "Update vendor dependencies", - Long: `Update vendor dependencies`, - Example: `Update the vendor dependencies: -kubebuilder update vendor -`, - Run: func(cmd *cobra.Command, args []string) { - internal.DieIfNotConfigured() - - universe, err := model.NewUniverse( - model.WithConfigFrom("PROJECT"), - model.WithoutBoilerplate, - ) - if err != nil { - log.Fatalf("error updating vendor dependencies: %v", err) - } - - err = (&scaffold.Scaffold{}).Execute( - universe, - input.Options{}, - &project.GopkgToml{}, - ) - if err != nil { - log.Fatalf("error updating vendor dependencies: %v", err) - } - }, - } -} diff --git a/cmd/webhook.go b/cmd/webhook.go new file mode 100644 index 00000000000..a217138a41e --- /dev/null +++ b/cmd/webhook.go @@ -0,0 +1,210 @@ +/* +Copyright 2018 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 main + +import ( + "errors" + "fmt" + "log" + "os" + + "github.com/spf13/cobra" + + "sigs.k8s.io/kubebuilder/cmd/internal" + "sigs.k8s.io/kubebuilder/internal/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold" + "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" +) + +type webhookError struct { + err error +} + +func (e webhookError) Error() string { + return fmt.Sprintf("failed to create webhook: %v", e.err) +} + +func newWebhookCmd() *cobra.Command { + options := &webhookV1Options{} + + cmd := &cobra.Command{ + Use: "webhook", + Short: "Scaffold a webhook server", + Long: `Scaffold a webhook server if there is no existing server. +Scaffolds webhook handlers based on group, version, kind and other user inputs. +This command is only available for v1 scaffolding project. +`, + Example: ` # Create webhook for CRD of group crew, version v1 and kind FirstMate. + # Set type to be mutating and operations to be create and update. + kubebuilder alpha webhook --group crew --version v1 --kind FirstMate --type=mutating --operations=create,update +`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(webhookError{err}) + } + }, + } + + options.bindFlags(cmd) + + return cmd +} + +var _ commandOptions = &webhookV1Options{} + +type webhookV1Options struct { + resource *resource.Resource + server string + webhookType string + operations []string + doMake bool +} + +func (o *webhookV1Options) bindFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.server, "server", "default", "name of the server") + cmd.Flags().StringVar(&o.webhookType, "type", "", "webhook type, e.g. mutating or validating") + cmd.Flags().StringSliceVar(&o.operations, "operations", []string{"create"}, + "the operations that the webhook will intercept, e.g. create, update, delete and connect") + + cmd.Flags().BoolVar(&o.doMake, "make", true, "if true, run make after generating files") + + o.resource = &resource.Resource{} + cmd.Flags().StringVar(&o.resource.Group, "group", "", "resource Group") + cmd.Flags().StringVar(&o.resource.Version, "version", "", "resource Version") + cmd.Flags().StringVar(&o.resource.Kind, "kind", "", "resource Kind") + cmd.Flags().StringVar(&o.resource.Resource, "resource", "", "resource Resource") +} + +func (o *webhookV1Options) loadConfig() (*config.Config, error) { + projectConfig, err := config.Load() + if os.IsNotExist(err) { + return nil, errors.New("unable to find configuration file, project must be initialized") + } + + return projectConfig, err +} + +func (o *webhookV1Options) validate(c *config.Config) error { + if !c.IsV1() { + return fmt.Errorf("webhook scaffolding is no longer alpha for version %s", c.Version) + } + + if err := o.resource.Validate(); err != nil { + return err + } + + return nil +} + +func (o *webhookV1Options) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { // nolint:unparam + return scaffold.NewV1WebhookScaffolder(&c.Config, o.resource, o.server, o.webhookType, o.operations), nil +} + +func (o *webhookV1Options) postScaffold(_ *config.Config) error { + if o.doMake { + err := internal.RunCmd("Running make", "make") + if err != nil { + return err + } + } + + return nil +} + +func newWebhookV2Cmd() *cobra.Command { + options := &webhookV2Options{} + + cmd := &cobra.Command{ + Use: "webhook", + Short: "Scaffold a webhook for an API resource.", + Long: `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, ` + + `validating and (or) conversion webhooks.`, + Example: ` # Create defaulting and validating webhooks for CRD of group crew, version v1 and kind FirstMate. + kubebuilder create webhook --group crew --version v1 --kind FirstMate --defaulting --programmatic-validation + + # Create conversion webhook for CRD of group crew, version v1 and kind FirstMate. + kubebuilder create webhook --group crew --version v1 --kind FirstMate --conversion +`, + Run: func(_ *cobra.Command, _ []string) { + if err := run(options); err != nil { + log.Fatal(webhookError{err}) + } + }, + } + + options.bindFlags(cmd) + + return cmd +} + +var _ commandOptions = &webhookV2Options{} + +type webhookV2Options struct { + resource *resource.Resource + defaulting bool + validation bool + conversion bool +} + +func (o *webhookV2Options) bindFlags(cmd *cobra.Command) { + o.resource = &resource.Resource{} + cmd.Flags().StringVar(&o.resource.Group, "group", "", "resource Group") + cmd.Flags().StringVar(&o.resource.Version, "version", "", "resource Version") + cmd.Flags().StringVar(&o.resource.Kind, "kind", "", "resource Kind") + cmd.Flags().StringVar(&o.resource.Resource, "resource", "", "resource Resource") + + cmd.Flags().BoolVar(&o.defaulting, "defaulting", false, + "if set, scaffold the defaulting webhook") + cmd.Flags().BoolVar(&o.validation, "programmatic-validation", false, + "if set, scaffold the validating webhook") + cmd.Flags().BoolVar(&o.conversion, "conversion", false, + "if set, scaffold the conversion webhook") +} + +func (o *webhookV2Options) loadConfig() (*config.Config, error) { + projectConfig, err := config.Load() + if os.IsNotExist(err) { + return nil, errors.New("unable to find configuration file, project must be initialized") + } + + return projectConfig, err +} + +func (o *webhookV2Options) validate(c *config.Config) error { + if c.IsV1() { + return fmt.Errorf("webhook scaffolding is alpha for version %s", c.Version) + } + + if err := o.resource.Validate(); err != nil { + return err + } + + if !o.defaulting && !o.validation && !o.conversion { + return errors.New("kubebuilder webhook requires at least one of" + + " --defaulting, --programmatic-validation and --conversion to be true") + } + + return nil +} + +func (o *webhookV2Options) scaffolder(c *config.Config) (scaffold.Scaffolder, error) { // nolint:unparam + return scaffold.NewV2WebhookScaffolder(&c.Config, o.resource, o.defaulting, o.validation, o.conversion), nil +} + +func (o *webhookV2Options) postScaffold(_ *config.Config) error { + return nil +} diff --git a/cmd/webhook_v1.go b/cmd/webhook_v1.go deleted file mode 100644 index 1cdcdcaab87..00000000000 --- a/cmd/webhook_v1.go +++ /dev/null @@ -1,136 +0,0 @@ -/* -Copyright 2018 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 main - -import ( - "fmt" - "log" - "os" - "os/exec" - - "github.com/spf13/cobra" - flag "github.com/spf13/pflag" - - "sigs.k8s.io/kubebuilder/cmd/internal" - "sigs.k8s.io/kubebuilder/internal/config" - "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/resource" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/webhook" -) - -func newWebhookCmd() *cobra.Command { - o := webhookOptions{} - - cmd := &cobra.Command{ - Use: "webhook", - Short: "Scaffold a webhook server", - Long: `Scaffold a webhook server if there is no existing server. -Scaffolds webhook handlers based on group, version, kind and other user inputs. -This command is only available for v1 scaffolding project. -`, - Example: ` # Create webhook for CRD of group crew, version v1 and kind FirstMate. - # Set type to be mutating and operations to be create and update. - kubebuilder alpha webhook --group crew --version v1 --kind FirstMate --type=mutating --operations=create,update -`, - Run: func(cmd *cobra.Command, args []string) { - internal.DieIfNotConfigured() - - projectConfig, err := config.Read() - if err != nil { - log.Fatalf("failed to read the configuration file: %v", err) - } - - if !projectConfig.IsV1() { - log.Fatalf("webhook scaffolding is not supported for this project version: %s \n", projectConfig.Version) - } - - if err := o.res.Validate(); err != nil { - log.Fatal(err) - } - - fmt.Println("Writing scaffold for you to edit...") - - universe, err := model.NewUniverse( - model.WithConfig(projectConfig), - // TODO: missing model.WithBoilerplate[From], needs boilerplate or path - model.WithResource(o.res, projectConfig), - ) - if err != nil { - log.Fatalf("error scaffolding webhook: %v", err) - } - - webhookConfig := webhook.Config{Server: o.server, Type: o.webhookType, Operations: o.operations} - - err = (&scaffold.Scaffold{}).Execute( - universe, - input.Options{}, - &manager.Webhook{}, - &webhook.AdmissionHandler{Resource: o.res, Config: webhookConfig}, - &webhook.AdmissionWebhookBuilder{Resource: o.res, Config: webhookConfig}, - &webhook.AdmissionWebhooks{Resource: o.res, Config: webhookConfig}, - &webhook.AddAdmissionWebhookBuilderHandler{Resource: o.res, Config: webhookConfig}, - &webhook.Server{Config: webhookConfig}, - &webhook.AddServer{Config: webhookConfig}, - ) - if err != nil { - log.Fatalf("error scaffolding webhook: %v", err) - } - - if o.doMake { - fmt.Println("Running make...") - cm := exec.Command("make") // #nosec - cm.Stderr = os.Stderr - cm.Stdout = os.Stdout - if err := cm.Run(); err != nil { - log.Fatal(err) - } - } - }, - } - cmd.Flags().StringVar(&o.server, "server", "default", - "name of the server") - cmd.Flags().StringVar(&o.webhookType, "type", "", - "webhook type, e.g. mutating or validating") - cmd.Flags().StringSliceVar(&o.operations, "operations", []string{"create"}, - "the operations that the webhook will intercept, e.g. create, update, delete and connect") - cmd.Flags().BoolVar(&o.doMake, "make", true, - "if true, run make after generating files") - o.res = gvkForFlags(cmd.Flags()) - return cmd -} - -// webhookOptions represents commandline options for scaffolding a webhook. -type webhookOptions struct { - res *resource.Resource - operations []string - server string - webhookType string - doMake bool -} - -// gvkForFlags registers flags for Resource fields and returns the Resource -func gvkForFlags(f *flag.FlagSet) *resource.Resource { - r := &resource.Resource{} - f.StringVar(&r.Group, "group", "", "resource Group") - f.StringVar(&r.Version, "version", "", "resource Version") - f.StringVar(&r.Kind, "kind", "", "resource Kind") - f.StringVar(&r.Resource, "resource", "", "resource Resource") - return r -} diff --git a/cmd/webhook_v2.go b/cmd/webhook_v2.go deleted file mode 100644 index bca4438666c..00000000000 --- a/cmd/webhook_v2.go +++ /dev/null @@ -1,145 +0,0 @@ -/* -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 main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - - "github.com/spf13/cobra" - - "sigs.k8s.io/kubebuilder/cmd/internal" - "sigs.k8s.io/kubebuilder/internal/config" - "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/resource" - scaffoldv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/webhook" -) - -func newWebhookV2Cmd() *cobra.Command { - o := webhookV2Options{} - - cmd := &cobra.Command{ - Use: "webhook", - Short: "Scaffold a webhook for an API resource.", - Long: `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, ` + - `validating and (or) conversion webhooks.`, - Example: ` # Create defaulting and validating webhooks for CRD of group crew, version v1 and kind FirstMate. - kubebuilder create webhook --group crew --version v1 --kind FirstMate --defaulting --programmatic-validation - - # Create conversion webhook for CRD of group crew, version v1 and kind FirstMate. - kubebuilder create webhook --group crew --version v1 --kind FirstMate --conversion -`, - Run: func(cmd *cobra.Command, args []string) { - internal.DieIfNotConfigured() - - projectConfig, err := config.Read() - if err != nil { - log.Fatalf("failed to read the configuration file: %v", err) - } - - if !projectConfig.IsV2() { - fmt.Printf("kubebuilder webhook is for project version: 2,"+ - " the version of this project is: %s \n", projectConfig.Version) - os.Exit(1) - } - - if !o.defaulting && !o.validation && !o.conversion { - fmt.Printf("kubebuilder webhook requires at least one of" + - " --defaulting, --programmatic-validation and --conversion to be true") - os.Exit(1) - } - - if err := o.res.Validate(); err != nil { - log.Fatal(err) - } - - fmt.Println("Writing scaffold for you to edit...") - - if projectConfig.MultiGroup { - fmt.Println(filepath.Join("apis", o.res.Group, o.res.Version, - fmt.Sprintf("%s_webhook.go", strings.ToLower(o.res.Kind)))) - } else { - fmt.Println(filepath.Join("api", o.res.Version, - fmt.Sprintf("%s_webhook.go", strings.ToLower(o.res.Kind)))) - } - - if o.conversion { - fmt.Println(`Webhook server has been set up for you. -You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) - } - - universe, err := model.NewUniverse( - model.WithConfig(projectConfig), - // TODO: missing model.WithBoilerplate[From], needs boilerplate or path - model.WithResource(o.res, projectConfig), - ) - if err != nil { - log.Fatalf("error scaffolding webhook: %v", err) - } - - err = (&scaffold.Scaffold{}).Execute( - universe, - input.Options{}, - &webhook.Webhook{ - Resource: o.res, - Defaulting: o.defaulting, - Validating: o.validation, - }, - ) - if err != nil { - log.Fatalf("error scaffolding webhook: %v", err) - } - - err = (&scaffoldv2.Main{}).Update( - &scaffoldv2.MainUpdateOptions{ - Config: projectConfig, - WireResource: false, - WireController: false, - WireWebhook: true, - Resource: o.res, - }) - if err != nil { - fmt.Printf("error updating main.go: %v", err) - os.Exit(1) - } - - }, - } - o.res = gvkForFlags(cmd.Flags()) - cmd.Flags().BoolVar(&o.defaulting, "defaulting", false, - "if set, scaffold the defaulting webhook") - cmd.Flags().BoolVar(&o.validation, "programmatic-validation", false, - "if set, scaffold the validating webhook") - cmd.Flags().BoolVar(&o.conversion, "conversion", false, - "if set, scaffold the conversion webhook") - - return cmd -} - -// webhookOptions represents commandline options for scaffolding a webhook. -type webhookV2Options struct { - res *resource.Resource - defaulting bool - validation bool - conversion bool -} diff --git a/internal/config/config.go b/internal/config/config.go index d7ff3c7155e..f231afe6692 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,8 +26,13 @@ import ( "sigs.k8s.io/yaml" ) -// Default path for the configuration file -const DefaultPath = "PROJECT" +const ( + // Default path for the configuration file + DefaultPath = "PROJECT" + + // Default version if flag not provided + DefaultVersion = config.Version2 +) func exists(path string) (bool, error) { // Look up the file @@ -45,12 +50,6 @@ func exists(path string) (bool, error) { return false, err } -// Exists verifies that the configuration file exists in the default path -// TODO: consider removing this verification in favor of using Load and checking the error -func Exists() (bool, error) { - return exists(DefaultPath) -} - func readFrom(path string) (c config.Config, err error) { // Read the file in, err := ioutil.ReadFile(path) // nolint: gosec @@ -96,7 +95,6 @@ type Config struct { } // New creates a new configuration that will be stored at the provided path -// TODO: this method should be used during the initialization command, unused for now func New(path string) *Config { return &Config{ Config: config.Config{ @@ -146,7 +144,7 @@ func (c Config) Save() error { } // Write the marshalled configuration - err = ioutil.WriteFile(c.path, content, os.ModePerm) + err = ioutil.WriteFile(c.path, content, 0600) if err != nil { return saveError{fmt.Errorf("failed to save configuration to %s: %v", c.path, err)} } @@ -154,6 +152,10 @@ func (c Config) Save() error { return nil } +func (c Config) Path() string { + return c.path +} + type saveError struct { err error } diff --git a/pkg/model/universe.go b/pkg/model/universe.go index 1549cc4dd6d..b80993b2e8a 100644 --- a/pkg/model/universe.go +++ b/pkg/model/universe.go @@ -22,7 +22,6 @@ import ( "github.com/gobuffalo/flect" - internalconfig "sigs.k8s.io/kubebuilder/internal/config" "sigs.k8s.io/kubebuilder/pkg/model/config" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" "sigs.k8s.io/kubebuilder/pkg/scaffold/util" @@ -60,19 +59,6 @@ func NewUniverse(options ...UniverseOption) (*Universe, error) { // UniverseOption configure Universe type UniverseOption func(*Universe) error -// WithConfigFrom loads the project configuration from the provided path -func WithConfigFrom(path string) UniverseOption { - return func(universe *Universe) error { - projectConfig, err := internalconfig.ReadFrom(path) - if err != nil { - return err - } - - universe.Config = projectConfig - return nil - } -} - // WithConfig stores the already loaded project configuration func WithConfig(projectConfig *config.Config) UniverseOption { return func(universe *Universe) error { diff --git a/pkg/scaffold/api.go b/pkg/scaffold/api.go index 58beb431b81..cbfc6033625 100644 --- a/pkg/scaffold/api.go +++ b/pkg/scaffold/api.go @@ -25,111 +25,86 @@ import ( "sigs.k8s.io/kubebuilder/pkg/model" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/controller" + controllerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/controller" crdv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/crd" scaffoldv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" controllerv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/controller" crdv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/crd" ) -// API contains configuration for generating scaffolding for Go type +// apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. -type API struct { - // Plugins is the list of plugins we should allow to transform our generated scaffolding - Plugins []Plugin - - Resource *resource.Resource - - config *config.Config - - // DoResource indicates whether to scaffold API Resource or not - DoResource bool - - // DoController indicates whether to scaffold controller files or not - DoController bool - - // Force indicates that the resource should be created even if it already exists. - Force bool -} - -// Validate validates whether API scaffold has correct bits to generate -// scaffolding for API. -func (api *API) Validate() error { - if err := api.setDefaults(); err != nil { - return err - } - if err := api.Resource.Validate(); err != nil { - return err - } - - if api.config.HasResource(api.Resource) && !api.Force { - return fmt.Errorf("API resource already exists") - } - - return nil +type apiScaffolder struct { + config *config.Config + resource *resource.Resource + // plugins is the list of plugins we should allow to transform our generated scaffolding + plugins []Plugin + // doResource indicates whether to scaffold API Resource or not + doResource bool + // doController indicates whether to scaffold controller files or not + doController bool } -func (api *API) setDefaults() (err error) { - if api.config == nil { - api.config, err = config.Load() - if err != nil { - return - } +func NewAPIScaffolder( + config *config.Config, + res *resource.Resource, + doResource, doController bool, + plugins []Plugin, +) Scaffolder { + return &apiScaffolder{ + plugins: plugins, + resource: res, + config: config, + doResource: doResource, + doController: doController, } - - return } -func (api *API) Scaffold() error { - if err := api.setDefaults(); err != nil { - return err - } +func (s *apiScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") switch { - case api.config.IsV1(): - return api.scaffoldV1() - case api.config.IsV2(): - return api.scaffoldV2() + case s.config.IsV1(): + return s.scaffoldV1() + case s.config.IsV2(): + return s.scaffoldV2() default: - return fmt.Errorf("unknown project version %v", api.config.Version) + return fmt.Errorf("unknown project version %v", s.config.Version) } } -func (api *API) buildUniverse(resource *resource.Resource) (*model.Universe, error) { +func (s *apiScaffolder) buildUniverse() (*model.Universe, error) { return model.NewUniverse( - model.WithConfig(&api.config.Config), + model.WithConfig(&s.config.Config), // TODO: missing model.WithBoilerplate[From], needs boilerplate or path - model.WithResource(resource, &api.config.Config), + model.WithResource(s.resource, &s.config.Config), ) } -func (api *API) scaffoldV1() error { - r := api.Resource +func (s *apiScaffolder) scaffoldV1() error { + if s.doResource { + fmt.Println(filepath.Join("pkg", "apis", s.resource.Group, s.resource.Version, + fmt.Sprintf("%s_types.go", strings.ToLower(s.resource.Kind)))) + fmt.Println(filepath.Join("pkg", "apis", s.resource.Group, s.resource.Version, + fmt.Sprintf("%s_types_test.go", strings.ToLower(s.resource.Kind)))) - if api.DoResource { - fmt.Println(filepath.Join("pkg", "apis", r.Group, r.Version, - fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind)))) - fmt.Println(filepath.Join("pkg", "apis", r.Group, r.Version, - fmt.Sprintf("%s_types_test.go", strings.ToLower(r.Kind)))) - - universe, err := api.buildUniverse(r) + universe, err := s.buildUniverse() if err != nil { return fmt.Errorf("error building API scaffold: %v", err) } - err = (&Scaffold{}).Execute( + if err := (&Scaffold{}).Execute( universe, input.Options{}, - &crdv1.Register{Resource: r}, - &crdv1.Types{Resource: r}, - &crdv1.VersionSuiteTest{Resource: r}, - &crdv1.TypesTest{Resource: r}, - &crdv1.Doc{Resource: r}, - &crdv1.Group{Resource: r}, - &crdv1.AddToScheme{Resource: r}, - &crdv1.CRDSample{Resource: r}, - ) - if err != nil { + &crdv1.Register{Resource: s.resource}, + &crdv1.Types{Resource: s.resource}, + &crdv1.VersionSuiteTest{Resource: s.resource}, + &crdv1.TypesTest{Resource: s.resource}, + &crdv1.Doc{Resource: s.resource}, + &crdv1.Group{Resource: s.resource}, + &crdv1.AddToScheme{Resource: s.resource}, + &crdv1.CRDSample{Resource: s.resource}, + ); err != nil { return fmt.Errorf("error scaffolding APIs: %v", err) } } else { @@ -137,29 +112,28 @@ func (api *API) scaffoldV1() error { // because this could result in a fork-bomb of k8s resources where watching a // deployment, replicaset etc. results in generating deployment which // end up generating replicaset, pod etc recursively. - r.CreateExampleReconcileBody = false + s.resource.CreateExampleReconcileBody = false } - if api.DoController { - fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(r.Kind), - fmt.Sprintf("%s_controller.go", strings.ToLower(r.Kind)))) - fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(r.Kind), - fmt.Sprintf("%s_controller_test.go", strings.ToLower(r.Kind)))) + if s.doController { + fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(s.resource.Kind), + fmt.Sprintf("%s_controller.go", strings.ToLower(s.resource.Kind)))) + fmt.Println(filepath.Join("pkg", "controller", strings.ToLower(s.resource.Kind), + fmt.Sprintf("%s_controller_test.go", strings.ToLower(s.resource.Kind)))) - universe, err := api.buildUniverse(r) + universe, err := s.buildUniverse() if err != nil { return fmt.Errorf("error building controller scaffold: %v", err) } - err = (&Scaffold{}).Execute( + if err := (&Scaffold{}).Execute( universe, input.Options{}, - &controller.Controller{Resource: r}, - &controller.AddController{Resource: r}, - &controller.Test{Resource: r}, - &controller.SuiteTest{Resource: r}, - ) - if err != nil { + &controllerv1.Controller{Resource: s.resource}, + &controllerv1.AddController{Resource: s.resource}, + &controllerv1.Test{Resource: s.resource}, + &controllerv1.SuiteTest{Resource: s.resource}, + ); err != nil { return fmt.Errorf("error scaffolding controller: %v", err) } } @@ -167,73 +141,60 @@ func (api *API) scaffoldV1() error { return nil } -func (api *API) scaffoldV2() error { - r := api.Resource - - if api.DoResource { - if err := api.validateResourceGroup(r); err != nil { - return err - } - +func (s *apiScaffolder) scaffoldV2() error { + if s.doResource { // Only save the resource in the config file if it didn't exist - if api.config.AddResource(api.Resource) { - if err := api.config.Save(); err != nil { + if s.config.AddResource(s.resource) { + if err := s.config.Save(); err != nil { return fmt.Errorf("error updating project file with resource information : %v", err) } } var path string - if api.config.MultiGroup { - path = filepath.Join("apis", r.Group, r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) + if s.config.MultiGroup { + path = filepath.Join("apis", s.resource.Group, s.resource.Version, + fmt.Sprintf("%s_types.go", strings.ToLower(s.resource.Kind))) } else { - path = filepath.Join("api", r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) + path = filepath.Join("api", s.resource.Version, + fmt.Sprintf("%s_types.go", strings.ToLower(s.resource.Kind))) } fmt.Println(path) - scaffold := &Scaffold{ - Plugins: api.Plugins, - } - - universe, err := api.buildUniverse(r) + universe, err := s.buildUniverse() if err != nil { return fmt.Errorf("error building API scaffold: %v", err) } - files := []input.File{ - &scaffoldv2.Types{ - Input: input.Input{ - Path: path, - }, - Resource: r}, - &scaffoldv2.Group{Resource: r}, - &scaffoldv2.CRDSample{Resource: r}, - &scaffoldv2.CRDEditorRole{Resource: r}, - &scaffoldv2.CRDViewerRole{Resource: r}, - &crdv2.EnableWebhookPatch{Resource: r}, - &crdv2.EnableCAInjectionPatch{Resource: r}, - } - - if err = scaffold.Execute(universe, input.Options{}, files...); err != nil { + if err := (&Scaffold{Plugins: s.plugins}).Execute( + universe, + input.Options{}, + &scaffoldv2.Types{Input: input.Input{Path: path}, Resource: s.resource}, + &scaffoldv2.Group{Resource: s.resource}, + &scaffoldv2.CRDSample{Resource: s.resource}, + &scaffoldv2.CRDEditorRole{Resource: s.resource}, + &scaffoldv2.CRDViewerRole{Resource: s.resource}, + &crdv2.EnableWebhookPatch{Resource: s.resource}, + &crdv2.EnableCAInjectionPatch{Resource: s.resource}, + ); err != nil { return fmt.Errorf("error scaffolding APIs: %v", err) } - universe, err = api.buildUniverse(r) + universe, err = s.buildUniverse() if err != nil { return fmt.Errorf("error building kustomization scaffold: %v", err) } - crdKustomization := &crdv2.Kustomization{Resource: r} - err = (&Scaffold{}).Execute( + kustomizationFile := &crdv2.Kustomization{Resource: s.resource} + if err := (&Scaffold{}).Execute( universe, input.Options{}, - crdKustomization, + kustomizationFile, &crdv2.KustomizeConfig{}, - ) - if err != nil { + ); err != nil { return fmt.Errorf("error scaffolding kustomization: %v", err) } - if err := crdKustomization.Update(); err != nil { + if err := kustomizationFile.Update(); err != nil { return fmt.Errorf("error updating kustomization.yaml: %v", err) } @@ -242,78 +203,48 @@ func (api *API) scaffoldV2() error { // because this could result in a fork-bomb of k8s resources where watching a // deployment, replicaset etc. results in generating deployment which // end up generating replicaset, pod etc recursively. - r.CreateExampleReconcileBody = false + s.resource.CreateExampleReconcileBody = false } - if api.DoController { - if api.config.MultiGroup { - fmt.Println(filepath.Join("controllers", fmt.Sprintf("%s/%s_controller.go", r.Group, strings.ToLower(r.Kind)))) + if s.doController { + if s.config.MultiGroup { + fmt.Println(filepath.Join("controllers", s.resource.Group, + fmt.Sprintf("%s_controller.go", strings.ToLower(s.resource.Kind)))) } else { - fmt.Println(filepath.Join("controllers", fmt.Sprintf("%s_controller.go", strings.ToLower(r.Kind)))) + fmt.Println(filepath.Join("controllers", + fmt.Sprintf("%s_controller.go", strings.ToLower(s.resource.Kind)))) } - scaffold := &Scaffold{ - Plugins: api.Plugins, - } - - universe, err := api.buildUniverse(r) + universe, err := s.buildUniverse() if err != nil { return fmt.Errorf("error building controller scaffold: %v", err) } - testsuiteScaffolder := &controllerv2.SuiteTest{Resource: r} - err = scaffold.Execute( + suiteTestFile := &controllerv2.SuiteTest{Resource: s.resource} + if err := (&Scaffold{Plugins: s.plugins}).Execute( universe, input.Options{}, - testsuiteScaffolder, - &controllerv2.Controller{Resource: r}, - ) - if err != nil { + suiteTestFile, + &controllerv2.Controller{Resource: s.resource}, + ); err != nil { return fmt.Errorf("error scaffolding controller: %v", err) } - err = testsuiteScaffolder.Update() - if err != nil { + if err := suiteTestFile.Update(); err != nil { return fmt.Errorf("error updating suite_test.go under controllers pkg: %v", err) } } - err := (&scaffoldv2.Main{}).Update( + if err := (&scaffoldv2.Main{}).Update( &scaffoldv2.MainUpdateOptions{ - Config: &api.config.Config, - WireResource: api.DoResource, - WireController: api.DoController, - Resource: r, - }) - if err != nil { + Config: &s.config.Config, + WireResource: s.doResource, + WireController: s.doController, + Resource: s.resource, + }, + ); err != nil { return fmt.Errorf("error updating main.go: %v", err) } return nil } - -// isGroupAllowed will check if the group is == the group used before -// and not allow new groups if the project is not enabled to use multigroup layout -func (api *API) isGroupAllowed(r *resource.Resource) bool { - if api.config.MultiGroup { - return true - } - for _, existingGroup := range api.config.ResourceGroups() { - if !strings.EqualFold(r.Group, existingGroup) { - return false - } - } - return true -} - -// validateResourceGroup will return an error if the group cannot be created -func (api *API) validateResourceGroup(r *resource.Resource) error { - if api.config.HasResource(api.Resource) && !api.Force { - return fmt.Errorf("group '%s', version '%s' and kind '%s' already exists", r.Group, r.Version, r.Kind) - } - if !api.isGroupAllowed(r) { - return fmt.Errorf("group '%s' is not same as existing group."+ - " Multiple groups are not enabled in this project. To enable, use the multigroup command", r.Group) - } - return nil -} diff --git a/pkg/scaffold/edit.go b/pkg/scaffold/edit.go new file mode 100644 index 00000000000..e15a7f82db7 --- /dev/null +++ b/pkg/scaffold/edit.go @@ -0,0 +1,39 @@ +/* +Copyright 2020 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 scaffold + +import ( + "sigs.k8s.io/kubebuilder/internal/config" +) + +type editScaffolder struct { + config *config.Config + multigroup bool +} + +func NewEditScaffolder(config *config.Config, multigroup bool) Scaffolder { + return &editScaffolder{ + config: config, + multigroup: multigroup, + } +} + +func (s *editScaffolder) Scaffold() error { + s.config.MultiGroup = s.multigroup + + return s.config.Save() +} diff --git a/pkg/scaffold/init.go b/pkg/scaffold/init.go new file mode 100644 index 00000000000..34f67e70a78 --- /dev/null +++ b/pkg/scaffold/init.go @@ -0,0 +1,185 @@ +/* +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 scaffold + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/internal/config" + "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" + managerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" + metricsauthv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/metricsauth" + scaffoldv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" + certmanagerv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/certmanager" + managerv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/manager" + metricsauthv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/metricsauth" + prometheusv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/prometheus" + webhookv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/webhook" +) + +const ( + // controller runtime version to be used in the project + ControllerRuntimeVersion = "v0.4.0" + // ControllerTools version to be used in the project + ControllerToolsVersion = "v0.2.4" + + ImageName = "controller:latest" +) + +type initScaffolder struct { + config *config.Config + boilerplatePath string + license string + owner string +} + +func NewInitScaffolder(config *config.Config, license, owner string) Scaffolder { + return &initScaffolder{ + config: config, + boilerplatePath: filepath.Join("hack", "boilerplate.go.txt"), + license: license, + owner: owner, + } +} + +func (s *initScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + if err := s.config.Save(); err != nil { + return err + } + + universe, err := model.NewUniverse( + model.WithConfig(&s.config.Config), + model.WithoutBoilerplate, + ) + if err != nil { + return fmt.Errorf("error initializing project: %v", err) + } + + if err := (&Scaffold{BoilerplateOptional: true}).Execute( + universe, + input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, + &project.Boilerplate{ + Input: input.Input{Path: s.boilerplatePath}, + License: s.license, + Owner: s.owner, + }, + ); err != nil { + return err + } + + universe, err = model.NewUniverse( + model.WithConfig(&s.config.Config), + model.WithBoilerplateFrom(s.boilerplatePath), + ) + if err != nil { + return fmt.Errorf("error initializing project: %v", err) + } + + if err := (&Scaffold{}).Execute( + universe, + input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, + &project.GitIgnore{}, + &project.AuthProxyRole{}, + &project.AuthProxyRoleBinding{}, + ); err != nil { + return err + } + + switch { + case s.config.IsV1(): + return s.scaffoldV1() + case s.config.IsV2(): + return s.scaffoldV2() + default: + return fmt.Errorf("unknown project version %v", s.config.Version) + } +} + +func (s *initScaffolder) scaffoldV1() error { + universe, err := model.NewUniverse( + model.WithConfig(&s.config.Config), + model.WithBoilerplateFrom(s.boilerplatePath), + ) + if err != nil { + return fmt.Errorf("error initializing project: %v", err) + } + + return (&Scaffold{}).Execute( + universe, + input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, + &project.KustomizeRBAC{}, + &scaffoldv1.KustomizeImagePatch{}, + &metricsauthv1.KustomizePrometheusMetricsPatch{}, + &metricsauthv1.KustomizeAuthProxyPatch{}, + &scaffoldv1.AuthProxyService{}, + &managerv1.Config{Image: ImageName}, + &project.Makefile{Image: ImageName}, + &project.GopkgToml{}, + &managerv1.Dockerfile{}, + &project.Kustomize{}, + &project.KustomizeManager{}, + &managerv1.APIs{}, + &managerv1.Controller{}, + &managerv1.Webhook{}, + &managerv1.Cmd{}, + ) +} + +func (s *initScaffolder) scaffoldV2() error { + universe, err := model.NewUniverse( + model.WithConfig(&s.config.Config), + model.WithBoilerplateFrom(s.boilerplatePath), + ) + if err != nil { + return fmt.Errorf("error initializing project: %v", err) + } + + return (&Scaffold{}).Execute( + universe, + input.Options{ProjectPath: s.config.Path(), BoilerplatePath: s.boilerplatePath}, + &metricsauthv2.AuthProxyPatch{}, + &metricsauthv2.AuthProxyService{}, + &metricsauthv2.ClientClusterRole{}, + &managerv2.Config{Image: ImageName}, + &scaffoldv2.Main{}, + &scaffoldv2.GoMod{ControllerRuntimeVersion: ControllerRuntimeVersion}, + &scaffoldv2.Makefile{Image: ImageName, ControllerToolsVersion: ControllerToolsVersion}, + &scaffoldv2.Dockerfile{}, + &scaffoldv2.Kustomize{}, + &scaffoldv2.ManagerWebhookPatch{}, + &scaffoldv2.ManagerRoleBinding{}, + &scaffoldv2.LeaderElectionRole{}, + &scaffoldv2.LeaderElectionRoleBinding{}, + &scaffoldv2.KustomizeRBAC{}, + &managerv2.Kustomization{}, + &webhookv2.Kustomization{}, + &webhookv2.KustomizeConfigWebhook{}, + &webhookv2.Service{}, + &webhookv2.InjectCAPatch{}, + &prometheusv2.Kustomization{}, + &prometheusv2.ServiceMonitor{}, + &certmanagerv2.CertManager{}, + &certmanagerv2.Kustomization{}, + &certmanagerv2.KustomizeConfig{}, + ) +} diff --git a/pkg/scaffold/interface.go b/pkg/scaffold/interface.go new file mode 100644 index 00000000000..9dabb5a2cd8 --- /dev/null +++ b/pkg/scaffold/interface.go @@ -0,0 +1,21 @@ +/* +Copyright 2020 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 scaffold + +type Scaffolder interface { + Scaffold() error +} diff --git a/pkg/scaffold/project.go b/pkg/scaffold/project.go deleted file mode 100644 index 678c35fde4c..00000000000 --- a/pkg/scaffold/project.go +++ /dev/null @@ -1,270 +0,0 @@ -/* -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 scaffold - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "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" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" - metricsauthv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/metricsauth" - scaffoldv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/certmanager" - managerv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/manager" - metricsauthv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/metricsauth" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/prometheus" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/webhook" -) - -const ( - // controller runtime version to be used in the project - controllerRuntimeVersion = "v0.4.0" - // ControllerTools version to be used in the project - controllerToolsVersion = "v0.2.4" -) - -type ProjectScaffolder interface { - EnsureDependencies() (bool, error) - Scaffold() error - Validate() error -} - -type V1Project struct { - Project project.Project - Boilerplate project.Boilerplate - - DepArgs []string - DefinitelyEnsure *bool -} - -func (p *V1Project) Validate() error { - _, err := exec.LookPath("dep") - if err != nil { - return fmt.Errorf("dep is not installed (%v). "+ - "Follow steps at: https://golang.github.io/dep/docs/installation.html", err) - } - return nil -} - -func (p *V1Project) EnsureDependencies() (bool, error) { - if p.DefinitelyEnsure == nil { - reader := bufio.NewReader(os.Stdin) - fmt.Println("Run `dep ensure` to fetch dependencies (Recommended) [y/n]?") - if !util.YesNo(reader) { - return false, nil - } - } else if !*p.DefinitelyEnsure { - return false, nil - } - - c := exec.Command("dep", "ensure") // #nosec - c.Args = append(c.Args, p.DepArgs...) - c.Stderr = os.Stderr - c.Stdout = os.Stdout - fmt.Println(strings.Join(c.Args, " ")) - return true, c.Run() -} - -func (p *V1Project) Scaffold() error { - s := &Scaffold{ - BoilerplateOptional: true, - ConfigOptional: true, - } - - universe, err := model.NewUniverse( - model.WithConfig(&p.Project.Config), - model.WithoutBoilerplate, - ) - if err != nil { - return fmt.Errorf("error initializing project: %v", err) - } - - projectInput, err := p.Project.GetInput() - if err != nil { - return err - } - - bpInput, err := p.Boilerplate.GetInput() - if err != nil { - return err - } - - err = s.Execute( - universe, - input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, - &p.Project, - &p.Boilerplate, - ) - if err != nil { - return err - } - - // default controller manager image name - imgName := "controller:latest" - - s = &Scaffold{} - - universe, err = model.NewUniverse( - model.WithConfig(&p.Project.Config), - model.WithBoilerplateFrom(p.Boilerplate.Path), - ) - if err != nil { - return fmt.Errorf("error initializing project: %v", err) - } - - return s.Execute( - universe, - input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, - &project.GitIgnore{}, - &project.KustomizeRBAC{}, - &scaffoldv1.KustomizeImagePatch{}, - &metricsauthv1.KustomizePrometheusMetricsPatch{}, - &metricsauthv1.KustomizeAuthProxyPatch{}, - &scaffoldv1.AuthProxyService{}, - &project.AuthProxyRole{}, - &project.AuthProxyRoleBinding{}, - &manager.Config{Image: imgName}, - &project.Makefile{Image: imgName}, - &project.GopkgToml{}, - &manager.Dockerfile{}, - &project.Kustomize{}, - &project.KustomizeManager{}, - &manager.APIs{}, - &manager.Controller{}, - &manager.Webhook{}, - &manager.Cmd{}) -} - -type V2Project struct { - Project project.Project - Boilerplate project.Boilerplate -} - -func (p *V2Project) Validate() error { - return nil -} - -func (p *V2Project) EnsureDependencies() (bool, error) { - // ensure that we are pinning controller-runtime version - // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 - c := exec.Command("go", "get", "sigs.k8s.io/controller-runtime@"+controllerRuntimeVersion) // #nosec - c.Stderr = os.Stderr - c.Stdout = os.Stdout - fmt.Println(strings.Join(c.Args, " ")) - err := c.Run() - if err != nil { - return false, err - } - - c = exec.Command("go", "mod", "tidy") // #nosec - c.Stderr = os.Stderr - c.Stdout = os.Stdout - fmt.Println(strings.Join(c.Args, " ")) - err = c.Run() - if err != nil { - return false, err - } - return true, err -} - -func (p *V2Project) Scaffold() error { - s := &Scaffold{ - BoilerplateOptional: true, - ConfigOptional: true, - } - - universe, err := model.NewUniverse( - model.WithConfig(&p.Project.Config), - model.WithoutBoilerplate, - ) - if err != nil { - return fmt.Errorf("error initializing project: %v", err) - } - - projectInput, err := p.Project.GetInput() - if err != nil { - return err - } - - bpInput, err := p.Boilerplate.GetInput() - if err != nil { - return err - } - - err = s.Execute( - universe, - input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, - &p.Project, - &p.Boilerplate, - ) - if err != nil { - return err - } - - // default controller manager image name - imgName := "controller:latest" - - s = &Scaffold{} - - universe, err = model.NewUniverse( - model.WithConfig(&p.Project.Config), - model.WithBoilerplateFrom(p.Boilerplate.Path), - ) - if err != nil { - return fmt.Errorf("error initializing project: %v", err) - } - - return s.Execute( - universe, - input.Options{ProjectPath: projectInput.Path, BoilerplatePath: bpInput.Path}, - &project.GitIgnore{}, - &metricsauthv2.AuthProxyPatch{}, - &metricsauthv2.AuthProxyService{}, - &metricsauthv2.ClientClusterRole{}, - &project.AuthProxyRole{}, - &project.AuthProxyRoleBinding{}, - &managerv2.Config{Image: imgName}, - &scaffoldv2.Main{}, - &scaffoldv2.GoMod{ControllerRuntimeVersion: controllerRuntimeVersion}, - &scaffoldv2.Makefile{Image: imgName, ControllerToolsVersion: controllerToolsVersion}, - &scaffoldv2.Dockerfile{}, - &scaffoldv2.Kustomize{}, - &scaffoldv2.ManagerWebhookPatch{}, - &scaffoldv2.ManagerRoleBinding{}, - &scaffoldv2.LeaderElectionRole{}, - &scaffoldv2.LeaderElectionRoleBinding{}, - &scaffoldv2.KustomizeRBAC{}, - &managerv2.Kustomization{}, - &webhook.Kustomization{}, - &webhook.KustomizeConfigWebhook{}, - &webhook.Service{}, - &webhook.InjectCAPatch{}, - &prometheus.Kustomization{}, - &prometheus.ServiceMonitor{}, - &certmanager.CertManager{}, - &certmanager.Kustomization{}, - &certmanager.KustomizeConfig{}, - ) -} diff --git a/pkg/scaffold/project/project.go b/pkg/scaffold/project/project.go deleted file mode 100644 index ab495219260..00000000000 --- a/pkg/scaffold/project/project.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright 2018 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 project - -import ( - "fmt" - - internalconfig "sigs.k8s.io/kubebuilder/internal/config" - "sigs.k8s.io/kubebuilder/pkg/model/config" - "sigs.k8s.io/kubebuilder/pkg/scaffold/input" - "sigs.k8s.io/yaml" -) - -var _ input.File = &Project{} - -// Project scaffolds the PROJECT file with project metadata -type Project struct { - // Path is the output file location - defaults to PROJECT - Path string - - config.Config -} - -// GetInput implements input.File -func (f *Project) GetInput() (input.Input, error) { - if f.Path == "" { - f.Path = internalconfig.DefaultPath - } - if f.Version == "" { - f.Version = config.Version1 - } - if f.Repo == "" { - return input.Input{}, fmt.Errorf("must specify repository") - } - - out, err := yaml.Marshal(f.Config) - if err != nil { - return input.Input{}, err - } - - return input.Input{ - Path: f.Path, - TemplateBody: string(out), - Repo: f.Repo, - Version: f.Version, - Domain: f.Domain, - MultiGroup: f.MultiGroup, - IfExistsAction: input.Error, - }, nil -} diff --git a/pkg/scaffold/project/project_test.go b/pkg/scaffold/project/project_test.go index 477bc267444..0115b6bdd58 100644 --- a/pkg/scaffold/project/project_test.go +++ b/pkg/scaffold/project/project_test.go @@ -311,23 +311,4 @@ Copyright %s Example Owners. }) }) }) - - Describe("scaffolding a PROEJCT", func() { - BeforeEach(func() { - goldenPath = filepath.Join("PROJECT") - writeToPath = goldenPath - }) - Context("with defaults", func() { - It("should match the golden file", func() { - instance := &project.Project{} - instance.Version = "1" - instance.Domain = "testproject.org" - instance.Repo = repo - 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/update.go b/pkg/scaffold/update.go new file mode 100644 index 00000000000..e20fa4d4e32 --- /dev/null +++ b/pkg/scaffold/update.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 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 scaffold + +import ( + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/project" +) + +type updateScaffolder struct { + config *config.Config +} + +func NewUpdateScaffolder(config *config.Config) Scaffolder { + return &updateScaffolder{ + config: config, + } +} + +func (s *updateScaffolder) Scaffold() error { + universe, err := model.NewUniverse( + model.WithConfig(s.config), + model.WithoutBoilerplate, + ) + if err != nil { + return err + } + + return (&Scaffold{}).Execute( + universe, + input.Options{}, + &project.GopkgToml{}, + ) +} diff --git a/pkg/scaffold/v1/webhook/webhook_test.go b/pkg/scaffold/v1/webhook/webhook_test.go index 713df9d6712..a23cddef80e 100644 --- a/pkg/scaffold/v1/webhook/webhook_test.go +++ b/pkg/scaffold/v1/webhook/webhook_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package webhook +package webhook_test import ( "fmt" @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/kubebuilder/pkg/scaffold/input" "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" "sigs.k8s.io/kubebuilder/pkg/scaffold/scaffoldtest" + . "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/webhook" ) var _ = Describe("Webhook", func() { diff --git a/pkg/scaffold/webhook.go b/pkg/scaffold/webhook.go new file mode 100644 index 00000000000..fd1f8fb31cb --- /dev/null +++ b/pkg/scaffold/webhook.go @@ -0,0 +1,164 @@ +/* +Copyright 2020 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 scaffold + +import ( + "fmt" + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/model" + "sigs.k8s.io/kubebuilder/pkg/model/config" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/resource" + managerv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/manager" + webhookv1 "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/webhook" + scaffoldv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2" + webhookv2 "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/webhook" +) + +type webhookScaffolder struct { + config *config.Config + resource *resource.Resource + // v1 + server string + webhookType string + operations []string + // v2 + defaulting, validation, conversion bool +} + +func NewV1WebhookScaffolder( + config *config.Config, + resource *resource.Resource, + server string, + webhookType string, + operations []string, +) Scaffolder { + return &webhookScaffolder{ + config: config, + resource: resource, + server: server, + webhookType: webhookType, + operations: operations, + } +} + +func NewV2WebhookScaffolder( + config *config.Config, + resource *resource.Resource, + defaulting bool, + validation bool, + conversion bool, +) Scaffolder { + return &webhookScaffolder{ + config: config, + resource: resource, + defaulting: defaulting, + validation: validation, + conversion: conversion, + } +} + +func (s *webhookScaffolder) Scaffold() error { + fmt.Println("Writing scaffold for you to edit...") + + switch { + case s.config.IsV1(): + return s.scaffoldV1() + case s.config.IsV2(): + return s.scaffoldV2() + default: + return fmt.Errorf("unknown project version %v", s.config.Version) + } +} + +func (s *webhookScaffolder) scaffoldV1() error { + universe, err := model.NewUniverse( + model.WithConfig(s.config), + // TODO(adirio): missing model.WithBoilerplate[From], needs boilerplate or path + model.WithResource(s.resource, s.config), + ) + if err != nil { + return err + } + + webhookConfig := webhookv1.Config{Server: s.server, Type: s.webhookType, Operations: s.operations} + + return (&Scaffold{}).Execute( + universe, + input.Options{}, + &managerv1.Webhook{}, + &webhookv1.AdmissionHandler{Resource: s.resource, Config: webhookConfig}, + &webhookv1.AdmissionWebhookBuilder{Resource: s.resource, Config: webhookConfig}, + &webhookv1.AdmissionWebhooks{Resource: s.resource, Config: webhookConfig}, + &webhookv1.AddAdmissionWebhookBuilderHandler{Resource: s.resource, Config: webhookConfig}, + &webhookv1.Server{Config: webhookConfig}, + &webhookv1.AddServer{Config: webhookConfig}, + ) +} + +func (s *webhookScaffolder) scaffoldV2() error { + if s.config.MultiGroup { + fmt.Println(filepath.Join("apis", s.resource.Group, s.resource.Version, + fmt.Sprintf("%s_webhook.go", strings.ToLower(s.resource.Kind)))) + } else { + fmt.Println(filepath.Join("api", s.resource.Version, + fmt.Sprintf("%s_webhook.go", strings.ToLower(s.resource.Kind)))) + } + + if s.conversion { + fmt.Println(`Webhook server has been set up for you. +You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`) + } + + universe, err := model.NewUniverse( + model.WithConfig(s.config), + // TODO(adirio): missing model.WithBoilerplate[From], needs boilerplate or path + model.WithResource(s.resource, s.config), + ) + if err != nil { + return err + } + + webhookScaffolder := &webhookv2.Webhook{ + Resource: s.resource, + Defaulting: s.defaulting, + Validating: s.validation, + } + if err := (&Scaffold{}).Execute( + universe, + input.Options{}, + webhookScaffolder, + ); err != nil { + return err + } + + if err := (&scaffoldv2.Main{}).Update( + &scaffoldv2.MainUpdateOptions{ + Config: s.config, + WireResource: false, + WireController: false, + WireWebhook: true, + Resource: s.resource, + }, + ); err != nil { + return fmt.Errorf("error updating main.go: %v", err) + } + + return nil +}