diff --git a/cmd/main.go b/cmd/main.go index 59f63c4bbd4..0eb0cc1a452 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -116,6 +116,7 @@ func main() { rootCmd.AddCommand( newInitProjectCmd(), newAPICommand(), + newWebhookV2Cmd(), version.NewVersionCmd(), newDocsCmd(), newVendorUpdateCmd(), diff --git a/cmd/webhook.go b/cmd/webhook_v1.go similarity index 93% rename from cmd/webhook.go rename to cmd/webhook_v1.go index a89a48b896a..9aaa55be2a7 100644 --- a/cmd/webhook.go +++ b/cmd/webhook_v1.go @@ -40,8 +40,8 @@ func newWebhookCmd() *cobra.Command { cmd := &cobra.Command{ Use: "webhook", - Short: "Scaffold a webhook server", - Long: `Scaffold a webhook server if there is no existing server. + Short: "Scaffold a webhook server for v1 scaffolding.", + Long: `Scaffold a webhook server for v1 scaffolding. 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. `, @@ -58,7 +58,7 @@ This command is only available for v1 scaffolding project. } if projectInfo.Version != project.Version1 { - fmt.Printf("webhook scaffolding is not supported for this project version: %s \n", projectInfo.Version) + fmt.Printf("kubebuilder alpha webhook is for project version: 1, the version of this project is: %s \n", projectInfo.Version) os.Exit(0) } diff --git a/cmd/webhook_v2.go b/cmd/webhook_v2.go new file mode 100644 index 00000000000..b17cf39228d --- /dev/null +++ b/cmd/webhook_v2.go @@ -0,0 +1,119 @@ +/* +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/gobuffalo/flect" + "github.com/spf13/cobra" + + "sigs.k8s.io/kubebuilder/pkg/scaffold" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/project" + "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" + resourcev2 "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 using v2 scaffolding.", + Long: `Scaffold a webhook using v2 scaffolding. You can choose to scaffold defaulting and (or) validating webhooks.`, + Example: ` # Create defaulting and validating webhooks for CRD of group crew, version v1 and kind FirstMate. + kubebuilder webhook --group crew --version v1 --kind FirstMate --defaulting --programmatic-validation +`, + Run: func(cmd *cobra.Command, args []string) { + dieIfNoProject() + + projectInfo, err := scaffold.LoadProjectFile("PROJECT") + if err != nil { + log.Fatalf("failed to read the PROJECT file: %v", err) + } + + if projectInfo.Version != project.Version2 { + fmt.Printf("kubebuilder webhook is for project version: 2, the version of this project is: %s \n", projectInfo.Version) + os.Exit(0) + } + + if !o.defaulting && !o.validation { + fmt.Printf("kubebuilder webhook requires at least one of --defaulting and --programmatic-validation") + os.Exit(0) + } + + fmt.Println(filepath.Join("api", fmt.Sprintf("%s_webhook.go", strings.ToLower(o.res.Kind)))) + webhookScaffolder := &webhook.Webhook{ + Resource: o.res, + Defaulting: o.defaulting, + Validating: o.validation, + } + err = (&scaffold.Scaffold{}).Execute( + input.Options{}, + webhookScaffolder, + ) + if err != nil { + fmt.Printf("error scaffolding webhook: %v", err) + os.Exit(1) + } + + err = (&resourcev2.Main{}).Update( + &resourcev2.MainUpdateOptions{ + Project: &projectInfo, + WireResource: false, + WireController: false, + WireWebhook: true, + Resource: o.res, + }) + if err != nil { + fmt.Printf("error updating main.go: %v", err) + os.Exit(1) + } + + if projectInfo.Version != project.Version1 { + fmt.Printf("webhook scaffolding is not supported for this project version: %s \n", projectInfo.Version) + os.Exit(0) + } + + fmt.Println("Writing scaffold for you to edit...") + + if len(o.res.Resource) == 0 { + o.res.Resource = flect.Pluralize(strings.ToLower(o.res.Kind)) + } + }, + } + o.res = gvkForFlags(cmd.Flags()) + cmd.Flags().BoolVar(&o.defaulting, "defaulting", false, + "if scaffold the defaulting webhook") + cmd.Flags().BoolVar(&o.validation, "programmatic-validation", false, + "if scaffold the validating webhook") + + return cmd +} + +// webhookOptions represents commandline options for scaffolding a webhook. +type webhookV2Options struct { + res *resource.Resource + defaulting bool + validation bool +} diff --git a/generated_golden.sh b/generated_golden.sh index 6d5750f162a..b4780e7570e 100755 --- a/generated_golden.sh +++ b/generated_golden.sh @@ -64,7 +64,9 @@ scaffold_test_project() { $kb init --project-version $version --domain testproject.org --license apache2 --owner "The Kubernetes authors" $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false + $kb webhook --group crew --version v1 --kind Captain --defaulting $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + $kb webhook --group crew --version v1 --kind FirstMate --programmatic-validation # TODO(droot): Adding a second group is a valid test case and kubebuilder is expected to report an error in this case. It # doesn't do that currently so leaving it commented so that we can enable it later. # $kb create api --group ship --version v1beta1 --kind Frigate --example=false --controller=true --resource=true --make=false diff --git a/pkg/scaffold/util/util.go b/pkg/scaffold/util/util.go new file mode 100644 index 00000000000..dc220b70d45 --- /dev/null +++ b/pkg/scaffold/util/util.go @@ -0,0 +1,73 @@ +/* +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 util + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" +) + +func GetResourceInfo(r *resource.Resource, in input.Input) (resourcePackage, groupDomain string) { + // Use the k8s.io/api package for core resources + coreGroups := map[string]string{ + "apps": "", + "admission": "k8s.io", + "admissionregistration": "k8s.io", + "auditregistration": "k8s.io", + "apiextensions": "k8s.io", + "authentication": "k8s.io", + "authorization": "k8s.io", + "autoscaling": "", + "batch": "", + "certificates": "k8s.io", + "coordination": "k8s.io", + "core": "", + "events": "k8s.io", + "extensions": "", + "imagepolicy": "k8s.io", + "networking": "k8s.io", + "node": "k8s.io", + "metrics": "k8s.io", + "policy": "", + "rbac.authorization": "k8s.io", + "scheduling": "k8s.io", + "setting": "k8s.io", + "storage": "k8s.io", + } + resourcePath := filepath.Join("api", r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) + if _, err := os.Stat(resourcePath); os.IsNotExist(err) { + if domain, found := coreGroups[r.Group]; found { + // TODO: support apiextensions.k8s.io and metrics.k8s.io. + // apiextensions.k8s.io is in k8s.io/apiextensions-apiserver/pkg/apis/apiextensions + // metrics.k8s.io is in k8s.io/metrics/pkg/apis/metrics + resourcePackage := path.Join("k8s.io", "api", r.Group) + groupDomain = r.Group + if domain != "" { + groupDomain = r.Group + "." + domain + } + return resourcePackage, groupDomain + } + // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath + } + return path.Join(in.Repo, "api"), r.Group + "." + in.Domain +} diff --git a/pkg/scaffold/v1/controller/controller.go b/pkg/scaffold/v1/controller/controller.go index 5fe490ff663..b828a8c26bc 100644 --- a/pkg/scaffold/v1/controller/controller.go +++ b/pkg/scaffold/v1/controller/controller.go @@ -17,14 +17,13 @@ limitations under the License. package controller import ( - "fmt" - "os" - "path" "path/filepath" "strings" "github.com/gobuffalo/flect" + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) @@ -47,24 +46,7 @@ type Controller struct { // GetInput implements input.File func (a *Controller) GetInput() (input.Input, error) { - // Use the k8s.io/api package for core resources - coreGroups := map[string]string{ - "apps": "", - "admissionregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "core": "", - "extensions": "", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "storage": "k8s.io", - } - - a.ResourcePackage, a.GroupDomain = getResourceInfo(coreGroups, a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) if a.Plural == "" { a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) @@ -80,23 +62,6 @@ func (a *Controller) GetInput() (input.Input, error) { return a.Input, nil } -func getResourceInfo(coreGroups map[string]string, r *resource.Resource, in input.Input) (resourcePackage, groupDomain string) { - resourcePath := filepath.Join("pkg", "apis", r.Group, r.Version, - fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) - if _, err := os.Stat(resourcePath); os.IsNotExist(err) { - if domain, found := coreGroups[r.Group]; found { - resourcePackage := path.Join("k8s.io", "api") - groupDomain = r.Group - if domain != "" { - groupDomain = r.Group + "." + domain - } - return resourcePackage, groupDomain - } - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - } - return path.Join(in.Repo, "pkg", "apis"), r.Group + "." + in.Domain -} - var controllerTemplate = `{{ .Boilerplate }} package {{ lower .Resource.Kind }} diff --git a/pkg/scaffold/v1/controller/controllertest.go b/pkg/scaffold/v1/controller/controllertest.go index b71b74259a8..716bfa5effc 100644 --- a/pkg/scaffold/v1/controller/controllertest.go +++ b/pkg/scaffold/v1/controller/controllertest.go @@ -21,6 +21,7 @@ import ( "strings" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) @@ -42,24 +43,7 @@ func (a *Test) GetInput() (input.Input, error) { strings.ToLower(a.Resource.Kind), strings.ToLower(a.Resource.Kind)+"_controller_test.go") } - // Use the k8s.io/api package for core resources - coreGroups := map[string]string{ - "apps": "", - "admissionregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "core": "", - "extensions": "", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "storage": "k8s.io", - } - - a.ResourcePackage, _ = getResourceInfo(coreGroups, a.Resource, a.Input) + a.ResourcePackage, _ = util.GetResourceInfo(a.Resource, a.Input) a.TemplateBody = controllerTestTemplate a.Input.IfExistsAction = input.Error diff --git a/pkg/scaffold/v1/webhook/admissionbuilder.go b/pkg/scaffold/v1/webhook/admissionbuilder.go index 167b6445299..de3213103f3 100644 --- a/pkg/scaffold/v1/webhook/admissionbuilder.go +++ b/pkg/scaffold/v1/webhook/admissionbuilder.go @@ -22,6 +22,7 @@ import ( "strings" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) @@ -51,7 +52,7 @@ type AdmissionWebhookBuilder struct { // GetInput implements input.File func (a *AdmissionWebhookBuilder) GetInput() (input.Input, error) { - a.ResourcePackage, a.GroupDomain = getResourceInfo(coreGroups, a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) if a.Type == "mutating" { a.Mutating = true diff --git a/pkg/scaffold/v1/webhook/admissionhandler.go b/pkg/scaffold/v1/webhook/admissionhandler.go index e00197ad48f..ead6d52fea0 100644 --- a/pkg/scaffold/v1/webhook/admissionhandler.go +++ b/pkg/scaffold/v1/webhook/admissionhandler.go @@ -22,6 +22,7 @@ import ( "strings" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) @@ -51,7 +52,7 @@ type AdmissionHandler struct { // GetInput implements input.File func (a *AdmissionHandler) GetInput() (input.Input, error) { - a.ResourcePackage, a.GroupDomain = getResourceInfo(coreGroups, a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) a.Type = strings.ToLower(a.Type) if a.Type == "mutating" { a.Mutate = true diff --git a/pkg/scaffold/v1/webhook/util.go b/pkg/scaffold/v1/webhook/util.go index e27b7c594fe..bbd714c8ec1 100644 --- a/pkg/scaffold/v1/webhook/util.go +++ b/pkg/scaffold/v1/webhook/util.go @@ -18,50 +18,10 @@ package webhook import ( "fmt" - "os" - "path" - "path/filepath" "strings" - - "sigs.k8s.io/kubebuilder/pkg/scaffold/input" - "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) -// Use the k8s.io/api package for core resources -var coreGroups = map[string]string{ - "apps": "", - "admissionregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "core": "", - "extensions": "", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "storage": "k8s.io", -} - func builderName(config Config, resource string) string { opsStr := strings.Join(config.Operations, "-") return fmt.Sprintf("%s-%s-%s", config.Type, opsStr, resource) } - -func getResourceInfo(coreGroups map[string]string, r *resource.Resource, in input.Input) (resourcePackage, groupDomain string) { - resourcePath := filepath.Join("pkg", "apis", r.Group, r.Version, - fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) - if _, err := os.Stat(resourcePath); os.IsNotExist(err) { - if domain, found := coreGroups[r.Group]; found { - resourcePackage := path.Join("k8s.io", "api") - groupDomain = r.Group - if domain != "" { - groupDomain = r.Group + "." + domain - } - return resourcePackage, groupDomain - } - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - } - return path.Join(in.Repo, "pkg", "apis"), r.Group + "." + in.Domain -} diff --git a/pkg/scaffold/v2/controller.go b/pkg/scaffold/v2/controller.go index cf25bb39bd3..7eca90ab147 100644 --- a/pkg/scaffold/v2/controller.go +++ b/pkg/scaffold/v2/controller.go @@ -17,15 +17,13 @@ limitations under the License. package v2 import ( - "fmt" - "os" - "path" "path/filepath" "strings" "github.com/gobuffalo/flect" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" ) @@ -49,7 +47,7 @@ type Controller struct { // GetInput implements input.File func (a *Controller) GetInput() (input.Input, error) { - a.ResourcePackage, a.GroupDomain = getResourceInfo(a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) if a.Plural == "" { a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) @@ -64,38 +62,6 @@ func (a *Controller) GetInput() (input.Input, error) { return a.Input, nil } -func getResourceInfo(r *resource.Resource, in input.Input) (resourcePackage, groupDomain string) { - // Use the k8s.io/api package for core resources - coreGroups := map[string]string{ - "apps": "", - "admissionregistration": "k8s.io", - "apiextensions": "k8s.io", - "authentication": "k8s.io", - "autoscaling": "", - "batch": "", - "certificates": "k8s.io", - "core": "", - "extensions": "", - "metrics": "k8s.io", - "policy": "", - "rbac.authorization": "k8s.io", - "storage": "k8s.io", - } - resourcePath := filepath.Join("api", r.Version, fmt.Sprintf("%s_types.go", strings.ToLower(r.Kind))) - if _, err := os.Stat(resourcePath); os.IsNotExist(err) { - if domain, found := coreGroups[r.Group]; found { - resourcePackage := path.Join("k8s.io", "api", r.Group) - groupDomain = r.Group - if domain != "" { - groupDomain = r.Group + "." + domain - } - return resourcePackage, groupDomain - } - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - } - return path.Join(in.Repo, "api"), r.Group + "." + in.Domain -} - var controllerTemplate = `{{ .Boilerplate }} package controllers diff --git a/pkg/scaffold/v2/controller_suitetest.go b/pkg/scaffold/v2/controller_suitetest.go index 0bb018e00b0..9a0caecbfbd 100644 --- a/pkg/scaffold/v2/controller_suitetest.go +++ b/pkg/scaffold/v2/controller_suitetest.go @@ -23,6 +23,7 @@ import ( "github.com/gobuffalo/flect" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/internal" ) @@ -130,7 +131,7 @@ var _ = AfterSuite(func() { // adding import paths and code setup for new types. func (a *ControllerSuiteTest) Update() error { - a.ResourcePackage, a.GroupDomain = getResourceInfo(a.Resource, a.Input) + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) if a.Plural == "" { a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) } diff --git a/pkg/scaffold/v2/group.go b/pkg/scaffold/v2/group.go index 1a220eac2bb..c9a2c61d75b 100644 --- a/pkg/scaffold/v2/group.go +++ b/pkg/scaffold/v2/group.go @@ -57,6 +57,7 @@ package {{ .Resource.Version }} import ( "sigs.k8s.io/controller-runtime/pkg/scheme" "k8s.io/apimachinery/pkg/runtime/schema" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" ) var ( diff --git a/pkg/scaffold/v2/main.go b/pkg/scaffold/v2/main.go index 080e8d18ca4..7bc95641389 100644 --- a/pkg/scaffold/v2/main.go +++ b/pkg/scaffold/v2/main.go @@ -21,6 +21,7 @@ import ( "path/filepath" "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" "sigs.k8s.io/kubebuilder/pkg/scaffold/v2/internal" ) @@ -52,7 +53,7 @@ func (m *Main) GetInput() (input.Input, error) { func (m *Main) Update(opts *MainUpdateOptions) error { path := "main.go" - resPkg, _ := getResourceInfo(opts.Resource, input.Input{ + resPkg, _ := util.GetResourceInfo(opts.Resource, input.Input{ Domain: opts.Project.Domain, Repo: opts.Project.Repo, }) @@ -64,21 +65,25 @@ func (m *Main) Update(opts *MainUpdateOptions) error { `, opts.Project.Repo) addschemeCodeFragment := fmt.Sprintf(`_ = %s%s.AddToScheme(scheme) `, opts.Resource.Group, opts.Resource.Version) - reconcilerSetupCodeFragment := fmt.Sprintf(`err = (&controllers.%sReconciler{ + reconcilerSetupCodeFragment := fmt.Sprintf(`if err = (&controllers.%sReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("%s"), - }).SetupWithManager(mgr) - if err != nil { + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "%s") os.Exit(1) - } + } `, opts.Resource.Kind, opts.Resource.Kind, opts.Resource.Kind) + webhookSetupCodeFragment := fmt.Sprintf(`if err = (&%s%s.%s{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "FirstMate") + os.Exit(1) + } +`, opts.Resource.Group, opts.Resource.Version, opts.Resource.Kind) if opts.WireResource { err := internal.InsertStringsInFile(path, map[string][]string{ - apiPkgImportScaffoldMarker: []string{apiImportCodeFragment}, - apiSchemeScaffoldMarker: []string{addschemeCodeFragment}, + apiPkgImportScaffoldMarker: {apiImportCodeFragment}, + apiSchemeScaffoldMarker: {addschemeCodeFragment}, }) if err != nil { return err @@ -88,9 +93,18 @@ func (m *Main) Update(opts *MainUpdateOptions) error { if opts.WireController { return internal.InsertStringsInFile(path, map[string][]string{ - apiPkgImportScaffoldMarker: []string{apiImportCodeFragment, ctrlImportCodeFragment}, - apiSchemeScaffoldMarker: []string{addschemeCodeFragment}, - reconcilerSetupScaffoldMarker: []string{reconcilerSetupCodeFragment}, + apiPkgImportScaffoldMarker: {apiImportCodeFragment, ctrlImportCodeFragment}, + apiSchemeScaffoldMarker: {addschemeCodeFragment}, + reconcilerSetupScaffoldMarker: {reconcilerSetupCodeFragment}, + }) + } + + if opts.WireWebhook { + return internal.InsertStringsInFile(path, + map[string][]string{ + apiPkgImportScaffoldMarker: {apiImportCodeFragment, ctrlImportCodeFragment}, + apiSchemeScaffoldMarker: {addschemeCodeFragment}, + reconcilerSetupScaffoldMarker: {webhookSetupCodeFragment}, }) } @@ -109,6 +123,7 @@ type MainUpdateOptions struct { // Flags to indicate if resource/controller is being scaffolded or not WireResource bool WireController bool + WireWebhook bool } var mainTemplate = fmt.Sprintf(`{{ .Boilerplate }} diff --git a/pkg/scaffold/v2/webhook/webhook.go b/pkg/scaffold/v2/webhook/webhook.go new file mode 100644 index 00000000000..d2f8dd00cff --- /dev/null +++ b/pkg/scaffold/v2/webhook/webhook.go @@ -0,0 +1,140 @@ +/* +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 webhook + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/gobuffalo/flect" + + "sigs.k8s.io/kubebuilder/pkg/scaffold/input" + "sigs.k8s.io/kubebuilder/pkg/scaffold/util" + "sigs.k8s.io/kubebuilder/pkg/scaffold/v1/resource" +) + +// Webhook scaffolds a Webhook for a Resource +type Webhook struct { + input.Input + + // Resource is the Resource to make the Webhook for + Resource *resource.Resource + + // ResourcePackage is the package of the Resource + ResourcePackage string + + // Plural is the plural lowercase of kind + Plural string + + // Is the Group + "." + Domain for the Resource + GroupDomain string + + // Is the Group + "." + Domain for the Resource + GroupDomainWithDash string + + // If scaffold the defaulting webhook + Defaulting bool + // If scaffold the validating webhook + Validating bool +} + +// GetInput implements input.File +func (a *Webhook) GetInput() (input.Input, error) { + + a.ResourcePackage, a.GroupDomain = util.GetResourceInfo(a.Resource, a.Input) + + a.GroupDomainWithDash = strings.Replace(a.GroupDomain, ".", "-", -1) + + if a.Plural == "" { + a.Plural = flect.Pluralize(strings.ToLower(a.Resource.Kind)) + } + + if a.Path == "" { + a.Path = filepath.Join("api", a.Resource.Version, + fmt.Sprintf("%s_webhook.go", strings.ToLower(a.Resource.Kind))) + } + if a.Defaulting { + WebhookTemplate = WebhookTemplate + DefaultingWebhookTemplate + } + if a.Validating { + WebhookTemplate = WebhookTemplate + ValidatingWebhookTemplate + } + + a.TemplateBody = WebhookTemplate + a.Input.IfExistsAction = input.Error + return a.Input, nil +} + +var ( + WebhookTemplate = `{{ .Boilerplate }} + +package {{ .Resource.Version }} + +import ( + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource") + +func (r *{{.Resource.Kind}}) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r) +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +` + + DefaultingWebhookTemplate = ` +// +kubebuilder:webhook:path=/mutate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=true,failurePolicy=fail,groups={{ .Resource.Group }},resources={{ .Plural }},verbs=create;update,versions=v1,name=m{{ lower .Resource.Kind }}.kb.io + +var _ webhook.Defaulter = &{{ .Resource.Kind }}{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *{{ .Resource.Kind }}) Default() { + {{ lower .Resource.Kind }}log.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} +` + + ValidatingWebhookTemplate = ` +// +kubebuilder:webhook:path=/validate-{{ .GroupDomainWithDash }}-{{ .Resource.Version }}-{{ lower .Resource.Kind }},mutating=false,failurePolicy=fail,groups={{ .Resource.Group }},resources={{ .Plural }},verbs=create;update,versions=v1,name=v{{ lower .Resource.Kind }}.kb.io + +var _ webhook.Validator = &{{ .Resource.Kind }}{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *{{ .Resource.Kind }}) ValidateCreate() error { + {{ lower .Resource.Kind }}log.Info("validate create", "name", r.Name) + + // TODO(user): fill in your validation logic upon object creation. + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *{{ .Resource.Kind }}) ValidateUpdate(old runtime.Object) error { + {{ lower .Resource.Kind }}log.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil +} +` +) diff --git a/testdata/project-v2/api/v1/captain_webhook.go b/testdata/project-v2/api/v1/captain_webhook.go new file mode 100644 index 00000000000..12191f2e3fc --- /dev/null +++ b/testdata/project-v2/api/v1/captain_webhook.go @@ -0,0 +1,44 @@ +/* +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 v1 + +import ( + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var captainlog = logf.Log.WithName("captain-resource") + +func (r *Captain) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r) +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-captain,mutating=true,failurePolicy=fail,groups=crew,resources=captains,verbs=create;update,versions=v1,name=mcaptain.kb.io + +var _ webhook.Defaulter = &Captain{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *Captain) Default() { + captainlog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} diff --git a/testdata/project-v2/api/v1/firstmate_webhook.go b/testdata/project-v2/api/v1/firstmate_webhook.go new file mode 100644 index 00000000000..cf70a229944 --- /dev/null +++ b/testdata/project-v2/api/v1/firstmate_webhook.go @@ -0,0 +1,54 @@ +/* +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 v1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var firstmatelog = logf.Log.WithName("firstmate-resource") + +func (r *FirstMate) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r) +} + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/validate-crew-testproject-org-v1-firstmate,mutating=false,failurePolicy=fail,groups=crew,resources=firstmates,verbs=create;update,versions=v1,name=vfirstmate.kb.io + +var _ webhook.Validator = &FirstMate{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *FirstMate) ValidateCreate() error { + firstmatelog.Info("validate create", "name", r.Name) + + // TODO(user): fill in your validation logic upon object creation. + return nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *FirstMate) ValidateUpdate(old runtime.Object) error { + firstmatelog.Info("validate update", "name", r.Name) + + // TODO(user): fill in your validation logic upon object update. + return nil +} diff --git a/testdata/project-v2/api/v1/zz_generated.deepcopy.go b/testdata/project-v2/api/v1/zz_generated.deepcopy.go index 342d103b59b..cc2c50f293c 100644 --- a/testdata/project-v2/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v2/api/v1/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package v1 import ( - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. diff --git a/testdata/project-v2/config/webhook/manifests.yaml b/testdata/project-v2/config/webhook/manifests.yaml index e69de29bb2d..2df6c58ae70 100644 --- a/testdata/project-v2/config/webhook/manifests.yaml +++ b/testdata/project-v2/config/webhook/manifests.yaml @@ -0,0 +1,52 @@ + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + creationTimestamp: null + name: mutating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /mutate-crew-testproject-org-v1-captain + failurePolicy: Fail + name: mcaptain.kb.io + rules: + - apiGroups: + - crew + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - captains + +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /validate-crew-testproject-org-v1-firstmate + failurePolicy: Fail + name: vfirstmate.kb.io + rules: + - apiGroups: + - crew + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - firstmates diff --git a/testdata/project-v2/main.go b/testdata/project-v2/main.go index dcf19d5a868..8856689d5ea 100644 --- a/testdata/project-v2/main.go +++ b/testdata/project-v2/main.go @@ -64,27 +64,32 @@ func main() { os.Exit(1) } - err = (&controllers.CaptainReconciler{ + if err = (&controllers.CaptainReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Captain"), - }).SetupWithManager(mgr) - if err != nil { + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Captain") os.Exit(1) } - err = (&controllers.FirstMateReconciler{ + if err = (&crewv1.Captain{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "FirstMate") + os.Exit(1) + } + if err = (&controllers.FirstMateReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("FirstMate"), - }).SetupWithManager(mgr) - if err != nil { + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "FirstMate") os.Exit(1) } - err = (&controllers.NamespaceReconciler{ + if err = (&crewv1.FirstMate{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "FirstMate") + os.Exit(1) + } + if err = (&controllers.NamespaceReconciler{ Client: mgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("Namespace"), - }).SetupWithManager(mgr) - if err != nil { + }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Namespace") os.Exit(1) }