diff --git a/cmd/main.go b/cmd/main.go index c3527528372..d377ea11ffb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/cli" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + declarativev1 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1" pluginv2 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2" pluginv3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) @@ -34,6 +35,7 @@ func main() { cli.WithPlugins( &pluginv2.Plugin{}, &pluginv3.Plugin{}, + &declarativev1.Plugin{}, ), cli.WithDefaultPlugins(cfgv2.Version, &pluginv2.Plugin{}), cli.WithDefaultPlugins(cfgv3.Version, &pluginv3.Plugin{}), diff --git a/pkg/plugins/declarative/v1/api.go b/pkg/plugins/declarative/v1/api.go new file mode 100644 index 00000000000..b247c9c39f4 --- /dev/null +++ b/pkg/plugins/declarative/v1/api.go @@ -0,0 +1,107 @@ +/* +Copyright 2021 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 ( + "fmt" + "path/filepath" + "strings" + + "github.com/spf13/afero" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1/internal/templates" + goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util" +) + +const ( + // kbDeclarativePattern is the sigs.k8s.io/kubebuilder-declarative-pattern version + kbDeclarativePatternForV2 = "v0.0.0-20200522144838-848d48e5b073" + kbDeclarativePatternForV3 = "v0.0.0-20210113160450-b84d99da0217" + + exampleManifestVersion = "0.0.1" +) + +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + +type createAPISubcommand struct { + config config.Config + + resource *resource.Resource +} + +func (p *createAPISubcommand) InjectConfig(c config.Config) error { + p.config = c + + return nil +} + +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if !p.resource.HasAPI() || !p.resource.HasController() { + return plugin.ExitError{ + Plugin: pluginName, + Reason: "declarative pattern is only supported when API and controller are scaffolded", + } + } + + return nil +} + +func (p *createAPISubcommand) Scaffold(fs afero.Fs) error { + fmt.Println("updating scaffold with declarative pattern...") + + // Load the boilerplate + bp, err := afero.ReadFile(fs, filepath.Join("hack", "boilerplate.go.txt")) + if err != nil { + return fmt.Errorf("error updating scaffold: unable to load boilerplate: %w", err) + } + boilerplate := string(bp) + + if err := machinery.NewScaffold(fs).Execute( + model.NewUniverse( + model.WithConfig(p.config), + model.WithBoilerplate(boilerplate), + model.WithResource(p.resource), + ), + &templates.Types{}, + &templates.Controller{}, + &templates.Channel{ManifestVersion: exampleManifestVersion}, + &templates.Manifest{ManifestVersion: exampleManifestVersion}, + ); err != nil { + return fmt.Errorf("error updating scaffold: %w", err) + } + + // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version + kbDeclarativePattern := kbDeclarativePatternForV2 + if strings.Split(p.config.GetLayout(), ",")[0] == plugin.KeyFor(goPluginV3.Plugin{}) { + kbDeclarativePattern = kbDeclarativePatternForV3 + } + err = util.RunCmd("Get declarative pattern", "go", "get", + "sigs.k8s.io/kubebuilder-declarative-pattern@"+kbDeclarativePattern) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/plugins/declarative/v1/internal/templates/channel.go b/pkg/plugins/declarative/v1/internal/templates/channel.go new file mode 100644 index 00000000000..c2e4a0bfbb1 --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/channel.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 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 templates + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/file" +) + +var _ file.Template = &Channel{} + +// Channel scaffolds the file for the channel +type Channel struct { + file.TemplateMixin + + ManifestVersion string +} + +// SetTemplateDefaults implements file.Template +func (f *Channel) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("channels", "stable") + } + fmt.Println(f.Path) + + f.TemplateBody = channelTemplate + + f.IfExistsAction = file.Skip + + return nil +} + +const channelTemplate = `# Versions for the stable channel +manifests: +- version: {{ .ManifestVersion }} +` diff --git a/pkg/plugins/declarative/v1/internal/templates/controller.go b/pkg/plugins/declarative/v1/internal/templates/controller.go new file mode 100644 index 00000000000..bdbe2c3836d --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/controller.go @@ -0,0 +1,130 @@ +/* +Copyright 2021 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 templates + +import ( + "path/filepath" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/file" +) + +var _ file.Template = &Controller{} + +// Controller scaffolds the file that defines the controller for a CRD or a builtin resource +// nolint:maligned +type Controller struct { + file.TemplateMixin + file.MultiGroupMixin + file.BoilerplateMixin + file.ResourceMixin +} + +// SetTemplateDefaults implements file.Template +func (f *Controller) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup { + f.Path = filepath.Join("controllers", "%[group]", "%[kind]_controller.go") + } else { + f.Path = filepath.Join("controllers", "%[kind]_controller.go") + } + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = controllerTemplate + + f.IfExistsAction = file.Overwrite + + return nil +} + +//nolint:lll +const controllerTemplate = `{{ .Boilerplate }} + +package controllers + +import ( + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/status" + "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/declarative" + + {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" +) + +var _ reconcile.Reconciler = &{{ .Resource.Kind }}Reconciler{} + +// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object +type {{ .Resource.Kind }}Reconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme + + declarative.Reconciler +} + +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch + +// SetupWithManager sets up the controller with the Manager. +func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { + addon.Init() + + labels := map[string]string{ + "k8s-app": "{{ lower .Resource.Kind }}", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}, + declarative.WithObjectTransform(declarative.AddLabels(labels)), + declarative.WithOwner(declarative.SourceAsOwner), + declarative.WithLabels(watchLabels), + declarative.WithStatus(status.NewBasic(mgr.GetClient())), + // TODO: add an application to your manifest: declarative.WithObjectTransform(addon.TransformApplicationFromStatus), + // TODO: add an application to your manifest: declarative.WithManagedApplication(watchLabels), + declarative.WithObjectTransform(addon.ApplyPatches), + ); err != nil { + return err + } + + c, err := controller.New("{{ lower .Resource.Kind }}-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to {{ .Resource.Kind }} + err = c.Watch(&source.Kind{Type: &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to deployed objects + _, err = declarative.WatchAll(mgr.GetConfig(), c, r, watchLabels) + if err != nil { + return err + } + + return nil +} +` diff --git a/pkg/plugins/declarative/v1/internal/templates/manifest.go b/pkg/plugins/declarative/v1/internal/templates/manifest.go new file mode 100644 index 00000000000..5034a20613e --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/manifest.go @@ -0,0 +1,52 @@ +/* +Copyright 2021 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 templates + +import ( + "fmt" + "path/filepath" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/file" +) + +var _ file.Template = &Manifest{} + +// Manifest scaffolds the file that acts as a placeholder for the manifest +type Manifest struct { + file.TemplateMixin + file.ResourceMixin + + ManifestVersion string +} + +// SetTemplateDefaults implements file.Template +func (f *Manifest) SetTemplateDefaults() error { + if f.Path == "" { + f.Path = filepath.Join("channels", "packages", "%[kind]", f.ManifestVersion, "manifest.yaml") + } + f.Path = f.Resource.Replacer().Replace(f.Path) + fmt.Println(f.Path) + + f.TemplateBody = manifestTemplate + + f.IfExistsAction = file.Skip + + return nil +} + +const manifestTemplate = `# Placeholder manifest - replace with the manifest for your addon +` diff --git a/pkg/plugins/declarative/v1/internal/templates/types.go b/pkg/plugins/declarative/v1/internal/templates/types.go new file mode 100644 index 00000000000..105a70ea4e6 --- /dev/null +++ b/pkg/plugins/declarative/v1/internal/templates/types.go @@ -0,0 +1,143 @@ +/* +Copyright 2021 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 templates + +import ( + "fmt" + "path/filepath" + "text/template" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/file" +) + +var _ file.Template = &Types{} + +// Types scaffolds the file that defines the schema for a CRD +// nolint:maligned +type Types struct { + file.TemplateMixin + file.MultiGroupMixin + file.BoilerplateMixin + file.ResourceMixin +} + +// SetTemplateDefaults implements file.Template +func (f *Types) SetTemplateDefaults() error { + if f.Path == "" { + if f.MultiGroup { + f.Path = filepath.Join("apis", "%[group]", "%[version]", "%[kind]_types.go") + } else { + f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go") + } + } + f.Path = f.Resource.Replacer().Replace(f.Path) + + f.TemplateBody = typesTemplate + + f.IfExistsAction = file.Overwrite + + return nil +} + +// GetFuncMap implements file.UseCustomFuncMap +func (f Types) GetFuncMap() template.FuncMap { + funcMap := file.DefaultFuncMap() + funcMap["JSONTag"] = func(tag string) string { + return fmt.Sprintf("`json:%q`", tag) + } + return funcMap +} + +const typesTemplate = `{{ .Boilerplate }} + +package {{ .Resource.Version }} + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + addonv1alpha1 "sigs.k8s.io/kubebuilder-declarative-pattern/pkg/patterns/addon/pkg/apis/v1alpha1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }} +type {{ .Resource.Kind }}Spec struct { + addonv1alpha1.CommonSpec {{ JSONTag ",inline" }} + addonv1alpha1.PatchSpec {{ JSONTag ",inline" }} + + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }} +type {{ .Resource.Kind }}Status struct { + addonv1alpha1.CommonStatus {{ JSONTag ",inline" }} + + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +{{- if not .Resource.API.Namespaced }} +//+kubebuilder:resource:scope=Cluster +{{- end }} + +// {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API +type {{ .Resource.Kind }} struct { + metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` + metav1.ObjectMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` + + Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec,omitempty"` + "`" + ` + Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitempty"` + "`" + ` +} + +var _ addonv1alpha1.CommonObject = &{{ .Resource.Kind }}{} + +func (o *{{ .Resource.Kind }}) ComponentName() string { + return "{{ lower .Resource.Kind }}" +} + +func (o *{{ .Resource.Kind }}) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *{{ .Resource.Kind }}) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *{{ .Resource.Kind }}) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *{{ .Resource.Kind }}) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + +//+kubebuilder:object:root=true + +// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }} +type {{ .Resource.Kind }}List struct { + metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` + metav1.ListMeta ` + "`" + `json:"metadata,omitempty"` + "`" + ` + Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + ` +} + +func init() { + SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{}) +} +` diff --git a/pkg/plugins/declarative/v1/plugin.go b/pkg/plugins/declarative/v1/plugin.go new file mode 100644 index 00000000000..bdcb5d63173 --- /dev/null +++ b/pkg/plugins/declarative/v1/plugin.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 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 ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" +) + +const pluginName = "declarative" + plugins.DefaultNameQualifier + +var ( + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} + pluginVersion = plugin.Version{Number: 1} +) + +var _ plugin.CreateAPI = Plugin{} + +// Plugin implements the plugin.Full interface +type Plugin struct { + createAPISubcommand +} + +// Name returns the name of the plugin +func (Plugin) Name() string { return pluginName } + +// Version returns the version of the plugin +func (Plugin) Version() plugin.Version { return pluginVersion } + +// SupportedProjectVersions returns an array with all project versions supported by the plugin +func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } + +// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis +func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 608bc757ce0..e9a15ae0217 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -96,13 +96,10 @@ function scaffold_test_project { $kb create webhook --version v1 --kind Lakers --defaulting --programmatic-validation fi elif [[ $project =~ addon ]]; then - header_text 'enabling --pattern flag ...' - export KUBEBUILDER_ENABLE_PLUGINS=1 header_text 'Creating APIs ...' - $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false --pattern=addon - $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false --pattern=addon - $kb create api --group crew --version v1 --kind Admiral --controller=true --resource=true --namespaced=false --make=false --pattern=addon - unset KUBEBUILDER_ENABLE_PLUGINS + $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false + $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + $kb create api --group crew --version v1 --kind Admiral --controller=true --resource=true --namespaced=false --make=false fi make generate manifests @@ -116,9 +113,9 @@ build_kb # Project version 2 uses plugin go/v2 (default). scaffold_test_project project-v2 --project-version=2 scaffold_test_project project-v2-multigroup --project-version=2 -scaffold_test_project project-v2-addon --project-version=2 +scaffold_test_project project-v2-addon --project-version=3 --plugins="go/v2,declarative" # Project version 3 (default) uses plugin go/v3 (default). scaffold_test_project project-v3 scaffold_test_project project-v3-multigroup -scaffold_test_project project-v3-addon +scaffold_test_project project-v3-addon --plugins="go/v3,declarative" scaffold_test_project project-v3-config --component-config diff --git a/testdata/project-v2-addon/PROJECT b/testdata/project-v2-addon/PROJECT index 77f34e78733..b6ab134abfa 100644 --- a/testdata/project-v2-addon/PROJECT +++ b/testdata/project-v2-addon/PROJECT @@ -1,13 +1,32 @@ domain: testproject.org +layout: go.kubebuilder.io/v2,declarative.kubebuilder.io/v1 +projectName: project-v2-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v2-addon resources: -- group: crew +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: testproject.org + group: crew kind: Captain + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -- group: crew +- api: + crdVersion: v1beta1 + namespaced: true + controller: true + domain: testproject.org + group: crew kind: FirstMate + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -- group: crew +- api: + crdVersion: v1beta1 + controller: true + domain: testproject.org + group: crew kind: Admiral + path: sigs.k8s.io/kubebuilder/testdata/project-v2-addon/api/v1 version: v1 -version: "2" +version: "3" diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT index f5b6f774bc0..53ba275352b 100644 --- a/testdata/project-v3-addon/PROJECT +++ b/testdata/project-v3-addon/PROJECT @@ -1,5 +1,5 @@ domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: go.kubebuilder.io/v3,declarative.kubebuilder.io/v1 projectName: project-v3-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v3-addon resources: