From f81fb0dac88219a0ceaa84ef64c5b8d0ade9ca15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20W=C3=BCrbach?= Date: Fri, 5 Mar 2021 21:08:23 +0100 Subject: [PATCH 1/5] feat: improve service monitor configuration --- .../project/config/prometheus/monitor.yaml | 4 ++++ .../project/config/prometheus/monitor.yaml | 4 ++++ .../project/config/prometheus/monitor.yaml | 4 ++++ docs/book/src/reference/metrics.md | 16 ++++++++-------- .../templates/config/prometheus/monitor.go | 4 ++++ .../templates/config/prometheus/monitor.go | 4 ++++ .../config/prometheus/monitor.yaml | 4 ++++ .../config/prometheus/monitor.yaml | 4 ++++ .../project-v2/config/prometheus/monitor.yaml | 4 ++++ .../config/prometheus/monitor.yaml | 4 ++++ .../config/prometheus/monitor.yaml | 4 ++++ .../config/prometheus/monitor.yaml | 4 ++++ .../project-v3/config/prometheus/monitor.yaml | 4 ++++ 13 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/book/src/component-config-tutorial/testdata/project/config/prometheus/monitor.yaml b/docs/book/src/component-config-tutorial/testdata/project/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/docs/book/src/component-config-tutorial/testdata/project/config/prometheus/monitor.yaml +++ b/docs/book/src/component-config-tutorial/testdata/project/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor.yaml b/docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor.yaml +++ b/docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor.yaml b/docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor.yaml +++ b/docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/docs/book/src/reference/metrics.md b/docs/book/src/reference/metrics.md index efb7a31c5ff..78f3862e884 100644 --- a/docs/book/src/reference/metrics.md +++ b/docs/book/src/reference/metrics.md @@ -12,7 +12,7 @@ can be found at `config/rbac/auth_proxy_client_clusterrole.yaml`. You will need to grant permissions to your Prometheus server so that it can scrape the protected metrics. To achieve that, you can create a `clusterRoleBinding` to bind the `clusterRole` to the service account that your -Prometheus server uses. +Prometheus server uses. If you are using `kube-prometheus`, this cluster binding already exists. You can run the following kubectl command to create it. If using kubebuilder `` is the `namePrefix` field in `config/default/kustomization.yaml`. @@ -26,7 +26,7 @@ kubectl create clusterrolebinding metrics --clusterrole=-metrics Follow the steps below to export the metrics using the Prometheus Operator: 1. Install Prometheus and Prometheus Operator. -We recommend using [kube-prometheus](https://github.com/coreos/kube-prometheus#installing) +We recommend using [kube-prometheus](https://github.com/coreos/kube-prometheus#installing) in production if you don't have your own monitoring system. If you are just experimenting, you can only install Prometheus and Prometheus Operator. 2. Uncomment the line `- ../prometheus` in the `config/default/kustomization.yaml`. @@ -38,21 +38,21 @@ It creates the `ServiceMonitor` resource which enables exporting the metrics. ``` Note that, when you install your project in the cluster, it will create the -`ServiceMonitor` to export the metrics. To check the ServiceMonitor, +`ServiceMonitor` to export the metrics. To check the ServiceMonitor, run `kubectl get ServiceMonitor -n -system`. See an example: ``` -$ kubectl get ServiceMonitor -n monitor-system +$ kubectl get ServiceMonitor -n monitor-system NAME AGE monitor-controller-manager-metrics-monitor 2m8s ``` Also, notice that the metrics are exported by default through port `8443`. In this way, -you are able to check the Prometheus metrics in its dashboard. To verify it, search -for the metrics exported from the namespace where the project is running -`{namespace="-system"}`. See an example: +you are able to check the Prometheus metrics in its dashboard. To verify it, search +for the metrics exported from the namespace where the project is running +`{namespace="-system"}`. See an example: -Screenshot 2019-10-02 at 13 07 13 +Screenshot 2019-10-02 at 13 07 13 ## Publishing Additional Metrics diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go index 4ee7d32c1c2..601a10b28e1 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go @@ -53,6 +53,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go index 4ee7d32c1c2..601a10b28e1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go @@ -53,6 +53,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v2-addon/config/prometheus/monitor.yaml b/testdata/project-v2-addon/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v2-addon/config/prometheus/monitor.yaml +++ b/testdata/project-v2-addon/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v2-multigroup/config/prometheus/monitor.yaml b/testdata/project-v2-multigroup/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v2-multigroup/config/prometheus/monitor.yaml +++ b/testdata/project-v2-multigroup/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v2/config/prometheus/monitor.yaml b/testdata/project-v2/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v2/config/prometheus/monitor.yaml +++ b/testdata/project-v2/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v3-addon/config/prometheus/monitor.yaml b/testdata/project-v3-addon/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v3-addon/config/prometheus/monitor.yaml +++ b/testdata/project-v3-addon/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v3-config/config/prometheus/monitor.yaml b/testdata/project-v3-config/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v3-config/config/prometheus/monitor.yaml +++ b/testdata/project-v3-config/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v3-multigroup/config/prometheus/monitor.yaml b/testdata/project-v3-multigroup/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v3-multigroup/config/prometheus/monitor.yaml +++ b/testdata/project-v3-multigroup/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager diff --git a/testdata/project-v3/config/prometheus/monitor.yaml b/testdata/project-v3/config/prometheus/monitor.yaml index 9b8047b760f..d19136ae710 100644 --- a/testdata/project-v3/config/prometheus/monitor.yaml +++ b/testdata/project-v3/config/prometheus/monitor.yaml @@ -11,6 +11,10 @@ spec: endpoints: - path: /metrics port: https + scheme: https + bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token + tlsConfig: + insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager From bdfa59704be0cf671da0dccbd858faa92ea6ff23 Mon Sep 17 00:00:00 2001 From: leovct Date: Sat, 6 Mar 2021 12:20:11 +0100 Subject: [PATCH 2/5] Use client.New instead of manager client in tests --- .../project/controllers/suite_test.go | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go b/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go index 0702a3a251e..38b3022923b 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go +++ b/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go @@ -25,9 +25,10 @@ package controllers import ( "path/filepath" - ctrl "sigs.k8s.io/controller-runtime" "testing" + ctrl "sigs.k8s.io/controller-runtime" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" @@ -112,8 +113,14 @@ var _ = BeforeSuite(func() { The only difference is that the manager is started in a separate goroutine so it does not block the cleanup of envtest when you’re done running your tests. - Once you've added the code below, you can actually delete the k8sClient above, because you can get k8sClient from the manager - (as shown below). + It is not recommended to use the manager client in tests because it is not strongly consistent. Indeed, the manager + client is designed to do the "right thing" for controllers by default which is to read from caches. The best solution + is to instantiate a new client using client.New for tests (as k8sClient above). It will provide a client that reads + directly from the API meaning that you can write tests expecting read-after-write consistency. + + However, keep in mind that you should not do this in the controller's conciliation loop (read an object after you have + written it). Kubernetes favors an approach where you first do some reads, process and then do some writes and return. + This way, you let the queue take care of the next cycle of readings if they are necessary. */ k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ @@ -122,7 +129,7 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) err = (&CronJobReconciler{ - Client: k8sManager.GetClient(), + Client: k8sClient, Scheme: k8sManager.GetScheme(), Log: ctrl.Log.WithName("controllers").WithName("CronJob"), }).SetupWithManager(k8sManager) @@ -132,10 +139,6 @@ var _ = BeforeSuite(func() { err = k8sManager.Start(ctrl.SetupSignalHandler()) Expect(err).ToNot(HaveOccurred()) }() - - k8sClient = k8sManager.GetClient() - Expect(k8sClient).ToNot(BeNil()) - }, 60) /* From 83230ae05376b53dd2db94a15aa9809ce8e90565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Orive?= Date: Tue, 9 Mar 2021 14:36:12 +0100 Subject: [PATCH 3/5] Improve plugin phase 1.5 EP Signed-off-by: Adrian Orive --- ...e-cli-and-scaffolding-plugins-phase-1-5.md | 137 ++++++++++++++---- 1 file changed, 107 insertions(+), 30 deletions(-) diff --git a/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md b/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md index 73f84ec9db5..e96e9a0f649 100644 --- a/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md +++ b/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md @@ -8,6 +8,9 @@ The goal of this phase is to achieve one of the goals proposed for Phase 2: chai Phase 2 includes several other challenging goals, but being able to chain plugins will be beneficial for third-party developers that are using kubebuilder as a library. +The other main goal of phase 2, discovering and using external plugins, is out of the scope of this phase, +and will be tackled when phase 2 is implemented. + ## Table of contents - [Goal](#goal) - [Motivation](#motivation) @@ -46,36 +49,42 @@ Plugin chaining solves the aforementioned problems but the current plugin API, a Design a Plugin API that combines the current [`Subcommand`](../pkg/plugin/interfaces.go) and [`RunOptions`](../pkg/plugins/internal/cmdutil/cmdutil.go) interfaces and enables plugin-chaining. -The new `Subcommand` methods can be split in two different categories: -- Initialization methods -- Execution methods +The new `Subcommand` hooks can be split in two different categories: +- Initialization hooks +- Execution hooks + +Initialization hooks are run during the dynamic creation of the CLI, which means that they are able to +modify the CLI, e.g. providing descriptions and examples for subcommands or binding flags. +Execution hooks are run after the CLI is created, and therefore cannot modify the CLI. On the other hand, +as they are run during the CLI execution, they have access to user-provided flag values, project configuration, +the new API resource or the filesystem abstraction, as opposed to the initialization hooks. -Additionally, some of these methods may be optional, in which case a non-implemented method will be skipped -when it should be called and consider it succeeded. This also allows to create some methods specific for -a certain subcommand call (e.g.: `Resource`-related methods for the `edit` subcommand are not needed). +Additionally, some of these hooks may be optional, in which case a non-implemented hook will be skipped +when it should be called and consider it succeeded. This also allows to create some hooks specific for +a certain subcommand call (e.g.: `Resource`-related hooks for the `edit` subcommand are not needed). Different ordering guarantees can be considered: -- Method order guarantee: a method for a plugin will be called after its previous methods succeeded. -- Steps order guarantee: methods will be called when all plugins have finished the previous method. -- Plugin order guarantee: same method for each plugin will be called in the order specified +- Hook order guarantee: a hook for a plugin will be called after its previous hooks succeeded. +- Steps order guarantee: hooks will be called when all plugins have finished the previous hook. +- Plugin order guarantee: same hook for each plugin will be called in the order specified by the plugin position at the plugin chain. -All of the methods will offer plugin order guarantee, as they all modify/update some item so the order -of plugins is important. Execution methods need to guarantee step order, as the items that are being modified +All of the hooks will offer plugin order guarantee, as they all modify/update some item so the order +of plugins is important. Execution hooks need to guarantee step order, as the items that are being modified in each step (config, resource, and filesystem) are also needed in the following steps. This is not true for -initialization methods that modify items (metadata and flagset) that are only used in their own methods, -so they only need to guarantee method order. +initialization hooks that modify items (metadata and flagset) that are only used in their own methods, +so they only need to guarantee hook order. -Execution methods will be able to return an error. A specific error can be returned to specify that -no further methods of this plugin should be called, but that the scaffold process should be continued. +Execution hooks will be able to return an error. A specific error can be returned to specify that +no further hooks of this plugin should be called, but that the scaffold process should be continued. This enables plugins to exit early, e.g., a plugin that scaffolds some files only for cluster-scoped resources can detect if the resource is cluster-scoped at one of the first execution steps, and therefore, use this error to tell the CLI that no further execution step should be called for itself. -### Initialization methods +### Initialization hooks #### Update metadata -This method will be used for two purposes. It provides CLI-related metadata to the Subcommand (e.g., +This hook will be used for two purposes. It provides CLI-related metadata to the Subcommand (e.g., command name) and update the subcommands metadata such as the description or examples. - Required/optional @@ -88,7 +97,7 @@ command name) and update the subcommands metadata such as the description or exa - [x] Create webhook #### Bind flags -This method will allow subcommands to define specific flags. +This hook will allow subcommands to define specific flags. - Required/optional - [ ] Required @@ -102,7 +111,7 @@ This method will allow subcommands to define specific flags. ### Execution methods #### Inject configuration -This method will be used to inject the `Config` object that the plugin can modify at will. +This hook will be used to inject the `Config` object that the plugin can modify at will. The CLI will create/load/save this configuration object. - Required/optional @@ -115,7 +124,7 @@ The CLI will create/load/save this configuration object. - [x] Create webhook #### Inject resource -This method will be used to inject the `Resource` object. +This hook will be used to inject the `Resource` object created by the CLI. - Required/optional - [x] Required @@ -127,9 +136,9 @@ This method will be used to inject the `Resource` object. - [x] Create webhook #### Pre-scaffold -This method will be used to take actions before the main scaffolding is performed, e.g. validations. +This hook will be used to take actions before the main scaffolding is performed, e.g. validations. -NOTE: a filesystem abstraction will be passed to this method that must be used for scaffolding. +NOTE: a filesystem abstraction will be passed to this hook, but it should not be used for scaffolding. - Required/optional - [ ] Required @@ -141,9 +150,9 @@ NOTE: a filesystem abstraction will be passed to this method that must be used f - [x] Create webhook #### Scaffold -This method will be used to perform the main scaffolding. +This hook will be used to perform the main scaffolding. -NOTE: a filesystem abstraction will be passed to this method that must be used for scaffolding. +NOTE: a filesystem abstraction will be passed to this hook that must be used for scaffolding. - Required/optional - [x] Required @@ -155,11 +164,14 @@ NOTE: a filesystem abstraction will be passed to this method that must be used f - [x] Create webhook #### Post-scaffold -This method will be used to take actions after the main scaffolding is performed, e.g. cleanup. +This hook will be used to take actions after the main scaffolding is performed, e.g. cleanup. -NOTE: a filesystem abstraction will **NOT** be passed to this method, as post-scaffold task do not require it. +NOTE: a filesystem abstraction will **NOT** be passed to this hook, as post-scaffold task do not require it. In case some post-scaffold task requires a filesystem abstraction, it could be added. +NOTE 2: the project configuration is saved by the CLI before calling this hook, so changes done to the +configuration at this hook will not be persisted. + - Required/optional - [ ] Required - [x] Optional @@ -168,10 +180,67 @@ In case some post-scaffold task requires a filesystem abstraction, it could be a - [x] Edit - [x] Create API - [x] Create webhook + +### Override plugins for single subcommand calls + +Defining plugins at initialization and using them for every command call will solve most of the cases. +However, there are some cases where a plugin may be wanted just for a certain subcommand call. For +example, a project with multiple controllers may want to follow the declarative pattern in only one of +their controllers. The other case is also relevant, a project where most of the controllers follow the +declarative pattern may need a single controller not to follow it. + +In order to achieve this, the `--plugins` flag will be allowed in every command call, overriding the +value used in its corresponging project initialization call. + +### Plugin chain persistence + +Currently, the project configuration v3 offers two mechanisms for storing plugin-related information. + +- A layout field (`string`) that is used for plugin resolution on initialized projects. +- A plugin field (`map[string]interface{}`) that is used for plugin configuration raw storage. + +Plugin resolution uses the `layout` field to resolve plugins. In this phase, it has to store a plugin +chain and not a single plugin. As this value is stored as a string, comma-separated representation can +be used to represent a chain of plugins instead. + +NOTE: commas are not allowed in the plugin key. + +While the `plugin` field may seem like a better fit to store the plugin chain, as it can already +contain multiple values, there are several issues with this alternative approach: +- A map does not provide any order guarantee, and the plugin chain order is relevant. +- Some plugins do not store plugin-specific configuration information, e.g. the `go`-plugins. So + the absence of a plugin key doesn't mean that the plugin is not part of the plugin chain. +- The desire of running a different set of plugins for a single subcommand call has already been + mentioned. Some of these out-of-chain plugins may need to store plugin-specific configuration, + so the presence of a plugin doesn't mean that is part of the plugin chain. + +The next project configuration version could consider this new requirements to define the +names/types of these two fields. + +### Plugin bundle + +As a side-effect of plugin chaining, the user experience may suffer if they need to provide +several plugin keys for the `--plugins` flag. Additionally, this would also mean a user-facing +important breaking change. + +In order to solve this issue, a plugin bundle concept will be introduced. A plugin bundle +behaves as a plugin: +- It has a name: provided at creation. +- It has a version: provided at creation. +- It has a list of supported project versions: computed from the common supported project + versions of all the plugins in the bundled. + +Instead of implementing the optional getter methods that return a subcommand, it offers a way +to retrieve the list of bundled plugins. This process will be done after plugin resolution. + +This way, CLIs will be able to define bundles, which will be used in the user-facing API and +the plugin resolution process, but later they will be treated as separate plugins offering +the maintainability and separation of concerns advantages that smaller plugins have in +comparison with bigger monolithic plugins. ## Implementation -The following types are used as input/output values of the described methods: +The following types are used as input/output values of the described hooks: ```go // CLIMetadata is the runtime meta-data of the CLI type CLIMetadata struct { @@ -197,7 +266,7 @@ func (e ExitError) Error() string { } ``` -The described methods are implemented through the use of the following interfaces. +The described hooks are implemented through the use of the following interfaces. ```go type RequiresCLIMetadata interface { InjectCLIMetadata(CLIMetadata) @@ -220,11 +289,11 @@ type RequiresResource interface { } type HasPreScaffold interface { - PreScaffold(afero.Fs) error + PreScaffold(machinery.Filesystem) error } type Scaffolder interface { - Scaffold(afero.Fs) error + Scaffold(machinery.Filesystem) error } type HasPostScaffold interface { @@ -256,3 +325,11 @@ type CreateWebhookSubcommand interface { Scaffolder } ``` + +An additional interface defines the bundle method to return the wrapped plugins: +```go +type Bundle interface { + Plugin + Plugins() []Plugin +} +``` From 668b999b0d303e8ce6e13ef71087d3ad3192c8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Orive?= Date: Wed, 17 Mar 2021 14:33:16 +0100 Subject: [PATCH 4/5] Enable the storage of multiple plugins as layout Use a custom type to ensure backwards compatibility: older configuration files will unmarshall properly despite not being an array and will marshal to the new format. Signed-off-by: Adrian Orive --- pkg/cli/cli.go | 10 +-- pkg/cli/cli_test.go | 8 +- pkg/config/interface.go | 60 +++++++------- pkg/config/store/yaml/store.go | 4 +- pkg/config/v2/config.go | 16 ++-- pkg/config/v2/config_test.go | 28 +++---- pkg/config/v3/config.go | 43 +++++++--- pkg/config/v3/config_test.go | 105 ++++++++++++++----------- pkg/plugins/golang/v2/init.go | 2 +- pkg/plugins/golang/v3/init.go | 2 +- testdata/project-v3-addon/PROJECT | 3 +- testdata/project-v3-config/PROJECT | 3 +- testdata/project-v3-multigroup/PROJECT | 3 +- testdata/project-v3/PROJECT | 3 +- 14 files changed, 162 insertions(+), 128 deletions(-) diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 4a432318560..403a4941b58 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -220,15 +220,7 @@ func (c CLI) getInfoFromConfigFile() (config.Version, []string, error) { // getInfoFromConfig obtains the project version and plugin keys from the project config. // It is extracted from getInfoFromConfigFile for testing purposes. func getInfoFromConfig(projectConfig config.Config) (config.Version, []string, error) { - // Split the comma-separated plugins - var pluginSet []string - if projectConfig.GetLayout() != "" { - for _, p := range strings.Split(projectConfig.GetLayout(), ",") { - pluginSet = append(pluginSet, strings.TrimSpace(p)) - } - } - - return projectConfig.GetVersion(), pluginSet, nil + return projectConfig.GetVersion(), projectConfig.GetPluginChain(), nil } // resolveFlagsAndConfigFileConflicts checks if the provided combined input from flags and diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index 7f77beb8171..a7084980119 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -191,6 +191,8 @@ var _ = Describe("CLI", func() { projectVersion config.Version plugins []string err error + + pluginChain = []string{"go.kubebuilder.io/v2"} ) When("not having layout field", func() { @@ -199,18 +201,18 @@ var _ = Describe("CLI", func() { projectVersion, plugins, err = getInfoFromConfig(projectConfig) Expect(err).NotTo(HaveOccurred()) Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(len(plugins)).To(Equal(0)) + Expect(plugins).To(Equal(pluginChain)) }) }) When("having layout field", func() { It("should succeed", func() { projectConfig = cfgv3.New() - Expect(projectConfig.SetLayout("go.kubebuilder.io/v2")).To(Succeed()) + Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) projectVersion, plugins, err = getInfoFromConfig(projectConfig) Expect(err).NotTo(HaveOccurred()) Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(plugins).To(Equal([]string{projectConfig.GetLayout()})) + Expect(plugins).To(Equal(pluginChain)) }) }) }) diff --git a/pkg/config/interface.go b/pkg/config/interface.go index e6aacf38ec0..3d0e0e2c110 100644 --- a/pkg/config/interface.go +++ b/pkg/config/interface.go @@ -20,78 +20,78 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) -// Config defines the interface that project configuration types must follow +// Config defines the interface that project configuration types must follow. type Config interface { /* Version */ - // GetVersion returns the current project version + // GetVersion returns the current project version. GetVersion() Version /* String fields */ - // GetDomain returns the project domain + // GetDomain returns the project domain. GetDomain() string - // SetDomain sets the project domain + // SetDomain sets the project domain. SetDomain(domain string) error // GetRepository returns the project repository. GetRepository() string - // SetRepository sets the project repository + // SetRepository sets the project repository. SetRepository(repository string) error - // GetProjectName returns the project name + // GetProjectName returns the project name. // This method was introduced in project version 3. GetProjectName() string - // SetProjectName sets the project name + // SetProjectName sets the project name. // This method was introduced in project version 3. SetProjectName(name string) error - // GetLayout returns the config layout + // GetPluginChain returns the plugin chain. // This method was introduced in project version 3. - GetLayout() string - // SetLayout sets the Config layout + GetPluginChain() []string + // SetPluginChain sets the plugin chain. // This method was introduced in project version 3. - SetLayout(layout string) error + SetPluginChain(pluginChain []string) error /* Boolean fields */ - // IsMultiGroup checks if multi-group is enabled + // IsMultiGroup checks if multi-group is enabled. IsMultiGroup() bool - // SetMultiGroup enables multi-group + // SetMultiGroup enables multi-group. SetMultiGroup() error - // ClearMultiGroup disables multi-group + // ClearMultiGroup disables multi-group. ClearMultiGroup() error - // IsComponentConfig checks if component config is enabled + // IsComponentConfig checks if component config is enabled. // This method was introduced in project version 3. IsComponentConfig() bool - // SetComponentConfig enables component config + // SetComponentConfig enables component config. // This method was introduced in project version 3. SetComponentConfig() error - // ClearComponentConfig disables component config + // ClearComponentConfig disables component config. // This method was introduced in project version 3. ClearComponentConfig() error /* Resources */ - // ResourcesLength returns the number of tracked resources + // ResourcesLength returns the number of tracked resources. ResourcesLength() int - // HasResource checks if the provided GVK is stored in the Config + // HasResource checks if the provided GVK is stored in the Config. HasResource(gvk resource.GVK) bool - // GetResource returns the stored resource matching the provided GVK + // GetResource returns the stored resource matching the provided GVK. GetResource(gvk resource.GVK) (resource.Resource, error) - // GetResources returns all the stored resources + // GetResources returns all the stored resources. GetResources() ([]resource.Resource, error) - // AddResource adds the provided resource if it was not present, no-op if it was already present + // AddResource adds the provided resource if it was not present, no-op if it was already present. AddResource(res resource.Resource) error - // UpdateResource adds the provided resource if it was not present, modifies it if it was already present + // UpdateResource adds the provided resource if it was not present, modifies it if it was already present. UpdateResource(res resource.Resource) error - // HasGroup checks if the provided group is the same as any of the tracked resources + // HasGroup checks if the provided group is the same as any of the tracked resources. HasGroup(group string) bool - // ListCRDVersions returns a list of the CRD versions in use by the tracked resources + // ListCRDVersions returns a list of the CRD versions in use by the tracked resources. ListCRDVersions() []string - // ListWebhookVersions returns a list of the webhook versions in use by the tracked resources + // ListWebhookVersions returns a list of the webhook versions in use by the tracked resources. ListWebhookVersions() []string /* Plugins */ @@ -105,8 +105,8 @@ type Config interface { /* Persistence */ - // Marshal returns the YAML representation of the Config - Marshal() ([]byte, error) - // Unmarshal loads the Config fields from its YAML representation - Unmarshal([]byte) error + // Marshal returns the YAML representation of the Config. + MarshalYAML() ([]byte, error) + // Unmarshal loads the Config fields from its YAML representation. + UnmarshalYAML([]byte) error } diff --git a/pkg/config/store/yaml/store.go b/pkg/config/store/yaml/store.go index 236a4fd0df5..2065a4557e6 100644 --- a/pkg/config/store/yaml/store.go +++ b/pkg/config/store/yaml/store.go @@ -94,7 +94,7 @@ func (s *yamlStore) LoadFrom(path string) error { } // Unmarshal the file content - if err := cfg.Unmarshal(in); err != nil { + if err := cfg.UnmarshalYAML(in); err != nil { return store.LoadError{Err: fmt.Errorf("unable to unmarshal config at %q: %w", path, err)} } @@ -128,7 +128,7 @@ func (s yamlStore) SaveTo(path string) error { } // Marshall into YAML - content, err := s.cfg.Marshal() + content, err := s.cfg.MarshalYAML() if err != nil { return store.SaveError{Err: fmt.Errorf("unable to marshal to YAML: %w", err)} } diff --git a/pkg/config/v2/config.go b/pkg/config/v2/config.go index c7dc55ad38b..a01f6c06053 100644 --- a/pkg/config/v2/config.go +++ b/pkg/config/v2/config.go @@ -92,16 +92,16 @@ func (c *cfg) SetProjectName(string) error { } } -// GetLayout implements config.Config -func (c cfg) GetLayout() string { - return "" +// GetPluginChain implements config.Config +func (c cfg) GetPluginChain() []string { + return []string{"go.kubebuilder.io/v2"} } -// SetLayout implements config.Config -func (c *cfg) SetLayout(string) error { +// SetPluginChain implements config.Config +func (c *cfg) SetPluginChain([]string) error { return config.UnsupportedFieldError{ Version: Version, - Field: "layout", + Field: "plugin chain", } } @@ -247,7 +247,7 @@ func (c cfg) EncodePluginConfig(string, interface{}) error { } // Marshal implements config.Config -func (c cfg) Marshal() ([]byte, error) { +func (c cfg) MarshalYAML() ([]byte, error) { content, err := yaml.Marshal(c) if err != nil { return nil, config.MarshalError{Err: err} @@ -257,7 +257,7 @@ func (c cfg) Marshal() ([]byte, error) { } // Unmarshal implements config.Config -func (c *cfg) Unmarshal(b []byte) error { +func (c *cfg) UnmarshalYAML(b []byte) error { if err := yaml.UnmarshalStrict(b, c); err != nil { return config.UnmarshalError{Err: err} } diff --git a/pkg/config/v2/config_test.go b/pkg/config/v2/config_test.go index 4773b0e9a3f..fc42045b2ff 100644 --- a/pkg/config/v2/config_test.go +++ b/pkg/config/v2/config_test.go @@ -78,7 +78,7 @@ var _ = Describe("cfg", func() { }) }) - Context("ProjectName", func() { + Context("Project name", func() { It("GetProjectName should return an empty name", func() { Expect(c.GetProjectName()).To(Equal("")) }) @@ -88,13 +88,13 @@ var _ = Describe("cfg", func() { }) }) - Context("Layout", func() { - It("GetLayout should return an empty layout", func() { - Expect(c.GetLayout()).To(Equal("")) + Context("Plugin chain", func() { + It("GetPluginChain should return the only supported plugin", func() { + Expect(c.GetPluginChain()).To(Equal([]string{"go.kubebuilder.io/v2"})) }) - It("SetLayout should fail to set the layout", func() { - Expect(c.SetLayout("layout")).NotTo(Succeed()) + It("SetPluginChain should fail to set the plugin chain", func() { + Expect(c.SetPluginChain([]string{})).NotTo(Succeed()) }) }) @@ -288,9 +288,9 @@ version: "2" ` ) - DescribeTable("Marshal should succeed", + DescribeTable("MarshalYAML should succeed", func(c cfg, content string) { - b, err := c.Marshal() + b, err := c.MarshalYAML() Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(content)) }, @@ -298,18 +298,18 @@ version: "2" Entry("for a full configuration", c2, s2), ) - DescribeTable("Marshal should fail", + DescribeTable("MarshalYAML should fail", func(c cfg) { - _, err := c.Marshal() + _, err := c.MarshalYAML() Expect(err).To(HaveOccurred()) }, // TODO (coverage): add cases where yaml.Marshal returns an error ) - DescribeTable("Unmarshal should succeed", + DescribeTable("UnmarshalYAML should succeed", func(content string, c cfg) { var unmarshalled cfg - Expect(unmarshalled.Unmarshal([]byte(content))).To(Succeed()) + Expect(unmarshalled.UnmarshalYAML([]byte(content))).To(Succeed()) Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0)) Expect(unmarshalled.Domain).To(Equal(c.Domain)) Expect(unmarshalled.Repository).To(Equal(c.Repository)) @@ -320,10 +320,10 @@ version: "2" Entry("full", s2, c2), ) - DescribeTable("Unmarshal should fail", + DescribeTable("UnmarshalYAML should fail", func(content string) { var c cfg - Expect(c.Unmarshal([]byte(content))).NotTo(Succeed()) + Expect(c.UnmarshalYAML([]byte(content))).NotTo(Succeed()) }, Entry("for unknown fields", `field: 1 version: "2"`), diff --git a/pkg/config/v3/config.go b/pkg/config/v3/config.go index b2d2de713a8..7c5655b1519 100644 --- a/pkg/config/v3/config.go +++ b/pkg/config/v3/config.go @@ -29,15 +29,38 @@ import ( // Version is the config.Version for project configuration 3 var Version = config.Version{Number: 3} +// stringSlice is a []string but that can also be unmarshalled from a single string, +// which is introduced as the first and only element of the slice +// It is used to offer backwards compatibility as the field used to be a string. +type stringSlice []string + +func (ss *stringSlice) UnmarshalJSON(b []byte) error { + if b[0] == '[' { + var sl []string + if err := yaml.Unmarshal(b, &sl); err != nil { + return err + } + *ss = sl + return nil + } + + var st string + if err := yaml.Unmarshal(b, &st); err != nil { + return err + } + *ss = stringSlice{st} + return nil +} + type cfg struct { // Version Version config.Version `json:"version"` // String fields - Domain string `json:"domain,omitempty"` - Repository string `json:"repo,omitempty"` - Name string `json:"projectName,omitempty"` - Layout string `json:"layout,omitempty"` + Domain string `json:"domain,omitempty"` + Repository string `json:"repo,omitempty"` + Name string `json:"projectName,omitempty"` + PluginChain stringSlice `json:"layout,omitempty"` // Boolean fields MultiGroup bool `json:"multigroup,omitempty"` @@ -104,13 +127,13 @@ func (c *cfg) SetProjectName(name string) error { } // GetLayout implements config.Config -func (c cfg) GetLayout() string { - return c.Layout +func (c cfg) GetPluginChain() []string { + return c.PluginChain } // SetLayout implements config.Config -func (c *cfg) SetLayout(layout string) error { - c.Layout = layout +func (c *cfg) SetPluginChain(pluginChain []string) error { + c.PluginChain = pluginChain return nil } @@ -324,7 +347,7 @@ func (c *cfg) EncodePluginConfig(key string, configObj interface{}) error { } // Marshal implements config.Config -func (c cfg) Marshal() ([]byte, error) { +func (c cfg) MarshalYAML() ([]byte, error) { for i, r := range c.Resources { // If API is empty, omit it (prevents `api: {}`). if r.API != nil && r.API.IsEmpty() { @@ -345,7 +368,7 @@ func (c cfg) Marshal() ([]byte, error) { } // Unmarshal implements config.Config -func (c *cfg) Unmarshal(b []byte) error { +func (c *cfg) UnmarshalYAML(b []byte) error { if err := yaml.UnmarshalStrict(b, c); err != nil { return config.UnmarshalError{Err: err} } diff --git a/pkg/config/v3/config_test.go b/pkg/config/v3/config_test.go index a8eb4af5939..8450f230887 100644 --- a/pkg/config/v3/config_test.go +++ b/pkg/config/v3/config_test.go @@ -39,23 +39,27 @@ var _ = Describe("cfg", func() { domain = "my.domain" repo = "myrepo" name = "ProjectName" - layout = "go.kubebuilder.io/v2" otherDomain = "other.domain" otherRepo = "otherrepo" otherName = "OtherProjectName" - otherLayout = "go.kubebuilder.io/v3" ) - var c cfg + var ( + c cfg + + pluginChain = []string{"go.kubebuilder.io/v2"} + + otherPluginChain = []string{"go.kubebuilder.io/v3"} + ) BeforeEach(func() { c = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, } }) @@ -87,7 +91,7 @@ var _ = Describe("cfg", func() { }) }) - Context("ProjectName", func() { + Context("Project name", func() { It("GetProjectName should return the name", func() { Expect(c.GetProjectName()).To(Equal(name)) }) @@ -98,14 +102,14 @@ var _ = Describe("cfg", func() { }) }) - Context("Layout", func() { - It("GetLayout should return the layout", func() { - Expect(c.GetLayout()).To(Equal(layout)) + Context("Plugin chain", func() { + It("GetPluginChain should return the plugin chain", func() { + Expect(c.GetPluginChain()).To(Equal(pluginChain)) }) - It("SetLayout should set the layout", func() { - Expect(c.SetLayout(otherLayout)).To(Succeed()) - Expect(c.Layout).To(Equal(otherLayout)) + It("SetPluginChain should set the plugin chain", func() { + Expect(c.SetPluginChain(otherPluginChain)).To(Succeed()) + Expect([]string(c.PluginChain)).To(Equal(otherPluginChain)) }) }) @@ -374,18 +378,18 @@ var _ = Describe("cfg", func() { var ( c0 = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, } c1 = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, Plugins: pluginConfigs{ key: map[string]interface{}{ "data-1": "", @@ -393,11 +397,11 @@ var _ = Describe("cfg", func() { }, } c2 = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, Plugins: pluginConfigs{ key: map[string]interface{}{ "data-1": "plugin value 1", @@ -453,18 +457,18 @@ var _ = Describe("cfg", func() { var ( // BeforeEach is called after the entries are evaluated, and therefore, c is not available c1 = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, } c2 = cfg{ Version: Version, Domain: otherDomain, Repository: otherRepo, Name: otherName, - Layout: otherLayout, + PluginChain: otherPluginChain, MultiGroup: true, ComponentConfig: true, Resources: []resource.Resource{ @@ -527,6 +531,13 @@ var _ = Describe("cfg", func() { } // TODO: include cases with Path when added s1 = `domain: my.domain +layout: +- go.kubebuilder.io/v2 +projectName: ProjectName +repo: myrepo +version: "3" +` + s1bis = `domain: my.domain layout: go.kubebuilder.io/v2 projectName: ProjectName repo: myrepo @@ -534,7 +545,8 @@ version: "3" ` s2 = `componentConfig: true domain: other.domain -layout: go.kubebuilder.io/v3 +layout: +- go.kubebuilder.io/v3 multigroup: true plugins: plugin-x: @@ -579,9 +591,9 @@ version: "3" ` ) - DescribeTable("Marshal should succeed", + DescribeTable("MarshalYAML should succeed", func(c cfg, content string) { - b, err := c.Marshal() + b, err := c.MarshalYAML() Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(content)) }, @@ -589,23 +601,23 @@ version: "3" Entry("for a full configuration", c2, s2), ) - DescribeTable("Marshal should fail", + DescribeTable("MarshalYAML should fail", func(c cfg) { - _, err := c.Marshal() + _, err := c.MarshalYAML() Expect(err).To(HaveOccurred()) }, // TODO (coverage): add cases where yaml.Marshal returns an error ) - DescribeTable("Unmarshal should succeed", + DescribeTable("UnmarshalYAML should succeed", func(content string, c cfg) { var unmarshalled cfg - Expect(unmarshalled.Unmarshal([]byte(content))).To(Succeed()) + Expect(unmarshalled.UnmarshalYAML([]byte(content))).To(Succeed()) Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0)) Expect(unmarshalled.Domain).To(Equal(c.Domain)) Expect(unmarshalled.Repository).To(Equal(c.Repository)) Expect(unmarshalled.Name).To(Equal(c.Name)) - Expect(unmarshalled.Layout).To(Equal(c.Layout)) + Expect(unmarshalled.PluginChain).To(Equal(c.PluginChain)) Expect(unmarshalled.MultiGroup).To(Equal(c.MultiGroup)) Expect(unmarshalled.ComponentConfig).To(Equal(c.ComponentConfig)) Expect(unmarshalled.Resources).To(Equal(c.Resources)) @@ -614,12 +626,13 @@ version: "3" }, Entry("basic", s1, c1), Entry("full", s2, c2), + Entry("string layout", s1bis, c1), ) - DescribeTable("Unmarshal should fail", + DescribeTable("UnmarshalYAML should fail", func(content string) { var c cfg - Expect(c.Unmarshal([]byte(content))).NotTo(Succeed()) + Expect(c.UnmarshalYAML([]byte(content))).NotTo(Succeed()) }, Entry("for unknown fields", `field: 1 version: "3"`), diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index d5a345030bd..550e7d6d75c 100644 --- a/pkg/plugins/golang/v2/init.go +++ b/pkg/plugins/golang/v2/init.go @@ -102,7 +102,7 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { func (p *initSubcommand) InjectConfig(c config.Config) { // v2+ project configs get a 'layout' value. if c.GetVersion().Compare(cfgv2.Version) > 0 { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) + _ = c.SetPluginChain([]string{plugin.KeyFor(Plugin{})}) } p.config = c diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index d8c8ee7d5b1..8cc17170fcb 100644 --- a/pkg/plugins/golang/v3/init.go +++ b/pkg/plugins/golang/v3/init.go @@ -101,7 +101,7 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { } func (p *initSubcommand) InjectConfig(c config.Config) { - _ = c.SetLayout(plugin.KeyFor(Plugin{})) + _ = c.SetPluginChain([]string{plugin.KeyFor(Plugin{})}) p.config = c } diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT index f5b6f774bc0..7a6103d2fcf 100644 --- a/testdata/project-v3-addon/PROJECT +++ b/testdata/project-v3-addon/PROJECT @@ -1,5 +1,6 @@ domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: +- go.kubebuilder.io/v3 projectName: project-v3-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v3-addon resources: diff --git a/testdata/project-v3-config/PROJECT b/testdata/project-v3-config/PROJECT index 804ab898289..83e7f357591 100644 --- a/testdata/project-v3-config/PROJECT +++ b/testdata/project-v3-config/PROJECT @@ -1,6 +1,7 @@ componentConfig: true domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: +- go.kubebuilder.io/v3 projectName: project-v3-config repo: sigs.k8s.io/kubebuilder/testdata/project-v3-config resources: diff --git a/testdata/project-v3-multigroup/PROJECT b/testdata/project-v3-multigroup/PROJECT index 4d23fb47636..fa59fa454f5 100644 --- a/testdata/project-v3-multigroup/PROJECT +++ b/testdata/project-v3-multigroup/PROJECT @@ -1,5 +1,6 @@ domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: +- go.kubebuilder.io/v3 multigroup: true projectName: project-v3-multigroup repo: sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup diff --git a/testdata/project-v3/PROJECT b/testdata/project-v3/PROJECT index 8dde5b76ffe..8ef4c5b14ac 100644 --- a/testdata/project-v3/PROJECT +++ b/testdata/project-v3/PROJECT @@ -1,5 +1,6 @@ domain: testproject.org -layout: go.kubebuilder.io/v3 +layout: +- go.kubebuilder.io/v3 projectName: project-v3 repo: sigs.k8s.io/kubebuilder/testdata/project-v3 resources: From 29f646c38ed0d17f5e328c51a3aa6b6b3f762346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Orive?= Date: Mon, 15 Mar 2021 08:45:59 +0100 Subject: [PATCH 5/5] Plugin phase 1.5 implementation Signed-off-by: Adrian Orive --- cmd/main.go | 4 +- pkg/cli/api.go | 77 +- pkg/cli/cli.go | 419 +++++----- pkg/cli/cli_test.go | 768 ++++++------------ pkg/cli/cmd_helpers.go | 305 ++++++- pkg/cli/edit.go | 77 +- pkg/cli/init.go | 114 +-- pkg/cli/options.go | 38 +- pkg/cli/options_test.go | 113 ++- pkg/cli/resource.go | 85 ++ pkg/cli/resource_test.go | 110 +++ pkg/cli/root.go | 35 +- pkg/cli/{cli_suite_test.go => suite_test.go} | 0 pkg/cli/webhook.go | 77 +- pkg/machinery/errors.go | 14 +- pkg/machinery/errors_test.go | 1 - pkg/{model/file => machinery}/file.go | 16 +- pkg/{model/file => machinery}/funcmap.go | 11 +- pkg/machinery/funcmap_test.go | 45 + pkg/machinery/injector.go | 66 ++ pkg/machinery/injector_test.go | 295 +++++++ pkg/{model/file => machinery}/interfaces.go | 14 +- pkg/{model/file => machinery}/marker.go | 8 +- pkg/machinery/marker_test.go | 47 ++ pkg/{model/file => machinery}/mixins.go | 28 +- pkg/machinery/mixins_test.go | 188 +++++ pkg/machinery/scaffold.go | 112 +-- pkg/machinery/scaffold_test.go | 183 ++--- pkg/model/resource/gvk.go | 13 + pkg/model/resource/resource_test.go | 33 + pkg/model/universe.go | 70 -- pkg/plugin/bundle.go | 67 ++ pkg/plugin/bundle_test.go | 101 +++ pkg/{model/plugin.go => plugin/errors.go} | 22 +- .../channel.go => pkg/plugin/errors_test.go | 34 +- pkg/plugin/filter.go | 61 ++ pkg/plugin/filter_test.go | 84 ++ pkg/plugin/helpers.go | 45 +- pkg/plugin/helpers_test.go | 190 +++++ pkg/plugin/metadata.go | 31 + pkg/plugin/{interfaces.go => plugin.go} | 57 +- pkg/plugin/subcommand.go | 94 +++ pkg/plugin/suite_test.go | 41 + pkg/plugin/version_test.go | 23 +- pkg/plugins/declarative/v1/api.go | 150 ++++ .../v1/internal/templates/channel.go | 52 ++ .../v1/internal/templates}/controller.go | 56 +- .../v1/internal/templates/manifest.go | 52 ++ .../v1/internal/templates/types.go | 75 +- pkg/plugins/declarative/v1/plugin.go | 57 ++ pkg/plugins/golang/options.go | 119 +-- pkg/plugins/golang/options_test.go | 306 +++---- pkg/plugins/golang/v2/api.go | 111 +-- pkg/plugins/golang/v2/edit.go | 46 +- pkg/plugins/golang/v2/init.go | 108 ++- pkg/plugins/golang/v2/options.go | 198 ----- pkg/plugins/golang/v2/options_test.go | 268 ------ pkg/plugins/golang/v2/plugin.go | 2 +- pkg/plugins/golang/v2/scaffolds/api.go | 21 +- pkg/plugins/golang/v2/scaffolds/edit.go | 6 +- pkg/plugins/golang/v2/scaffolds/init.go | 22 +- .../scaffolds/internal/templates/api/group.go | 12 +- .../scaffolds/internal/templates/api/types.go | 16 +- .../internal/templates/api/webhook.go | 14 +- .../config/certmanager/certificate.go | 6 +- .../config/certmanager/kustomization.go | 6 +- .../config/certmanager/kustomizeconfig.go | 6 +- .../templates/config/crd/kustomization.go | 36 +- .../templates/config/crd/kustomizeconfig.go | 6 +- .../crd/patches/enablecainjection_patch.go | 8 +- .../config/crd/patches/enablewebhook_patch.go | 8 +- .../config/kdefault/enablecainection_patch.go | 8 +- .../config/kdefault/kustomization.go | 10 +- .../kdefault/manager_auth_proxy_patch.go | 8 +- .../config/kdefault/webhook_manager_patch.go | 6 +- .../templates/config/manager/config.go | 6 +- .../templates/config/manager/kustomization.go | 8 +- .../config/prometheus/kustomization.go | 6 +- .../templates/config/prometheus/monitor.go | 6 +- .../config/rbac/auth_proxy_client_role.go | 6 +- .../templates/config/rbac/auth_proxy_role.go | 6 +- .../config/rbac/auth_proxy_role_binding.go | 6 +- .../config/rbac/auth_proxy_service.go | 6 +- .../templates/config/rbac/crd_editor_role.go | 8 +- .../templates/config/rbac/crd_viewer_role.go | 8 +- .../templates/config/rbac/kustomization.go | 8 +- .../config/rbac/leader_election_role.go | 6 +- .../rbac/leader_election_role_binding.go | 6 +- .../templates/config/rbac/role_binding.go | 6 +- .../templates/config/samples/crd_sample.go | 12 +- .../templates/config/webhook/kustomization.go | 8 +- .../config/webhook/kustomizeconfig.go | 8 +- .../templates/config/webhook/service.go | 8 +- .../templates/controllers/controller.go | 16 +- .../controllers/controller_suitetest.go | 36 +- .../internal/templates/dockerfile.go | 6 +- .../scaffolds/internal/templates/gitignore.go | 6 +- .../v2/scaffolds/internal/templates/gomod.go | 10 +- .../internal/templates/hack/boilerplate.go | 8 +- .../v2/scaffolds/internal/templates/main.go | 50 +- .../scaffolds/internal/templates/makefile.go | 8 +- pkg/plugins/golang/v2/scaffolds/webhook.go | 6 +- pkg/plugins/golang/v2/webhook.go | 66 +- pkg/plugins/golang/v3/api.go | 126 +-- pkg/plugins/golang/v3/edit.go | 46 +- pkg/plugins/golang/v3/init.go | 120 ++- pkg/plugins/golang/v3/plugin.go | 2 +- pkg/plugins/golang/v3/scaffolds/api.go | 17 +- pkg/plugins/golang/v3/scaffolds/edit.go | 6 +- pkg/plugins/golang/v3/scaffolds/init.go | 6 +- .../scaffolds/internal/templates/api/group.go | 12 +- .../scaffolds/internal/templates/api/types.go | 16 +- .../internal/templates/api/webhook.go | 16 +- .../templates/api/webhook_suitetest.go | 40 +- .../config/certmanager/certificate.go | 6 +- .../config/certmanager/kustomization.go | 6 +- .../config/certmanager/kustomizeconfig.go | 6 +- .../templates/config/crd/kustomization.go | 36 +- .../templates/config/crd/kustomizeconfig.go | 8 +- .../crd/patches/enablecainjection_patch.go | 8 +- .../config/crd/patches/enablewebhook_patch.go | 8 +- .../config/kdefault/enablecainection_patch.go | 10 +- .../config/kdefault/kustomization.go | 12 +- .../kdefault/manager_auth_proxy_patch.go | 10 +- .../config/kdefault/manager_config_patch.go | 6 +- .../config/kdefault/webhook_manager_patch.go | 10 +- .../templates/config/manager/config.go | 8 +- .../manager/controller_manager_config.go | 12 +- .../templates/config/manager/kustomization.go | 8 +- .../config/prometheus/kustomization.go | 6 +- .../templates/config/prometheus/monitor.go | 6 +- .../config/rbac/auth_proxy_client_role.go | 6 +- .../templates/config/rbac/auth_proxy_role.go | 6 +- .../config/rbac/auth_proxy_role_binding.go | 6 +- .../config/rbac/auth_proxy_service.go | 6 +- .../templates/config/rbac/crd_editor_role.go | 8 +- .../templates/config/rbac/crd_viewer_role.go | 8 +- .../templates/config/rbac/kustomization.go | 8 +- .../config/rbac/leader_election_role.go | 6 +- .../rbac/leader_election_role_binding.go | 6 +- .../templates/config/rbac/role_binding.go | 6 +- .../templates/config/rbac/service_account.go | 6 +- .../templates/config/samples/crd_sample.go | 12 +- .../templates/config/webhook/kustomization.go | 12 +- .../config/webhook/kustomizeconfig.go | 8 +- .../templates/config/webhook/service.go | 8 +- .../templates/controllers/controller.go | 16 +- .../controllers/controller_suitetest.go | 36 +- .../internal/templates/dockerfile.go | 6 +- .../internal/templates/dockerignore.go | 6 +- .../scaffolds/internal/templates/gitignore.go | 6 +- .../v3/scaffolds/internal/templates/gomod.go | 10 +- .../internal/templates/hack/boilerplate.go | 8 +- .../v3/scaffolds/internal/templates/main.go | 52 +- .../scaffolds/internal/templates/makefile.go | 10 +- pkg/plugins/golang/v3/scaffolds/webhook.go | 6 +- pkg/plugins/golang/v3/webhook.go | 59 +- pkg/plugins/internal/cmdutil/cmdutil.go | 68 -- .../v2/suite_test.go => scaffolder.go} | 17 +- plugins/README.md | 46 -- plugins/addon/helpers.go | 88 -- plugins/addon/manifest.go | 50 -- plugins/addon/plugin.go | 28 - test/e2e/v2/plugin_cluster_test.go | 1 + test/e2e/v3/generate_test.go | 4 +- test/testdata/generate.sh | 19 +- testdata/project-v2-addon/PROJECT | 42 +- testdata/project-v2/api/v1/firstmate_types.go | 31 +- .../api/v1/zz_generated.deepcopy.go | 7 +- .../packages/firstmate/0.0.1/manifest.yaml | 1 + testdata/project-v2/channels/stable | 3 + .../crew.testproject.org_firstmates.yaml | 23 +- .../controllers/firstmate_controller.go | 72 +- testdata/project-v2/go.mod | 1 + testdata/project-v3-addon/PROJECT | 16 + testdata/project-v3-config/PROJECT | 7 + .../api/v1/firstmate_types.go | 31 +- .../api/v1/zz_generated.deepcopy.go | 7 +- .../packages/firstmate/0.0.1/manifest.yaml | 1 + testdata/project-v3-config/channels/stable | 3 + .../crew.testproject.org_firstmates.yaml | 25 +- .../project-v3-config/config/rbac/role.yaml | 6 - .../controllers/firstmate_controller.go | 72 +- testdata/project-v3-config/go.mod | 1 + testdata/project-v3/PROJECT | 7 + testdata/project-v3/api/v1/firstmate_types.go | 31 +- .../api/v1/zz_generated.deepcopy.go | 7 +- .../packages/firstmate/0.0.1/manifest.yaml | 1 + testdata/project-v3/channels/stable | 3 + .../crew.testproject.org_firstmates.yaml | 25 +- testdata/project-v3/config/rbac/role.yaml | 6 - .../controllers/firstmate_controller.go | 72 +- testdata/project-v3/go.mod | 1 + 193 files changed, 4432 insertions(+), 3646 deletions(-) create mode 100644 pkg/cli/resource.go create mode 100644 pkg/cli/resource_test.go rename pkg/cli/{cli_suite_test.go => suite_test.go} (100%) rename pkg/{model/file => machinery}/file.go (75%) rename pkg/{model/file => machinery}/funcmap.go (86%) create mode 100644 pkg/machinery/funcmap_test.go create mode 100644 pkg/machinery/injector.go create mode 100644 pkg/machinery/injector_test.go rename pkg/{model/file => machinery}/interfaces.go (99%) rename pkg/{model/file => machinery}/marker.go (87%) create mode 100644 pkg/machinery/marker_test.go rename pkg/{model/file => machinery}/mixins.go (99%) create mode 100644 pkg/machinery/mixins_test.go delete mode 100644 pkg/model/universe.go create mode 100644 pkg/plugin/bundle.go create mode 100644 pkg/plugin/bundle_test.go rename pkg/{model/plugin.go => plugin/errors.go} (58%) rename plugins/addon/channel.go => pkg/plugin/errors_test.go (51%) create mode 100644 pkg/plugin/filter.go create mode 100644 pkg/plugin/filter_test.go create mode 100644 pkg/plugin/helpers_test.go create mode 100644 pkg/plugin/metadata.go rename pkg/plugin/{interfaces.go => plugin.go} (58%) create mode 100644 pkg/plugin/subcommand.go create mode 100644 pkg/plugin/suite_test.go create mode 100644 pkg/plugins/declarative/v1/api.go create mode 100644 pkg/plugins/declarative/v1/internal/templates/channel.go rename {plugins/addon => pkg/plugins/declarative/v1/internal/templates}/controller.go (66%) create mode 100644 pkg/plugins/declarative/v1/internal/templates/manifest.go rename plugins/addon/type.go => pkg/plugins/declarative/v1/internal/templates/types.go (63%) create mode 100644 pkg/plugins/declarative/v1/plugin.go delete mode 100644 pkg/plugins/golang/v2/options.go delete mode 100644 pkg/plugins/golang/v2/options_test.go delete mode 100644 pkg/plugins/internal/cmdutil/cmdutil.go rename pkg/plugins/{golang/v2/suite_test.go => scaffolder.go} (65%) delete mode 100644 plugins/README.md delete mode 100644 plugins/addon/helpers.go delete mode 100644 plugins/addon/manifest.go delete mode 100644 plugins/addon/plugin.go create mode 100644 testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml create mode 100644 testdata/project-v2/channels/stable create mode 100644 testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml create mode 100644 testdata/project-v3-config/channels/stable create mode 100644 testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml create mode 100644 testdata/project-v3/channels/stable diff --git a/cmd/main.go b/cmd/main.go index c3527528372..6cb55af7014 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" ) @@ -30,13 +31,14 @@ func main() { c, err := cli.New( cli.WithCommandName("kubebuilder"), cli.WithVersion(versionString()), - cli.WithDefaultProjectVersion(cfgv3.Version), cli.WithPlugins( &pluginv2.Plugin{}, &pluginv3.Plugin{}, + &declarativev1.Plugin{}, ), cli.WithDefaultPlugins(cfgv2.Version, &pluginv2.Plugin{}), cli.WithDefaultPlugins(cfgv3.Version, &pluginv3.Plugin{}), + cli.WithDefaultProjectVersion(cfgv3.Version), cli.WithCompletion(), ) if err != nil { diff --git a/pkg/cli/api.go b/pkg/cli/api.go index 60cccb81198..6d4f8a17969 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -14,77 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const apiErrorMsg = "failed to create API" + func (c CLI) newCreateAPICmd() *cobra.Command { - ctx := c.newAPIContext() cmd := &cobra.Command{ - Use: "api", - Short: "Scaffold a Kubernetes API", - Long: ctx.Description, - Example: ctx.Examples, + Use: "api", + Short: "Scaffold a Kubernetes API", + Long: `Scaffold a Kubernetes API. +`, RunE: errCmdFunc( fmt.Errorf("api subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateAPI(ctx, cmd) - return cmd -} - -func (c CLI) newAPIContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a Kubernetes API. -`, - } -} - -// nolint:dupl -func (c CLI) bindCreateAPI(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createAPIPlugin plugin.CreateAPI - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateAPI) - if isValid { - if createAPIPlugin != nil { - err := fmt.Errorf("duplicate API creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createAPIPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createAPIPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateAPI. + subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateAPI) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateAPI).GetCreateAPISubcommand() + }, + ) - if createAPIPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide an API creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"API creation"}) + return cmd } - subcommand := createAPIPlugin.GetCreateAPISubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + c.applySubcommandHooks(cmd, subcommands, apiErrorMsg, false) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 403a4941b58..f668f479e97 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -28,7 +28,6 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" - cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -37,29 +36,10 @@ const ( noticeColor = "\033[1;36m%s\033[0m" deprecationFmt = "[Deprecation Notice] %s\n\n" - projectVersionFlag = "project-version" pluginsFlag = "plugins" - - noPluginError = "invalid config file please verify that the version and layout fields are set and valid" + projectVersionFlag = "project-version" ) -// equalStringSlice checks if two string slices are equal. -func equalStringSlice(a, b []string) bool { - // Check lengths - if len(a) != len(b) { - return false - } - - // Check elements - for i, v := range a { - if v != b[i] { - return false - } - } - - return true -} - // CLI is the command line utility that is used to scaffold kubebuilder project files. type CLI struct { //nolint:maligned /* Fields set by Option */ @@ -68,12 +48,12 @@ type CLI struct { //nolint:maligned commandName string // CLI version string. version string - // Default project version in case none is provided and a config file can't be found. - defaultProjectVersion config.Version - // Default plugins in case none is provided and a config file can't be found. - defaultPlugins map[config.Version][]string // Plugins registered in the CLI. plugins map[string]plugin.Plugin + // Default plugins in case none is provided and a config file can't be found. + defaultPlugins map[config.Version][]string + // Default project version in case none is provided and a config file can't be found. + defaultProjectVersion config.Version // Commands injected by options. extraCommands []*cobra.Command // Alpha commands injected by options. @@ -83,10 +63,10 @@ type CLI struct { //nolint:maligned /* Internal fields */ - // Project version to scaffold. - projectVersion config.Version // Plugin keys to scaffold with. pluginKeys []string + // Project version to scaffold. + projectVersion config.Version // A filtered set of plugins that should be used by command constructors. resolvedPlugins []plugin.Plugin @@ -141,9 +121,8 @@ func newCLI(options ...Option) (*CLI, error) { // Default CLI options. c := &CLI{ commandName: "kubebuilder", - defaultProjectVersion: cfgv3.Version, - defaultPlugins: make(map[config.Version][]string), plugins: make(map[string]plugin.Plugin), + defaultPlugins: make(map[config.Version][]string), fs: machinery.Filesystem{FS: afero.NewOsFs()}, } @@ -157,250 +136,239 @@ func newCLI(options ...Option) (*CLI, error) { return c, nil } -// getInfoFromFlags obtains the project version and plugin keys from flags. -func (c *CLI) getInfoFromFlags() (string, []string, error) { - // Partially parse the command line arguments - fs := pflag.NewFlagSet("base", pflag.ContinueOnError) +// buildCmd creates the underlying cobra command and stores it internally. +func (c *CLI) buildCmd() error { + c.cmd = c.newRootCmd() - // Load the base command global flags - fs.AddFlagSet(c.cmd.PersistentFlags()) + // Get project version and plugin keys. + if err := c.getInfo(); err != nil { + return err + } - // Omit unknown flags to avoid parsing errors - fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + // Resolve plugins for project version and plugin keys. + if err := c.resolvePlugins(); err != nil { + return err + } - // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default - // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command. - fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName)) + // Add the subcommands + c.addSubcommands() - // Parse the arguments - if err := fs.Parse(os.Args[1:]); err != nil { - return "", []string{}, err - } + return nil +} - // Define the flags needed for plugin resolution - var ( - projectVersion string - plugins []string - err error - ) - // GetXxxxx methods will not yield errors because we know for certain these flags exist and types match. - projectVersion, err = fs.GetString(projectVersionFlag) - if err != nil { - return "", []string{}, err - } - plugins, err = fs.GetStringSlice(pluginsFlag) - if err != nil { - return "", []string{}, err +// getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags. +func (c *CLI) getInfo() error { + // Get plugin keys and project version from project configuration file + // We discard the error if file doesn't exist because not being able to read a project configuration + // file is not fatal for some commands. The ones that require it need to check its existence later. + hasConfigFile := true + if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) { + hasConfigFile = false + } else if err != nil { + return err } - // Remove leading and trailing spaces - for i, key := range plugins { - plugins[i] = strings.TrimSpace(key) + // We can't early return here in case a project configuration file was found because + // this command call may override the project plugins. + + // Get project version and plugin info from flags + if err := c.getInfoFromFlags(hasConfigFile); err != nil { + return err } - return projectVersion, plugins, nil + // Get project version and plugin info from defaults + c.getInfoFromDefaults() + + return nil } // getInfoFromConfigFile obtains the project version and plugin keys from the project config file. -func (c CLI) getInfoFromConfigFile() (config.Version, []string, error) { +func (c *CLI) getInfoFromConfigFile() error { // Read the project configuration file cfg := yamlstore.New(c.fs) - err := cfg.Load() - switch { - case err == nil: - case errors.Is(err, os.ErrNotExist): - return config.Version{}, nil, nil - default: - return config.Version{}, nil, err + if err := cfg.Load(); err != nil { + return err } - return getInfoFromConfig(cfg.Config()) + return c.getInfoFromConfig(cfg.Config()) } // getInfoFromConfig obtains the project version and plugin keys from the project config. // It is extracted from getInfoFromConfigFile for testing purposes. -func getInfoFromConfig(projectConfig config.Config) (config.Version, []string, error) { - return projectConfig.GetVersion(), projectConfig.GetPluginChain(), nil -} +func (c *CLI) getInfoFromConfig(projectConfig config.Config) error { + c.pluginKeys = projectConfig.GetPluginChain() + c.projectVersion = projectConfig.GetVersion() -// resolveFlagsAndConfigFileConflicts checks if the provided combined input from flags and -// the config file is valid and uses default values in case some info was not provided. -func (c CLI) resolveFlagsAndConfigFileConflicts( - flagProjectVersionString string, - cfgProjectVersion config.Version, - flagPlugins, cfgPlugins []string, -) (config.Version, []string, error) { - // Parse project configuration version from flags - var flagProjectVersion config.Version - if flagProjectVersionString != "" { - if err := flagProjectVersion.Parse(flagProjectVersionString); err != nil { - return config.Version{}, nil, fmt.Errorf("unable to parse project version flag: %w", err) + for _, pluginKey := range c.pluginKeys { + if err := plugin.ValidateKey(pluginKey); err != nil { + return fmt.Errorf("invalid plugin key found in project configuration file: %w", err) } } - // Resolve project version - var projectVersion config.Version - isFlagProjectVersionInvalid := flagProjectVersion.Validate() != nil - isCfgProjectVersionInvalid := cfgProjectVersion.Validate() != nil - switch { - // If they are both invalid (empty is invalid), use the default - case isFlagProjectVersionInvalid && isCfgProjectVersionInvalid: - projectVersion = c.defaultProjectVersion - // If any is invalid (empty is invalid), choose the other - case isCfgProjectVersionInvalid: - projectVersion = flagProjectVersion - case isFlagProjectVersionInvalid: - projectVersion = cfgProjectVersion - // If they are equal doesn't matter which we choose - case flagProjectVersion.Compare(cfgProjectVersion) == 0: - projectVersion = flagProjectVersion - // If both are valid (empty is invalid) and they are different error out - default: - return config.Version{}, nil, fmt.Errorf("project version conflict between command line args (%s) "+ - "and project configuration file (%s)", flagProjectVersionString, cfgProjectVersion) + return nil +} + +// getInfoFromFlags obtains the project version and plugin keys from flags. +func (c *CLI) getInfoFromFlags(hasConfigFile bool) error { + // Partially parse the command line arguments + fs := pflag.NewFlagSet("base", pflag.ContinueOnError) + + // Load the base command global flags + fs.AddFlagSet(c.cmd.PersistentFlags()) + + // If we were unable to load the project configuration, we should also accept the project version flag + var projectVersionStr string + if !hasConfigFile { + fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version") } - // Resolve plugins - var plugins []string - isFlagPluginsEmpty := len(flagPlugins) == 0 - isCfgPluginsEmpty := len(cfgPlugins) == 0 - switch { - // If they are both empty, use the default - case isFlagPluginsEmpty && isCfgPluginsEmpty: - if defaults, hasDefaults := c.defaultPlugins[projectVersion]; hasDefaults { - plugins = defaults + // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default + // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command. + fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName)) + + // Omit unknown flags to avoid parsing errors + fs.ParseErrorsWhitelist = pflag.ParseErrorsWhitelist{UnknownFlags: true} + + // Parse the arguments + if err := fs.Parse(os.Args[1:]); err != nil { + return err + } + + // If any plugin key was provided, replace those from the project configuration file + if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil { + return err + } else if len(pluginKeys) != 0 { + // Remove leading and trailing spaces and validate the plugin keys + for i, key := range pluginKeys { + pluginKeys[i] = strings.TrimSpace(key) + if err := plugin.ValidateKey(pluginKeys[i]); err != nil { + return fmt.Errorf("invalid plugin %q found in flags: %w", pluginKeys[i], err) + } } - // If any is empty, choose the other - case isCfgPluginsEmpty: - plugins = flagPlugins - case isFlagPluginsEmpty: - plugins = cfgPlugins - // If they are equal doesn't matter which we choose - case equalStringSlice(flagPlugins, cfgPlugins): - plugins = flagPlugins - // If none is empty and they are different error out - default: - return config.Version{}, nil, fmt.Errorf("plugins conflict between command line args (%v) "+ - "and project configuration file (%v)", flagPlugins, cfgPlugins) + + c.pluginKeys = pluginKeys } - // Validate the plugins - for _, p := range plugins { - if err := plugin.ValidateKey(p); err != nil { - return config.Version{}, nil, err + + // If the project version flag was accepted but not provided keep the empty version and try to resolve it later, + // else validate the provided project version + if projectVersionStr != "" { + if err := c.projectVersion.Parse(projectVersionStr); err != nil { + return fmt.Errorf("invalid project version flag: %w", err) } } - return projectVersion, plugins, nil + return nil } -// getInfo obtains the project version and plugin keys resolving conflicts among flags and the project config file. -func (c *CLI) getInfo() error { - // Get project version and plugin info from flags - flagProjectVersion, flagPlugins, err := c.getInfoFromFlags() - if err != nil { - return err +// getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values +func (c *CLI) getInfoFromDefaults() { + // Should not use default values if a plugin was already set + // This checks includes the case where a project configuration file was found, + // as it will always have at least one plugin key set by now + if len(c.pluginKeys) != 0 { + // We don't assign a default value for project version here because we may be able to + // resolve the project version after resolving the plugins. + return + } + + // If the user provided a project version, use the default plugins for that project version + if c.projectVersion.Validate() == nil { + c.pluginKeys = c.defaultPlugins[c.projectVersion] + return + } + + // Else try to use the default plugins for the default project version + if c.defaultProjectVersion.Validate() == nil { + var found bool + if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found { + c.projectVersion = c.defaultProjectVersion + return + } + } + + // Else check if only default plugins for a project version were provided + if len(c.defaultPlugins) == 1 { + for projectVersion, defaultPlugins := range c.defaultPlugins { + c.pluginKeys = defaultPlugins + c.projectVersion = projectVersion + return + } } - // Get project version and plugin info from project configuration file - cfgProjectVersion, cfgPlugins, _ := c.getInfoFromConfigFile() - // We discard the error because not being able to read a project configuration file - // is not fatal for some commands. The ones that require it need to check its existence. - - // Resolve project version and plugin keys - c.projectVersion, c.pluginKeys, err = c.resolveFlagsAndConfigFileConflicts( - flagProjectVersion, cfgProjectVersion, flagPlugins, cfgPlugins, - ) - return err } const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " + "https://kubebuilder.io/migration/plugin/plugins.html)" -// resolve selects from the available plugins those that match the project version and plugin keys provided. -func (c *CLI) resolve() error { - var plugins []plugin.Plugin +// resolvePlugins selects from the available plugins those that match the project version and plugin keys provided. +func (c *CLI) resolvePlugins() error { + knownProjectVersion := c.projectVersion.Validate() == nil + for _, pluginKey := range c.pluginKeys { - name, version := plugin.SplitKey(pluginKey) - shortName := plugin.GetShortName(name) + var extraErrMsg string + + plugins := make([]plugin.Plugin, 0, len(c.plugins)) + for _, p := range c.plugins { + plugins = append(plugins, p) + } + // We can omit the error because plugin keys have already been validated + plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey) + if knownProjectVersion { + plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion) + extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion) + } // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable". // This upgrade effectively removes a plugin, which is fine because unstable plugins are // under no support contract. However users should be notified _why_ their plugin cannot be found. - var extraErrMsg string - if version != "" { + if _, version := plugin.SplitKey(pluginKey); version != "" { var ver plugin.Version if err := ver.Parse(version); err != nil { return fmt.Errorf("error parsing input plugin version from key %q: %v", pluginKey, err) } if !ver.IsStable() { - extraErrMsg = unstablePluginMsg + extraErrMsg += unstablePluginMsg } } - var resolvedPlugins []plugin.Plugin - isFullName := shortName != name - hasVersion := version != "" - - switch { - // If it is fully qualified search it - case isFullName && hasVersion: - p, isKnown := c.plugins[pluginKey] - if !isKnown { - return fmt.Errorf("unknown fully qualified plugin %q%s", pluginKey, extraErrMsg) - } - if !plugin.SupportsVersion(p, c.projectVersion) { - return fmt.Errorf("plugin %q does not support project version %q", pluginKey, c.projectVersion) - } - plugins = append(plugins, p) - continue - // Shortname with version - case hasVersion: - for _, p := range c.plugins { - // Check that the shortname and version match - if plugin.GetShortName(p.Name()) == name && p.Version().String() == version { - resolvedPlugins = append(resolvedPlugins, p) - } - } - // Full name without version - case isFullName: - for _, p := range c.plugins { - // Check that the name matches - if p.Name() == name { - resolvedPlugins = append(resolvedPlugins, p) - } - } - // Shortname without version + // Only 1 plugin can match + switch len(plugins) { + case 1: + c.resolvedPlugins = append(c.resolvedPlugins, plugins[0]) + case 0: + return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg) default: - for _, p := range c.plugins { - // Check that the shortname matches - if plugin.GetShortName(p.Name()) == name { - resolvedPlugins = append(resolvedPlugins, p) - } - } + return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg) } + } - // Filter the ones that do not support the required project version - i := 0 - for _, resolvedPlugin := range resolvedPlugins { - if plugin.SupportsVersion(resolvedPlugin, c.projectVersion) { - resolvedPlugins[i] = resolvedPlugin - i++ - } - } - resolvedPlugins = resolvedPlugins[:i] + // Now we can try to resolve the project version if not known by this point + if !knownProjectVersion && len(c.resolvedPlugins) > 0 { + // Extract the common supported project versions + supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...) - // Only 1 plugin can match - switch len(resolvedPlugins) { - case 0: - return fmt.Errorf("no plugin could be resolved with key %q for project version %q%s", - pluginKey, c.projectVersion, extraErrMsg) + // If there is only one common supported project version, resolve to it + ProjectNumberVersionSwitch: + switch len(supportedProjectVersions) { case 1: - plugins = append(plugins, resolvedPlugins[0]) + c.projectVersion = supportedProjectVersions[0] + case 0: + return fmt.Errorf("no project version supported by all the resolved plugins") default: - return fmt.Errorf("ambiguous plugin %q for project version %q", pluginKey, c.projectVersion) + supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions)) + for _, supportedProjectVersion := range supportedProjectVersions { + // In case one of the multiple supported versions is the default one, choose that and exit the switch + if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 { + c.projectVersion = c.defaultProjectVersion + break ProjectNumberVersionSwitch + } + supportedProjectVersionStrings = append(supportedProjectVersionStrings, + fmt.Sprintf("%q", supportedProjectVersion)) + } + return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s", + strings.Join(supportedProjectVersionStrings, ", ")) } } - c.resolvedPlugins = plugins return nil } @@ -438,26 +406,6 @@ func (c *CLI) addSubcommands() { } } -// buildCmd creates the underlying cobra command and stores it internally. -func (c *CLI) buildCmd() error { - c.cmd = c.newRootCmd() - - // Get project version and plugin keys. - if err := c.getInfo(); err != nil { - return err - } - - // Resolve plugins for project version and plugin keys. - if err := c.resolve(); err != nil { - return err - } - - // Add the subcommands - c.addSubcommands() - - return nil -} - // addExtraCommands adds the additional commands. func (c *CLI) addExtraCommands() error { for _, cmd := range c.extraCommands { @@ -480,6 +428,13 @@ func (c CLI) printDeprecationWarnings() { } } +// metadata returns CLI's metadata. +func (c CLI) metadata() plugin.CLIMetadata { + return plugin.CLIMetadata{ + CommandName: c.commandName, + } +} + // Run executes the CLI utility. // // If an error is found, command help and examples will be printed. diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index a7084980119..a08c2a1dd94 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -20,8 +20,10 @@ import ( "fmt" "io/ioutil" "os" + "strings" . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "github.com/spf13/afero" "github.com/spf13/cobra" @@ -31,6 +33,7 @@ import ( cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" ) func makeMockPluginsFor(projectVersion config.Version, pluginKeys ...string) []plugin.Plugin { @@ -77,533 +80,231 @@ func hasSubCommand(cmd *cobra.Command, name string) bool { } var _ = Describe("CLI", func() { + var ( + c *CLI + projectVersion = config.Version{Number: 3} + ) + + BeforeEach(func() { + c = &CLI{ + fs: machinery.Filesystem{FS: afero.NewMemMapFs()}, + } + }) - Context("getInfoFromFlags", func() { - var ( - projectVersion string - plugins []string - err error - c *CLI - ) + // TODO: test CLI.getInfoFromConfigFile using a mock filesystem - // Save os.Args and restore it for every test - var args []string - BeforeEach(func() { - c = &CLI{} - c.cmd = c.newRootCmd() - args = os.Args - }) - AfterEach(func() { os.Args = args }) - - When("no flag is set", func() { + Context("getInfoFromConfig", func() { + When("not having layout field", func() { It("should succeed", func() { - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(len(plugins)).To(Equal(0)) - }) - }) + pluginChain := []string{"go.kubebuilder.io/v2"} - When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { - It("should succeed", func() { - setProjectVersionFlag("2") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(len(plugins)).To(Equal(0)) + projectConfig := cfgv2.New() + + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginChain)) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { - It("should succeed using one plugin key", func() { - setPluginsFlag("go/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1"})) - }) + When("having a single plugin in the layout field", func() { + It("should succeed", func() { + pluginChain := []string{"go.kubebuilder.io/v2"} - It("should succeed using more than one plugin key", func() { - setPluginsFlag("go/v1,example/v2,test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) - }) + projectConfig := cfgv3.New() + Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) - It("should succeed using more than one plugin key with spaces", func() { - setPluginsFlag("go/v1 , example/v2 , test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginChain)) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When(fmt.Sprintf("--%s and --%s flags are set", projectVersionFlag, pluginsFlag), func() { - It("should succeed using one plugin key", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1"})) - }) + When("having multiple plugins in the layout field", func() { + It("should succeed", func() { + pluginChain := []string{"go.kubebuilder.io/v2", "declarative.kubebuilder.io/v1"} - It("should succeed using more than one plugin keys", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1,example/v2,test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) - }) + projectConfig := cfgv3.New() + Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) - It("should succeed using more than one plugin keys with spaces", func() { - setProjectVersionFlag("2") - setPluginsFlag("go/v1 , example/v2 , test/v1") - projectVersion, plugins, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion).To(Equal("2")) - Expect(plugins).To(Equal([]string{"go/v1", "example/v2", "test/v1"})) + Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginChain)) + Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) - When("additional flags are set", func() { - It("should succeed", func() { - setFlag("extra-flag", "extra-value") - _, _, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) - }) + When("having invalid plugin keys in the layout field", func() { + It("should fail", func() { + pluginChain := []string{"_/v1"} - // `--help` is not captured by the whitelist, so we need to special case it - It("should not fail for `--help`", func() { - setBoolFlag("help") - _, _, err = c.getInfoFromFlags() - Expect(err).NotTo(HaveOccurred()) + projectConfig := cfgv3.New() + Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) + + Expect(c.getInfoFromConfig(projectConfig)).NotTo(Succeed()) }) }) }) - Context("getInfoFromConfig", func() { - var ( - projectConfig config.Config - projectVersion config.Version - plugins []string - err error - - pluginChain = []string{"go.kubebuilder.io/v2"} - ) + Context("getInfoFromFlags", func() { + // Save os.Args and restore it for every test + var args []string + BeforeEach(func() { + c.cmd = c.newRootCmd() - When("not having layout field", func() { - It("should succeed", func() { - projectConfig = cfgv2.New() - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(plugins).To(Equal(pluginChain)) - }) + args = os.Args + }) + AfterEach(func() { + os.Args = args }) - When("having layout field", func() { + When("no flag is set", func() { It("should succeed", func() { - projectConfig = cfgv3.New() - Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) - projectVersion, plugins, err = getInfoFromConfig(projectConfig) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) - Expect(plugins).To(Equal(pluginChain)) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(BeEmpty()) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) }) - }) - Context("CLI.resolveFlagsAndConfigFileConflicts", func() { - const ( - pluginKey1 = "go.kubebuilder.io/v1" - pluginKey2 = "go.kubebuilder.io/v2" - pluginKey3 = "go.kubebuilder.io/v3" - ) - var ( - c *CLI + When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { + It("should succeed using one plugin key", func() { + pluginKeys := []string{"go/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) - projectVersion config.Version - plugins []string - err error + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) + }) - projectVersion1 = config.Version{Number: 1} - projectVersion2 = config.Version{Number: 2} - projectVersion3 = config.Version{Number: 3} - ) + It("should succeed using more than one plugin key", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) - When("having no project version set", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(config.Version{})).To(Equal(0)) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - }) - When("having one project version source", func() { - When("having default project version set", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) - }) + It("should succeed using more than one plugin key with spaces", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ", ")) - When("having project version set from flags", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - When("having project version set from config file", func() { - It("should succeed", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - projectVersion1, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) + It("should fail for an invalid plugin key", func() { + setPluginsFlag("_/v1") + + Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) - When("having two project version source", func() { - When("having default project version set and from flags", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) - }) - }) + When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { + It("should succeed", func() { + setProjectVersionFlag(projectVersion.String()) - When("having default project version set and from config file", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - projectVersion2, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(BeEmpty()) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - When("having project version set from flags and config file", func() { - It("should succeed if they are the same", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - projectVersion1, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion1)).To(Equal(0)) - }) - - It("should fail if they are different", func() { - c = &CLI{} - _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion1.String(), - projectVersion2, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) - }) + It("should fail for an invalid project version", func() { + setProjectVersionFlag("v_1") + + Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) - When("having three project version sources", func() { - It("should succeed if project version from flags and config file are the same", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - projectVersion2, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(projectVersion.Compare(projectVersion2)).To(Equal(0)) + When(fmt.Sprintf("--%s and --%s flags are set", pluginsFlag, projectVersionFlag), func() { + It("should succeed using one plugin key", func() { + pluginKeys := []string{"go/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - It("should fail if project version from flags and config file are different", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - } - _, _, err = c.resolveFlagsAndConfigFileConflicts( - projectVersion2.String(), - projectVersion3, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) + It("should succeed using more than one plugin key", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ",")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - }) - When("an invalid project version is set", func() { - It("should fail", func() { - c = &CLI{} - projectVersion, _, err = c.resolveFlagsAndConfigFileConflicts( - "0", - config.Version{}, - nil, - nil, - ) - Expect(err).To(HaveOccurred()) + It("should succeed using more than one plugin key with spaces", func() { + pluginKeys := []string{"go/v1", "example/v2", "test/v1"} + setPluginsFlag(strings.Join(pluginKeys, ", ")) + setProjectVersionFlag(projectVersion.String()) + + Expect(c.getInfoFromFlags(false)).To(Succeed()) + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) - When("having no plugin keys set", func() { + When("additional flags are set", func() { It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(0)) - }) - }) + setFlag("extra-flag", "extra-value") - When("having one plugin keys source", func() { - When("having default plugin keys set", func() { - It("should succeed", func() { - c = &CLI{ - defaultProjectVersion: projectVersion1, - defaultPlugins: map[config.Version][]string{ - projectVersion1: {pluginKey1}, - projectVersion2: {pluginKey2}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) }) - When("having plugin keys set from flags", func() { - It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) - }) + // `--help` is not captured by the allowlist, so we need to special case it + It("should not fail for `--help`", func() { + setBoolFlag("help") - When("having plugin keys set from config file", func() { - It("should succeed", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - []string{pluginKey1}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) + Expect(c.getInfoFromFlags(false)).To(Succeed()) }) }) + }) - When("having two plugin keys source", func() { - When("having default plugin keys set and from flags", func() { - It("should succeed", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - nil, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) - }) + Context("getInfoFromDefaults", func() { + var ( + pluginKeys = []string{"go.kubebuilder.io/v2"} + ) - When("having default plugin keys set and from config file", func() { - It("should succeed", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - nil, - []string{pluginKey2}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) - }) + It("should be a no-op if already have plugin keys", func() { + c.pluginKeys = pluginKeys - When("having plugin keys set from flags and config file", func() { - It("should succeed if they are the same", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - []string{pluginKey1}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey1)) - }) - - It("should fail if they are different", func() { - c = &CLI{} - _, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey1}, - []string{pluginKey2}, - ) - Expect(err).To(HaveOccurred()) - }) - }) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) - When("having three plugin keys sources", func() { - It("should succeed if plugin keys from flags and config file are the same", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - []string{pluginKey2}, - ) - Expect(err).NotTo(HaveOccurred()) - Expect(len(plugins)).To(Equal(1)) - Expect(plugins[0]).To(Equal(pluginKey2)) - }) + It("should succeed if default plugins for project version are set", func() { + c.projectVersion = projectVersion + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} - It("should fail if plugin keys from flags and config file are different", func() { - c = &CLI{ - defaultPlugins: map[config.Version][]string{ - {}: {pluginKey1}, - }, - } - _, _, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{pluginKey2}, - []string{pluginKey3}, - ) - Expect(err).To(HaveOccurred()) - }) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) - When("an invalid plugin key is set", func() { - It("should fail", func() { - c = &CLI{} - _, plugins, err = c.resolveFlagsAndConfigFileConflicts( - "", - config.Version{}, - []string{"A"}, - nil, - ) - Expect(err).To(HaveOccurred()) - }) - }) - }) + It("should succeed if default plugins for default project version are set", func() { + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} + c.defaultProjectVersion = projectVersion - // NOTE: only flag info can be tested with CLI.getInfo as the config file doesn't exist, - // previous tests ensure that the info from config files is read properly and that - // conflicts are solved appropriately. - Context("CLI.getInfo", func() { - It("should set project version and plugin keys", func() { - projectVersion := config.Version{Number: 2} - pluginKeys := []string{"go.kubebuilder.io/v2"} - c := &CLI{ - defaultProjectVersion: projectVersion, - defaultPlugins: map[config.Version][]string{ - projectVersion: pluginKeys, - }, - fs: machinery.Filesystem{FS: afero.NewMemMapFs()}, - } - c.cmd = c.newRootCmd() - Expect(c.getInfo()).To(Succeed()) + c.getInfoFromDefaults() + Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) + }) + + It("should succeed if default plugins for only a single project version are set", func() { + c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} + + c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) - Context("CLI.resolve", func() { + Context("resolvePlugins", func() { var ( - c *CLI - - projectVersion = config.Version{Number: 2} - pluginKeys = []string{ "foo.example.com/v1", "bar.example.com/v1", @@ -616,48 +317,83 @@ var _ = Describe("CLI", func() { ) plugins := makeMockPluginsFor(projectVersion, pluginKeys...) - plugins = append(plugins, newMockPlugin("invalid.kubebuilder.io", "v1")) + plugins = append(plugins, + newMockPlugin("invalid.kubebuilder.io", "v1"), + newMockPlugin("only1.kubebuilder.io", "v1", + config.Version{Number: 1}), + newMockPlugin("only2.kubebuilder.io", "v1", + config.Version{Number: 2}), + newMockPlugin("1and2.kubebuilder.io", "v1", + config.Version{Number: 1}, config.Version{Number: 2}), + newMockPlugin("2and3.kubebuilder.io", "v1", + config.Version{Number: 2}, config.Version{Number: 3}), + newMockPlugin("1-2and3.kubebuilder.io", "v1", + config.Version{Number: 1}, config.Version{Number: 2}, config.Version{Number: 3}), + ) pluginMap := makeMapFor(plugins...) - for key, qualified := range map[string]string{ - "foo.example.com/v1": "foo.example.com/v1", - "foo.example.com": "foo.example.com/v1", - "baz": "baz.example.com/v1", - "foo/v2": "foo.kubebuilder.io/v2", - } { - key, qualified := key, qualified - It(fmt.Sprintf("should resolve %q", key), func() { - c = &CLI{ - plugins: pluginMap, - projectVersion: projectVersion, - pluginKeys: []string{key}, - } - Expect(c.resolve()).To(Succeed()) + BeforeEach(func() { + c.plugins = pluginMap + }) + + DescribeTable("should resolve", + func(key, qualified string) { + c.pluginKeys = []string{key} + c.projectVersion = projectVersion + + Expect(c.resolvePlugins()).To(Succeed()) Expect(len(c.resolvedPlugins)).To(Equal(1)) Expect(plugin.KeyFor(c.resolvedPlugins[0])).To(Equal(qualified)) - }) - } + }, + Entry("fully qualified plugin", "foo.example.com/v1", "foo.example.com/v1"), + Entry("plugin without version", "foo.example.com", "foo.example.com/v1"), + Entry("shortname without version", "baz", "baz.example.com/v1"), + Entry("shortname with version", "foo/v2", "foo.kubebuilder.io/v2"), + ) - for _, key := range []string{ - "foo.kubebuilder.io", - "foo/v1", - "foo", - "blah", - "foo.example.com/v2", - "foo/v3", - "foo.example.com/v3", - "invalid.kubebuilder.io/v1", - } { - key := key - It(fmt.Sprintf("should not resolve %q", key), func() { - c = &CLI{ - plugins: pluginMap, - projectVersion: projectVersion, - pluginKeys: []string{key}, - } - Expect(c.resolve()).NotTo(Succeed()) - }) - } + DescribeTable("should not resolve", + func(key string) { + c.pluginKeys = []string{key} + c.projectVersion = projectVersion + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }, + Entry("for an ambiguous version", "foo.kubebuilder.io"), + Entry("for an ambiguous name", "foo/v1"), + Entry("for an ambiguous name and version", "foo"), + Entry("for a non-existent name", "blah"), + Entry("for a non-existent version", "foo.example.com/v2"), + Entry("for a non-existent version", "foo/v3"), + Entry("for a non-existent version", "foo.example.com/v3"), + Entry("for a plugin that doesn't support the project version", "invalid.kubebuilder.io/v1"), + ) + + It("should succeed if only one common project version is found", func() { + c.pluginKeys = []string{"1and2", "2and3"} + + Expect(c.resolvePlugins()).To(Succeed()) + Expect(c.projectVersion.Compare(config.Version{Number: 2})).To(Equal(0)) + }) + + It("should fail if no common project version is found", func() { + c.pluginKeys = []string{"only1", "only2"} + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }) + + It("should fail if more than one common project versions are found", func() { + c.pluginKeys = []string{"1and2", "1-2and3"} + + Expect(c.resolvePlugins()).NotTo(Succeed()) + }) + + It("should succeed if more than one common project versions are found and one is the default", func() { + c.pluginKeys = []string{"2and3", "1-2and3"} + c.defaultProjectVersion = projectVersion + + Expect(c.resolvePlugins()).To(Succeed()) + Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) + }) }) Context("New", func() { @@ -677,7 +413,11 @@ var _ = Describe("CLI", func() { When("providing a version string", func() { It("should create a valid CLI", func() { const version = "version string" - c, err = New(WithVersion(version)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithVersion(version), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, "version")).To(BeTrue()) }) @@ -685,7 +425,11 @@ var _ = Describe("CLI", func() { When("enabling completion", func() { It("should create a valid CLI", func() { - c, err = New(WithCompletion()) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithCompletion(), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, "completion")).To(BeTrue()) }) @@ -707,8 +451,19 @@ var _ = Describe("CLI", func() { It("should return a CLI that returns an error", func() { setPluginsFlag("foo") + c, err = New() Expect(err).NotTo(HaveOccurred()) + + // Overwrite stderr to read the output and reset it afterwards + _, w, _ := os.Pipe() + temp := os.Stderr + defer func() { + os.Stderr = temp + _ = w.Close() + }() + os.Stderr = w + Expect(c.Run()).NotTo(Succeed()) }) }) @@ -716,14 +471,22 @@ var _ = Describe("CLI", func() { When("providing extra commands", func() { It("should create a valid CLI for non-conflicting ones", func() { extraCommand := &cobra.Command{Use: "extra"} - c, err = New(WithExtraCommands(extraCommand)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraCommands(extraCommand), + ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, extraCommand.Use)).To(BeTrue()) }) It("should return an error for conflicting ones", func() { extraCommand := &cobra.Command{Use: "init"} - _, err = New(WithExtraCommands(extraCommand)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraCommands(extraCommand), + ) Expect(err).To(HaveOccurred()) }) }) @@ -731,7 +494,11 @@ var _ = Describe("CLI", func() { When("providing extra alpha commands", func() { It("should create a valid CLI for non-conflicting ones", func() { extraAlphaCommand := &cobra.Command{Use: "extra"} - c, err = New(WithExtraAlphaCommands(extraAlphaCommand)) + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraAlphaCommands(extraAlphaCommand), + ) Expect(err).NotTo(HaveOccurred()) var alpha *cobra.Command for _, subcmd := range c.cmd.Commands() { @@ -746,7 +513,11 @@ var _ = Describe("CLI", func() { It("should return an error for conflicting ones", func() { extraAlphaCommand := &cobra.Command{Use: "extra"} - _, err = New(WithExtraAlphaCommands(extraAlphaCommand, extraAlphaCommand)) + _, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraAlphaCommands(extraAlphaCommand, extraAlphaCommand), + ) Expect(err).To(HaveOccurred()) }) }) @@ -757,7 +528,6 @@ var _ = Describe("CLI", func() { deprecationWarning = "DEPRECATED" ) var ( - projectVersion = config.Version{Number: 2} deprecatedPlugin = newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) ) @@ -770,11 +540,13 @@ var _ = Describe("CLI", func() { os.Stdout = w c, err = New( - WithDefaultProjectVersion(projectVersion), - WithDefaultPlugins(projectVersion, deprecatedPlugin), WithPlugins(deprecatedPlugin), + WithDefaultPlugins(projectVersion, deprecatedPlugin), + WithDefaultProjectVersion(projectVersion), ) + _ = w.Close() + Expect(err).NotTo(HaveOccurred()) printed, _ := ioutil.ReadAll(r) Expect(string(printed)).To(Equal( diff --git a/pkg/cli/cmd_helpers.go b/pkg/cli/cmd_helpers.go index 4f749a976e4..d802e856124 100644 --- a/pkg/cli/cmd_helpers.go +++ b/pkg/cli/cmd_helpers.go @@ -17,16 +17,38 @@ limitations under the License. package cli import ( + "errors" "fmt" "os" "github.com/spf13/cobra" + "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/config/store" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +// noResolvedPluginError is returned by subcommands that require a plugin when none was resolved. +type noResolvedPluginError struct{} + +// Error implements error interface. +func (e noResolvedPluginError) Error() string { + return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file" +} + +// noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found. +type noAvailablePluginError struct { + subcommand string +} + +// Error implements error interface. +func (e noAvailablePluginError) Error() string { + return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand) +} + // cmdErr updates a cobra command to output error information when executed // or used with the help flag. func cmdErr(cmd *cobra.Command, err error) { @@ -34,12 +56,6 @@ func cmdErr(cmd *cobra.Command, err error) { cmd.RunE = errCmdFunc(err) } -// cmdErrNoHelp calls cmdErr(cmd, err) then turns cmd's usage off. -func cmdErrNoHelp(cmd *cobra.Command, err error) { - cmdErr(cmd, err) - cmd.SilenceUsage = true -} - // errCmdFunc returns a cobra RunE function that returns the provided error func errCmdFunc(err error) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { @@ -47,39 +63,282 @@ func errCmdFunc(err error) func(*cobra.Command, []string) error { } } -// preRunECmdFunc returns a cobra PreRunE function that loads the configuration file -// and injects it into the subcommand -func preRunECmdFunc(subcmd plugin.Subcommand, cfg store.Store, msg string) func(*cobra.Command, []string) error { +// keySubcommandTuple represents a pairing of the key of a plugin with a plugin.Subcommand. +type keySubcommandTuple struct { + key string + subcommand plugin.Subcommand + + // skip will be used to flag subcommands that should be skipped after any hook returned a plugin.ExitError. + skip bool + // i will be used to specify the position in an slice so that the actual value can be modified. + i int +} + +// filterSubcommands returns a list of plugin keys and subcommands from a filtered list of resolved plugins. +func (c *CLI) filterSubcommands( + filter func(plugin.Plugin) bool, + extract func(plugin.Plugin) plugin.Subcommand, +) []keySubcommandTuple { + // Unbundle plugins + plugins := make([]plugin.Plugin, 0, len(c.resolvedPlugins)) + for _, p := range c.resolvedPlugins { + if bundle, isBundle := p.(plugin.Bundle); isBundle { + plugins = append(plugins, bundle.Plugins()...) + } else { + plugins = append(plugins, p) + } + } + + tuples := make([]keySubcommandTuple, 0, len(plugins)) + for _, p := range plugins { + if filter(p) { + tuples = append(tuples, keySubcommandTuple{ + key: plugin.KeyFor(p), + subcommand: extract(p), + }) + } + } + return tuples +} + +// applySubcommandHooks runs the initialization hooks and configures the commands pre-run, +// run, and post-run hooks with the appropriate execution hooks. +func (c *CLI) applySubcommandHooks( + cmd *cobra.Command, + subcommands []keySubcommandTuple, + errorMessage string, + createConfig bool, +) { + // In case we create a new project configuration we need to compute the plugin chain. + pluginChain := make([]string, 0, len(c.resolvedPlugins)) + if createConfig { + // We extract the plugin keys again instead of using the ones obtained when filtering subcommands + // as these plugins are unbundled but we want to keep bundle names in the plugin chain. + for _, p := range c.resolvedPlugins { + pluginChain = append(pluginChain, plugin.KeyFor(p)) + } + } + + options := initializationHooks(cmd, subcommands, c.metadata()) + + factory := executionHooksFactory{ + fs: c.fs, + store: yamlstore.New(c.fs), + subcommands: subcommands, + errorMessage: errorMessage, + projectVersion: c.projectVersion, + pluginChain: pluginChain, + } + cmd.PreRunE = factory.preRunEFunc(options, createConfig) + cmd.RunE = factory.runEFunc() + cmd.PostRunE = factory.postRunEFunc() +} + +// initializationHooks executes update metadata and bind flags plugin hooks. +func initializationHooks( + cmd *cobra.Command, + subcommands []keySubcommandTuple, + meta plugin.CLIMetadata, +) *resourceOptions { + // Update metadata hook. + subcmdMeta := plugin.SubcommandMetadata{ + Description: cmd.Long, + Examples: cmd.Example, + } + for _, tuple := range subcommands { + if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata { + subcommand.UpdateMetadata(meta, &subcmdMeta) + } + } + cmd.Long = subcmdMeta.Description + cmd.Example = subcmdMeta.Examples + + // Before binding specific plugin flags, bind common ones. + requiresResource := false + for _, tuple := range subcommands { + if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource { + break + } + } + var options *resourceOptions + if requiresResource { + options = bindResourceFlags(cmd.Flags()) + } + + // Bind flags hook. + for _, tuple := range subcommands { + if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags { + subcommand.BindFlags(cmd.Flags()) + } + } + + return options +} + +type executionHooksFactory struct { + // fs is the filesystem abstraction to scaffold files to. + fs machinery.Filesystem + // store is the backend used to load/save the project configuration. + store store.Store + // subcommands are the tuples representing the set of subcommands provided by the resolved plugins. + subcommands []keySubcommandTuple + // errorMessage is prepended to returned errors. + errorMessage string + // projectVersion is the project version that will be used to create new project configurations. + // It is only used for initialization. + projectVersion config.Version + // pluginChain is the plugin chain configured for this project. + pluginChain []string +} + +// subcommandTuples iterates over the subcommands skipping those flagged to do so. +func (factory *executionHooksFactory) subcommandTuples() chan keySubcommandTuple { + ch := make(chan keySubcommandTuple, len(factory.subcommands)) + for i, tuple := range factory.subcommands { + if !tuple.skip { + tuple.i = i + ch <- tuple + } + } + close(ch) + return ch +} + +// handleSpecialErrors performs certain actions if the errors adhere to certain types. +// +// - plugin.ExitError: remaining hooks of this plugin are skipped, error is logged and dismissed. +func (factory *executionHooksFactory) handleSpecialErrors(tuple keySubcommandTuple, err error) error { + var exitError plugin.ExitError + switch { + case errors.As(err, &exitError): + fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason) + factory.subcommands[tuple.i].skip = true + return nil + default: + return err + } +} + +// wrapError wraps the provided error with the message provided at construction. +func (factory executionHooksFactory) wrapError(err error) error { + if err == nil || factory.errorMessage == "" { + return err + } + return fmt.Errorf("%s: %w", factory.errorMessage, err) +} + +// preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource, +// and executes inject config, inject resource, and pre-scaffold hooks. +func (factory *executionHooksFactory) preRunEFunc( + options *resourceOptions, + createConfig bool, +) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - err := cfg.Load() - if os.IsNotExist(err) { - return fmt.Errorf("%s: unable to find configuration file, project must be initialized", msg) - } else if err != nil { - return fmt.Errorf("%s: unable to load configuration file: %w", msg, err) + if createConfig { + // Check if a project configuration is already present. + if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { + return factory.wrapError(fmt.Errorf("already initialized")) + } + + // Initialize the project configuration. + if err := factory.store.New(factory.projectVersion); err != nil { + return factory.wrapError(fmt.Errorf("error initializing project configuration: %w", err)) + } + } else { + // Load the project configuration. + if err := factory.store.Load(); os.IsNotExist(err) { + return factory.wrapError(fmt.Errorf("unable to find configuration file, project must be initialized")) + } else if err != nil { + return factory.wrapError(fmt.Errorf("unable to load configuration file: %w", err)) + } + } + cfg := factory.store.Config() + + // Set the pluginChain field. + if len(factory.pluginChain) != 0 { + _ = cfg.SetPluginChain(factory.pluginChain) + } + + // Create the resource if non-nil options provided + var res *resource.Resource + if options != nil { + // TODO: offer a flag instead of hard-coding project-wide domain + options.Domain = cfg.GetDomain() + if err := options.validate(); err != nil { + return factory.wrapError(fmt.Errorf("unable to create resource: %w", err)) + } + res = options.newResource() + } + + // Inject-config hook. + for tuple := range factory.subcommandTuples() { + if subcommand, requiresConfig := tuple.subcommand.(plugin.RequiresConfig); requiresConfig { + if err := factory.handleSpecialErrors(tuple, subcommand.InjectConfig(cfg)); err != nil { + return factory.wrapError(fmt.Errorf("unable to inject configuration to %q: %w", tuple.key, err)) + } + } + } + + // Inject resource hook. + if res != nil { + for tuple := range factory.subcommandTuples() { + if subcommand, requiresResource := tuple.subcommand.(plugin.RequiresResource); requiresResource { + if err := factory.handleSpecialErrors(tuple, subcommand.InjectResource(res)); err != nil { + return factory.wrapError(fmt.Errorf("unable to inject the resource to %q: %w", tuple.key, err)) + } + } + } + + if err := res.Validate(); err != nil { + return factory.wrapError(fmt.Errorf("created invalid resource: %w", err)) + } + } + + // Pre-scaffold hook. + for tuple := range factory.subcommandTuples() { + if subcommand, hasPreScaffold := tuple.subcommand.(plugin.HasPreScaffold); hasPreScaffold { + if err := factory.handleSpecialErrors(tuple, subcommand.PreScaffold(factory.fs)); err != nil { + return factory.wrapError(fmt.Errorf("unable to run pre-scaffold tasks of %q: %w", tuple.key, err)) + } + } } - subcmd.InjectConfig(cfg.Config()) return nil } } -// runECmdFunc returns a cobra RunE function that runs subcommand -func runECmdFunc(fs machinery.Filesystem, subcmd plugin.Subcommand, msg string) func(*cobra.Command, []string) error { +// runEFunc returns a cobra RunE function that executes the scaffold hook. +func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - if err := subcmd.Run(fs); err != nil { - return fmt.Errorf("%s: %v", msg, err) + // Scaffold hook. + for tuple := range factory.subcommandTuples() { + if err := factory.handleSpecialErrors(tuple, tuple.subcommand.Scaffold(factory.fs)); err != nil { + return factory.wrapError(fmt.Errorf("unable to scaffold with %q: %v", tuple.key, err)) + } } + return nil } } -// postRunECmdFunc returns a cobra PostRunE function that saves the configuration file -func postRunECmdFunc(cfg store.Store, msg string) func(*cobra.Command, []string) error { +// postRunEFunc returns a cobra RunE function that saves the configuration +// and executes the post-scaffold hook. +func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { - err := cfg.Save() + err := factory.store.Save() if err != nil { - return fmt.Errorf("%s: unable to save configuration file: %w", msg, err) + return factory.wrapError(fmt.Errorf("unable to save configuration file: %w", err)) + } + + // Post-scaffold hook. + for tuple := range factory.subcommandTuples() { + if subcommand, hasPostScaffold := tuple.subcommand.(plugin.HasPostScaffold); hasPostScaffold { + if err := factory.handleSpecialErrors(tuple, subcommand.PostScaffold()); err != nil { + return factory.wrapError(fmt.Errorf("unable to run post-scaffold tasks of %q: %w", tuple.key, err)) + } + } } + return nil } } diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index 07ff17a3c83..8f867790410 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -14,77 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const editErrorMsg = "failed to edit project" + func (c CLI) newEditCmd() *cobra.Command { - ctx := c.newEditContext() cmd := &cobra.Command{ - Use: "edit", - Short: "This command will edit the project configuration", - Long: ctx.Description, - Example: ctx.Examples, + Use: "edit", + Short: "Update the project configuration", + Long: `Edit the project configuration. +`, RunE: errCmdFunc( fmt.Errorf("project must be initialized"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindEdit(ctx, cmd) - return cmd -} - -func (c CLI) newEditContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Edit the project configuration. -`, - } -} - -func (c CLI) bindEdit(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var editPlugin plugin.Edit - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Edit) - if isValid { - if editPlugin != nil { - err := fmt.Errorf( - "duplicate edit project plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(editPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - editPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Edit. + subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Edit) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Edit).GetEditSubcommand() + }, + ) - if editPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project edit plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"edit project"}) + return cmd } - subcommand := editPlugin.GetEditSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + c.applySubcommandHooks(cmd, subcommands, editErrorMsg, false) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to edit project with %q", plugin.KeyFor(editPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 04a38bdbd80..390ed5b2a39 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -17,9 +17,7 @@ limitations under the License. package cli import ( - "errors" "fmt" - "os" "sort" "strconv" "strings" @@ -27,46 +25,54 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v3/pkg/config" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const initErrorMsg = "failed to initialize project" + func (c CLI) newInitCmd() *cobra.Command { - ctx := c.newInitContext() cmd := &cobra.Command{ - Use: "init", - Short: "Initialize a new project", - Long: ctx.Description, - Example: ctx.Examples, + Use: "init", + Short: "Initialize a new project", + Long: `Initialize a new project. + +For further help about a specific plugin, set --plugins. +`, + Example: c.getInitHelpExamples(), Run: func(cmd *cobra.Command, args []string) {}, } // Register --project-version on the dynamically created command // so that it shows up in help and does not cause a parse error. - cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), - fmt.Sprintf("project version, possible values: (%s)", strings.Join(c.getAvailableProjectVersions(), ", "))) - // The --plugins flag can only be called to init projects v2+. - if c.projectVersion.Compare(cfgv2.Version) == 1 { - cmd.Flags().StringSlice(pluginsFlag, nil, - "Name and optionally version of the plugin to initialize the project with. "+ - fmt.Sprintf("Available plugins: (%s)", strings.Join(c.getAvailablePlugins(), ", "))) + cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") + + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. + if len(c.resolvedPlugins) == 0 { + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindInit(ctx, cmd) - return cmd -} + // Obtain the plugin keys and subcommands from the plugins that implement plugin.Init. + subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.Init) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.Init).GetInitSubcommand() + }, + ) + + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"project initialization"}) + return cmd + } -func (c CLI) newInitContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Initialize a new project. + c.applySubcommandHooks(cmd, subcommands, initErrorMsg, true) -For further help about a specific project version, set --project-version. -`, - Examples: c.getInitHelpExamples(), - } + return cmd } func (c CLI) getInitHelpExamples() string { @@ -109,55 +115,3 @@ func (c CLI) getAvailablePlugins() (pluginKeys []string) { sort.Strings(pluginKeys) return pluginKeys } - -func (c CLI) bindInit(ctx plugin.Context, cmd *cobra.Command) { - if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf("no resolved plugins, please specify plugins with --%s or/and --%s flags", - projectVersionFlag, pluginsFlag)) - return - } - - var initPlugin plugin.Init - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.Init) - if isValid { - if initPlugin != nil { - err := fmt.Errorf("duplicate initialization plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(initPlugin), plugin.KeyFor(p)) - cmdErrNoHelp(cmd, err) - return - } - initPlugin = tmpPlugin - } - } - - if initPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a project init plugin: %v", c.pluginKeys)) - return - } - - subcommand := initPlugin.GetInitSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to initialize project with %q", plugin.KeyFor(initPlugin)) - cmd.PreRunE = func(*cobra.Command, []string) error { - // Check if a config is initialized. - if err := cfg.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("%s: already initialized", msg) - } - - err := cfg.New(c.projectVersion) - if err != nil { - return fmt.Errorf("%s: error initializing project configuration: %w", msg, err) - } - - subcommand.InjectConfig(cfg.Config()) - return nil - } - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) -} diff --git a/pkg/cli/options.go b/pkg/cli/options.go index bed48d81512..4ed0dce5c75 100644 --- a/pkg/cli/options.go +++ b/pkg/cli/options.go @@ -44,15 +44,21 @@ func WithVersion(version string) Option { } } -// WithDefaultProjectVersion is an Option that sets the CLI's default project version. +// WithPlugins is an Option that sets the CLI's plugins. // -// Setting an invalid version results in an error. -func WithDefaultProjectVersion(version config.Version) Option { +// Specifying any invalid plugin results in an error. +func WithPlugins(plugins ...plugin.Plugin) Option { return func(c *CLI) error { - if err := version.Validate(); err != nil { - return fmt.Errorf("broken pre-set default project version %q: %v", version, err) + for _, p := range plugins { + key := plugin.KeyFor(p) + if _, isConflicting := c.plugins[key]; isConflicting { + return fmt.Errorf("two plugins have the same key: %q", key) + } + if err := plugin.Validate(p); err != nil { + return fmt.Errorf("broken pre-set plugin %q: %v", key, err) + } + c.plugins[key] = p } - c.defaultProjectVersion = version return nil } } @@ -63,7 +69,7 @@ func WithDefaultProjectVersion(version config.Version) Option { func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { return func(c *CLI) error { if err := projectVersion.Validate(); err != nil { - return fmt.Errorf("broken pre-set project version %q for default plugins: %v", projectVersion, err) + return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err) } if len(plugins) == 0 { return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion) @@ -81,21 +87,15 @@ func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) } } -// WithPlugins is an Option that sets the CLI's plugins. +// WithDefaultProjectVersion is an Option that sets the CLI's default project version. // -// Specifying any invalid plugin results in an error. -func WithPlugins(plugins ...plugin.Plugin) Option { +// Setting an invalid version results in an error. +func WithDefaultProjectVersion(version config.Version) Option { return func(c *CLI) error { - for _, p := range plugins { - key := plugin.KeyFor(p) - if _, isConflicting := c.plugins[key]; isConflicting { - return fmt.Errorf("two plugins have the same key: %q", key) - } - if err := plugin.Validate(p); err != nil { - return fmt.Errorf("broken pre-set plugin %q: %v", key, err) - } - c.plugins[key] = p + if err := version.Validate(); err != nil { + return fmt.Errorf("broken pre-set default project version %q: %v", version, err) } + c.defaultProjectVersion = version return nil } } diff --git a/pkg/cli/options_test.go b/pkg/cli/options_test.go index 8943a710df5..7c40060041e 100644 --- a/pkg/cli/options_test.go +++ b/pkg/cli/options_test.go @@ -17,9 +17,8 @@ limitations under the License. package cli import ( - "fmt" - . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "github.com/spf13/cobra" @@ -68,142 +67,136 @@ var _ = Describe("CLI options", func() { }) }) - Context("WithDefaultProjectVersion", func() { - It("should return a valid CLI", func() { - defaultProjectVersions := []config.Version{ - {Number: 1}, - {Number: 2}, - {Number: 3, Stage: stage.Alpha}, - } - for _, defaultProjectVersion := range defaultProjectVersions { - By(fmt.Sprintf("using %q", defaultProjectVersion)) - c, err = newCLI(WithDefaultProjectVersion(defaultProjectVersion)) - Expect(err).NotTo(HaveOccurred()) - Expect(c).NotTo(BeNil()) - Expect(c.defaultProjectVersion).To(Equal(defaultProjectVersion)) - } - }) - - It("should return an error", func() { - defaultProjectVersions := []config.Version{ - {}, // Empty default project version - {Number: 1, Stage: stage.Stage(27)}, // Invalid stage in default project version - } - for _, defaultProjectVersion := range defaultProjectVersions { - By(fmt.Sprintf("using %q", defaultProjectVersion)) - _, err = newCLI(WithDefaultProjectVersion(defaultProjectVersion)) - Expect(err).To(HaveOccurred()) - } - }) - }) - - Context("WithDefaultPlugins", func() { + Context("WithPlugins", func() { It("should return a valid CLI", func() { - c, err = newCLI(WithDefaultPlugins(projectVersion, p)) + c, err = newCLI(WithPlugins(p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) - Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) + Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p})) }) - When("providing an invalid project version", func() { + When("providing plugins with same keys", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) + _, err = newCLI(WithPlugins(p, p)) Expect(err).To(HaveOccurred()) }) }) - When("providing an empty set of plugins", func() { + When("providing plugins with same keys in two steps", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion)) + _, err = newCLI(WithPlugins(p), WithPlugins(p)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np1)) + _, err = newCLI(WithPlugins(np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np2)) + _, err = newCLI(WithPlugins(np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np3)) + _, err = newCLI(WithPlugins(np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(projectVersion, np4)) - Expect(err).To(HaveOccurred()) - }) - }) - - When("providing a default plugin for an unsupported project version", func() { - It("should return an error", func() { - _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) + _, err = newCLI(WithPlugins(np4)) Expect(err).To(HaveOccurred()) }) }) }) - Context("WithPlugins", func() { + Context("WithDefaultPlugins", func() { It("should return a valid CLI", func() { - c, err = newCLI(WithPlugins(p)) + c, err = newCLI(WithDefaultPlugins(projectVersion, p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) - Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p})) + Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) }) - When("providing plugins with same keys", func() { + When("providing an invalid project version", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(p, p)) + _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) Expect(err).To(HaveOccurred()) }) }) - When("providing plugins with same keys in two steps", func() { + When("providing an empty set of plugins", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(p), WithPlugins(p)) + _, err = newCLI(WithDefaultPlugins(projectVersion)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np1)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np2)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np3)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { - _, err = newCLI(WithPlugins(np4)) + _, err = newCLI(WithDefaultPlugins(projectVersion, np4)) Expect(err).To(HaveOccurred()) }) }) + + When("providing a default plugin for an unsupported project version", func() { + It("should return an error", func() { + _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) + Expect(err).To(HaveOccurred()) + }) + }) + }) + + Context("WithDefaultProjectVersion", func() { + DescribeTable("should return a valid CLI", + func(projectVersion config.Version) { + c, err = newCLI(WithDefaultProjectVersion(projectVersion)) + Expect(err).NotTo(HaveOccurred()) + Expect(c).NotTo(BeNil()) + Expect(c.defaultProjectVersion).To(Equal(projectVersion)) + }, + Entry("for version `2`", config.Version{Number: 2}), + Entry("for version `3-alpha`", config.Version{Number: 3, Stage: stage.Alpha}), + Entry("for version `3`", config.Version{Number: 3}), + ) + + DescribeTable("should fail", + func(projectVersion config.Version) { + _, err = newCLI(WithDefaultProjectVersion(projectVersion)) + Expect(err).To(HaveOccurred()) + }, + Entry("for empty version", config.Version{}), + Entry("for invalid stage", config.Version{Number: 1, Stage: stage.Stage(27)}), + ) }) Context("WithExtraCommands", func() { diff --git a/pkg/cli/resource.go b/pkg/cli/resource.go new file mode 100644 index 00000000000..100bb32aa66 --- /dev/null +++ b/pkg/cli/resource.go @@ -0,0 +1,85 @@ +/* +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 cli + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + groupPresent = "group flag present but empty" + versionPresent = "version flag present but empty" + kindPresent = "kind flag present but empty" +) + +// resourceOptions contains the information required to build a new resource.Resource. +type resourceOptions struct { + resource.GVK +} + +func bindResourceFlags(fs *pflag.FlagSet) *resourceOptions { + options := &resourceOptions{} + + fs.StringVar(&options.Group, "group", "", "resource Group") + fs.StringVar(&options.Version, "version", "", "resource Version") + fs.StringVar(&options.Kind, "kind", "", "resource Kind") + + return options +} + +// validate verifies that all the fields have valid values. +func (opts resourceOptions) validate() error { + // Check that the required flags did not get a flag as their value. + // We can safely look for a '-' as the first char as none of the fields accepts it. + // NOTE: We must do this for all the required flags first or we may output the wrong + // error as flags may seem to be missing because Cobra assigned them to another flag. + if strings.HasPrefix(opts.Group, "-") { + return fmt.Errorf(groupPresent) + } + if strings.HasPrefix(opts.Version, "-") { + return fmt.Errorf(versionPresent) + } + if strings.HasPrefix(opts.Kind, "-") { + return fmt.Errorf(kindPresent) + } + + // We do not check here if the GVK values are empty because that would + // make them mandatory and some plugins may want to set default values. + // Instead, this is checked by resource.GVK.Validate() + + return nil +} + +// newResource creates a new resource from the options +func (opts resourceOptions) newResource() *resource.Resource { + return &resource.Resource{ + GVK: resource.GVK{ // Remove whitespaces to prevent values like " " pass validation + Group: strings.TrimSpace(opts.Group), + Domain: strings.TrimSpace(opts.Domain), + Version: strings.TrimSpace(opts.Version), + Kind: strings.TrimSpace(opts.Kind), + }, + Plural: resource.RegularPlural(opts.Kind), + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + } +} diff --git a/pkg/cli/resource_test.go b/pkg/cli/resource_test.go new file mode 100644 index 00000000000..6a4cd378180 --- /dev/null +++ b/pkg/cli/resource_test.go @@ -0,0 +1,110 @@ +/* +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 cli + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("resourceOptions", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" + ) + + var ( + fullGVK = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } + noDomainGVK = resource.GVK{ + Group: group, + Version: version, + Kind: kind, + } + noGroupGVK = resource.GVK{ + Domain: domain, + Version: version, + Kind: kind, + } + ) + + Context("validate", func() { + DescribeTable("should succeed for valid options", + func(options resourceOptions) { Expect(options.validate()).To(Succeed()) }, + Entry("full GVK", resourceOptions{GVK: fullGVK}), + Entry("missing domain", resourceOptions{GVK: noDomainGVK}), + Entry("missing group", resourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should fail for invalid options", + func(options resourceOptions) { Expect(options.validate()).NotTo(Succeed()) }, + Entry("group flag captured another flag", resourceOptions{GVK: resource.GVK{Group: "--version"}}), + Entry("version flag captured another flag", resourceOptions{GVK: resource.GVK{Version: "--kind"}}), + Entry("kind flag captured another flag", resourceOptions{GVK: resource.GVK{Kind: "--group"}}), + ) + }) + + Context("newResource", func() { + DescribeTable("should succeed if the Resource is valid", + func(options resourceOptions) { + Expect(options.validate()).To(Succeed()) + + resource := options.newResource() + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + // Plural is checked in the next test + Expect(resource.Path).To(Equal("")) + Expect(resource.API).NotTo(BeNil()) + Expect(resource.API.CRDVersion).To(Equal("")) + Expect(resource.API.Namespaced).To(BeFalse()) + Expect(resource.Controller).To(BeFalse()) + Expect(resource.Webhooks).NotTo(BeNil()) + Expect(resource.Webhooks.WebhookVersion).To(Equal("")) + Expect(resource.Webhooks.Defaulting).To(BeFalse()) + Expect(resource.Webhooks.Validation).To(BeFalse()) + Expect(resource.Webhooks.Conversion).To(BeFalse()) + }, + Entry("full GVK", resourceOptions{GVK: fullGVK}), + Entry("missing domain", resourceOptions{GVK: noDomainGVK}), + Entry("missing group", resourceOptions{GVK: noGroupGVK}), + ) + + DescribeTable("should default the Plural by pluralizing the Kind", + func(kind, plural string) { + options := resourceOptions{GVK: resource.GVK{Group: group, Version: version, Kind: kind}} + Expect(options.validate()).To(Succeed()) + + resource := options.newResource() + Expect(resource.Validate()).To(Succeed()) + Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) + Expect(resource.Plural).To(Equal(plural)) + }, + Entry("for `FirstMate`", "FirstMate", "firstmates"), + Entry("for `Fish`", "Fish", "fish"), + Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), + ) + }) +}) diff --git a/pkg/cli/root.go b/pkg/cli/root.go index af4be6be247..93d0c0e863d 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -40,14 +40,11 @@ func (c CLI) newRootCmd() *cobra.Command { }, } - // Global flags for all subcommands - // NOTE: the current plugin resolution doesn't allow to provide values to this flag different to those configured - // for the project, so default values need to be empty and considered when these two sources are compared. - // Another approach would be to allow users to overwrite the project configuration values. In this case, flags - // would take precedence over project configuration, which would take precedence over CLI defaults. - fs := cmd.PersistentFlags() - fs.String(projectVersionFlag, "", "project version") - fs.StringSlice(pluginsFlag, nil, "plugin keys of the plugin to initialize the project with") + // Global flags for all subcommands. + cmd.PersistentFlags().StringSlice(pluginsFlag, nil, "plugin keys to be used for this subcommand execution") + + // Register --project-version on the root command so that it shows up in help. + cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") // As the root command will be used to shot the help message under some error conditions, // like during plugin resolving, we need to allow unknown flags to prevent parsing errors. @@ -56,10 +53,10 @@ func (c CLI) newRootCmd() *cobra.Command { return cmd } -// rootExamples builds the examples string for the root command +// rootExamples builds the examples string for the root command before resolving plugins func (c CLI) rootExamples() string { str := fmt.Sprintf(`The first step is to initialize your project: - %[1]s init --project-version= --plugins= + %[1]s init [--plugins= [--project-version=]] is a comma-separated list of plugin keys from the following table and a supported project version for these plugins. @@ -68,21 +65,19 @@ and a supported project version for these plugins. For more specific help for the init command of a certain plugins and project version configuration please run: - %[1]s init --help --project-version= --plugins= + %[1]s init --help --plugins= [--project-version=] `, c.commandName, c.getPluginTable()) - str += fmt.Sprintf("\nDefault project version: %s\n", c.defaultProjectVersion) - - if defaultPlugins, hasDefaultPlugins := c.defaultPlugins[c.defaultProjectVersion]; hasDefaultPlugins { - str += fmt.Sprintf("Default plugin keys: %q\n", strings.Join(defaultPlugins, ",")) + if len(c.defaultPlugins) != 0 { + if defaultPlugins, found := c.defaultPlugins[c.defaultProjectVersion]; found { + str += fmt.Sprintf("\nDefault plugin keys: %q\n", strings.Join(defaultPlugins, ",")) + } } - str += fmt.Sprintf(` -After the project has been initialized, run - %[1]s --help -to obtain further info about available commands.`, - c.commandName) + if c.defaultProjectVersion.Validate() == nil { + str += fmt.Sprintf("Default project version: %q\n", c.defaultProjectVersion) + } return str } diff --git a/pkg/cli/cli_suite_test.go b/pkg/cli/suite_test.go similarity index 100% rename from pkg/cli/cli_suite_test.go rename to pkg/cli/suite_test.go diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index 80d102d1bc8..bea5b0b17a7 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -14,77 +14,54 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli // nolint:dupl +package cli //nolint:dupl import ( "fmt" "github.com/spf13/cobra" - yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) +const webhookErrorMsg = "failed to create webhook" + func (c CLI) newCreateWebhookCmd() *cobra.Command { - ctx := c.newWebhookContext() cmd := &cobra.Command{ - Use: "webhook", - Short: "Scaffold a webhook for an API resource", - Long: ctx.Description, - Example: ctx.Examples, + Use: "webhook", + Short: "Scaffold a webhook for an API resource", + Long: `Scaffold a webhook for an API resource. +`, RunE: errCmdFunc( fmt.Errorf("webhook subcommand requires an existing project"), ), } - // Lookup the plugin for projectVersion and bind it to the command. - c.bindCreateWebhook(ctx, cmd) - return cmd -} - -func (c CLI) newWebhookContext() plugin.Context { - return plugin.Context{ - CommandName: c.commandName, - Description: `Scaffold a webhook for an API resource. -`, - } -} - -// nolint:dupl -func (c CLI) bindCreateWebhook(ctx plugin.Context, cmd *cobra.Command) { + // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of + // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { - cmdErr(cmd, fmt.Errorf(noPluginError)) - return + cmdErr(cmd, noResolvedPluginError{}) + return cmd } - var createWebhookPlugin plugin.CreateWebhook - for _, p := range c.resolvedPlugins { - tmpPlugin, isValid := p.(plugin.CreateWebhook) - if isValid { - if createWebhookPlugin != nil { - err := fmt.Errorf("duplicate webhook creation plugins (%s, %s), use a more specific plugin key", - plugin.KeyFor(createWebhookPlugin), plugin.KeyFor(p)) - cmdErr(cmd, err) - return - } - createWebhookPlugin = tmpPlugin - } - } + // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateWebhook. + subcommands := c.filterSubcommands( + func(p plugin.Plugin) bool { + _, isValid := p.(plugin.CreateWebhook) + return isValid + }, + func(p plugin.Plugin) plugin.Subcommand { + return p.(plugin.CreateWebhook).GetCreateWebhookSubcommand() + }, + ) - if createWebhookPlugin == nil { - cmdErr(cmd, fmt.Errorf("resolved plugins do not provide a webhook creation plugin: %v", c.pluginKeys)) - return + // Verify that there is at least one remaining plugin. + if len(subcommands) == 0 { + cmdErr(cmd, noAvailablePluginError{"webhook creation"}) + return cmd } - subcommand := createWebhookPlugin.GetCreateWebhookSubcommand() - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples + c.applySubcommandHooks(cmd, subcommands, webhookErrorMsg, false) - cfg := yamlstore.New(c.fs) - msg := fmt.Sprintf("failed to create webhook with %q", plugin.KeyFor(createWebhookPlugin)) - cmd.PreRunE = preRunECmdFunc(subcommand, cfg, msg) - cmd.RunE = runECmdFunc(c.fs, subcommand, msg) - cmd.PostRunE = postRunECmdFunc(cfg, msg) + return cmd } diff --git a/pkg/machinery/errors.go b/pkg/machinery/errors.go index e053e156740..af015ac30e9 100644 --- a/pkg/machinery/errors.go +++ b/pkg/machinery/errors.go @@ -18,8 +18,6 @@ package machinery import ( "fmt" - - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" ) // This file contains the errors returned by the scaffolding machinery @@ -45,16 +43,6 @@ func (e SetTemplateDefaultsError) Unwrap() error { return e.error } -// PluginError is a wrapper error that will be used for errors returned by model.Plugin.Pipe -type PluginError struct { - error -} - -// Unwrap implements Wrapper interface -func (e PluginError) Unwrap() error { - return e.error -} - // ExistsFileError is a wrapper error that will be used for errors when checking for a file existence type ExistsFileError struct { error @@ -138,7 +126,7 @@ func (e ModelAlreadyExistsError) Error() string { // UnknownIfExistsActionError is returned if the if-exists-action is unknown type UnknownIfExistsActionError struct { path string - ifExistsAction file.IfExistsAction + ifExistsAction IfExistsAction } // Error implements error interface diff --git a/pkg/machinery/errors_test.go b/pkg/machinery/errors_test.go index d0ad7405c2b..2c4d682e957 100644 --- a/pkg/machinery/errors_test.go +++ b/pkg/machinery/errors_test.go @@ -37,7 +37,6 @@ var _ = Describe("Errors", func() { }, Entry("for validate errors", ValidateError{testErr}), Entry("for set template defaults errors", SetTemplateDefaultsError{testErr}), - Entry("for plugin errors", PluginError{testErr}), Entry("for file existence errors", ExistsFileError{testErr}), Entry("for file opening errors", OpenFileError{testErr}), Entry("for directory creation errors", CreateDirectoryError{testErr}), diff --git a/pkg/model/file/file.go b/pkg/machinery/file.go similarity index 75% rename from pkg/model/file/file.go rename to pkg/machinery/file.go index 3b9db1cd341..bf053d14ce5 100644 --- a/pkg/model/file/file.go +++ b/pkg/machinery/file.go @@ -14,30 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery // IfExistsAction determines what to do if the scaffold file already exists type IfExistsAction int const ( - // Skip skips the file and moves to the next one - Skip IfExistsAction = iota + // SkipFile skips the file and moves to the next one + SkipFile IfExistsAction = iota // Error returns an error and stops processing Error - // Overwrite truncates and overwrites the existing file - Overwrite + // OverwriteFile truncates and overwrites the existing file + OverwriteFile ) // File describes a file that will be written type File struct { // Path is the file to write - Path string `json:"path,omitempty"` + Path string // Contents is the generated output - Contents string `json:"contents,omitempty"` + Contents string // IfExistsAction determines what to do if the file exists - IfExistsAction IfExistsAction `json:"ifExistsAction,omitempty"` + IfExistsAction IfExistsAction } diff --git a/pkg/model/file/funcmap.go b/pkg/machinery/funcmap.go similarity index 86% rename from pkg/model/file/funcmap.go rename to pkg/machinery/funcmap.go index a0a9432427b..ac25e272d70 100644 --- a/pkg/model/file/funcmap.go +++ b/pkg/machinery/funcmap.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "fmt" @@ -39,10 +39,9 @@ func isEmptyString(s string) bool { } // hashFNV will generate a random string useful for generating a unique string -func hashFNV(s string) (string, error) { +func hashFNV(s string) string { hasher := fnv.New32a() - if _, err := hasher.Write([]byte(s)); err != nil { - return "", err - } - return fmt.Sprintf("%x", hasher.Sum(nil)), nil + // Hash.Write never returns an error + _, _ = hasher.Write([]byte(s)) + return fmt.Sprintf("%x", hasher.Sum(nil)) } diff --git a/pkg/machinery/funcmap_test.go b/pkg/machinery/funcmap_test.go new file mode 100644 index 00000000000..7bb33df48af --- /dev/null +++ b/pkg/machinery/funcmap_test.go @@ -0,0 +1,45 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("funcmap functions", func() { + Context("isEmptyString", func() { + It("should return true for empty strings", func() { + Expect(isEmptyString("")).To(BeTrue()) + }) + + DescribeTable("should return false for any other string", + func(str string) { Expect(isEmptyString(str)).To(BeFalse()) }, + Entry(`for "a"`, "a"), + Entry(`for "1"`, "1"), + Entry(`for "-"`, "-"), + Entry(`for "."`, "."), + ) + }) + + Context("hashFNV", func() { + It("should hash the input", func() { + Expect(hashFNV("test")).To(Equal("afd071e5")) + }) + }) +}) diff --git a/pkg/machinery/injector.go b/pkg/machinery/injector.go new file mode 100644 index 00000000000..5675a8a9ec3 --- /dev/null +++ b/pkg/machinery/injector.go @@ -0,0 +1,66 @@ +/* +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 machinery + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// injector is used to inject certain fields to file templates. +type injector struct { + // config stores the project configuration. + config config.Config + + // boilerplate is the copyright comment added at the top of scaffolded files. + boilerplate string + + // resource contains the information of the API that is being scaffolded. + resource *resource.Resource +} + +// injectInto injects fields from the universe into the builder +func (i injector) injectInto(builder Builder) { + // Inject project configuration + if i.config != nil { + if builderWithDomain, hasDomain := builder.(HasDomain); hasDomain { + builderWithDomain.InjectDomain(i.config.GetDomain()) + } + if builderWithRepository, hasRepository := builder.(HasRepository); hasRepository { + builderWithRepository.InjectRepository(i.config.GetRepository()) + } + if builderWithProjectName, hasProjectName := builder.(HasProjectName); hasProjectName { + builderWithProjectName.InjectProjectName(i.config.GetProjectName()) + } + if builderWithMultiGroup, hasMultiGroup := builder.(HasMultiGroup); hasMultiGroup { + builderWithMultiGroup.InjectMultiGroup(i.config.IsMultiGroup()) + } + if builderWithComponentConfig, hasComponentConfig := builder.(HasComponentConfig); hasComponentConfig { + builderWithComponentConfig.InjectComponentConfig(i.config.IsComponentConfig()) + } + } + // Inject boilerplate + if builderWithBoilerplate, hasBoilerplate := builder.(HasBoilerplate); hasBoilerplate { + builderWithBoilerplate.InjectBoilerplate(i.boilerplate) + } + // Inject resource + if i.resource != nil { + if builderWithResource, hasResource := builder.(HasResource); hasResource { + builderWithResource.InjectResource(i.resource) + } + } +} diff --git a/pkg/machinery/injector_test.go b/pkg/machinery/injector_test.go new file mode 100644 index 00000000000..7c5c3038f4b --- /dev/null +++ b/pkg/machinery/injector_test.go @@ -0,0 +1,295 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +type templateBase struct { + path string + ifExistsAction IfExistsAction +} + +func (t templateBase) GetPath() string { + return t.path +} + +func (t templateBase) GetIfExistsAction() IfExistsAction { + return t.ifExistsAction +} + +type templateWithDomain struct { + templateBase + domain string +} + +func (t *templateWithDomain) InjectDomain(domain string) { + t.domain = domain +} + +type templateWithRepository struct { + templateBase + repository string +} + +func (t *templateWithRepository) InjectRepository(repository string) { + t.repository = repository +} + +type templateWithProjectName struct { + templateBase + projectName string +} + +func (t *templateWithProjectName) InjectProjectName(projectName string) { + t.projectName = projectName +} + +type templateWithMultiGroup struct { + templateBase + multiGroup bool +} + +func (t *templateWithMultiGroup) InjectMultiGroup(multiGroup bool) { + t.multiGroup = multiGroup +} + +type templateWithComponentConfig struct { + templateBase + componentConfig bool +} + +func (t *templateWithComponentConfig) InjectComponentConfig(componentConfig bool) { + t.componentConfig = componentConfig +} + +type templateWithBoilerplate struct { + templateBase + boilerplate string +} + +func (t *templateWithBoilerplate) InjectBoilerplate(boilerplate string) { + t.boilerplate = boilerplate +} + +type templateWithResource struct { + templateBase + resource *resource.Resource +} + +func (t *templateWithResource) InjectResource(res *resource.Resource) { + t.resource = res +} + +var _ = Describe("injector", func() { + var tmp = templateBase{ + path: "my/path/to/file", + ifExistsAction: Error, + } + + Context("injectInto", func() { + Context("Config", func() { + var c config.Config + + BeforeEach(func() { + c = cfgv3.New() + }) + + Context("Domain", func() { + var template *templateWithDomain + + BeforeEach(func() { + template = &templateWithDomain{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.domain).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a domain set", func() { + injector{config: c}.injectInto(template) + Expect(template.domain).To(Equal("")) + }) + + It("should inject if the config has a domain set", func() { + const domain = "my.domain" + Expect(c.SetDomain(domain)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.domain).To(Equal(domain)) + }) + }) + + Context("Repository", func() { + var template *templateWithRepository + + BeforeEach(func() { + template = &templateWithRepository{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.repository).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a repository set", func() { + injector{config: c}.injectInto(template) + Expect(template.repository).To(Equal("")) + }) + + It("should inject if the config has a repository set", func() { + const repo = "test" + Expect(c.SetRepository(repo)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.repository).To(Equal(repo)) + }) + }) + + Context("Project name", func() { + var template *templateWithProjectName + + BeforeEach(func() { + template = &templateWithProjectName{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.projectName).To(Equal("")) + }) + + It("should not inject anything if the config doesn't have a project name set", func() { + injector{config: c}.injectInto(template) + Expect(template.projectName).To(Equal("")) + }) + + It("should inject if the config has a project name set", func() { + const projectName = "my project" + Expect(c.SetProjectName(projectName)).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.projectName).To(Equal(projectName)) + }) + }) + + Context("Multi-group", func() { + var template *templateWithMultiGroup + + BeforeEach(func() { + template = &templateWithMultiGroup{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.multiGroup).To(BeFalse()) + }) + + It("should not set the flag if the config doesn't have the multi-group flag set", func() { + injector{config: c}.injectInto(template) + Expect(template.multiGroup).To(BeFalse()) + }) + + It("should set the flag if the config has the multi-group flag set", func() { + Expect(c.SetMultiGroup()).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.multiGroup).To(BeTrue()) + }) + }) + + Context("Component config", func() { + var template *templateWithComponentConfig + + BeforeEach(func() { + template = &templateWithComponentConfig{templateBase: tmp} + }) + + It("should not inject anything if the config is nil", func() { + injector{}.injectInto(template) + Expect(template.componentConfig).To(BeFalse()) + }) + + It("should not set the flag if the config doesn't have the component config flag set", func() { + injector{config: c}.injectInto(template) + Expect(template.componentConfig).To(BeFalse()) + }) + + It("should set the flag if the config has the component config flag set", func() { + Expect(c.SetComponentConfig()).To(Succeed()) + + injector{config: c}.injectInto(template) + Expect(template.componentConfig).To(BeTrue()) + }) + }) + }) + + Context("Boilerplate", func() { + var template *templateWithBoilerplate + + BeforeEach(func() { + template = &templateWithBoilerplate{templateBase: tmp} + }) + + It("should not inject anything if no boilerplate was set", func() { + injector{}.injectInto(template) + Expect(template.boilerplate).To(Equal("")) + }) + + It("should inject if the a boilerplate was set", func() { + const boilerplate = `Copyright "The Kubernetes Authors"` + + injector{boilerplate: boilerplate}.injectInto(template) + Expect(template.boilerplate).To(Equal(boilerplate)) + }) + }) + + Context("Resource", func() { + var template *templateWithResource + + BeforeEach(func() { + template = &templateWithResource{templateBase: tmp} + }) + + It("should not inject anything if the resource is nil", func() { + injector{}.injectInto(template) + Expect(template.resource).To(BeNil()) + }) + + It("should inject if the config has a domain set", func() { + var res = &resource.Resource{ + GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }, + } + + injector{resource: res}.injectInto(template) + Expect(template.resource).To(Equal(res)) + }) + + }) + }) +}) diff --git a/pkg/model/file/interfaces.go b/pkg/machinery/interfaces.go similarity index 99% rename from pkg/model/file/interfaces.go rename to pkg/machinery/interfaces.go index 43470df4e60..73837dd8b30 100644 --- a/pkg/model/file/interfaces.go +++ b/pkg/machinery/interfaces.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "text/template" @@ -67,6 +67,12 @@ type HasRepository interface { InjectRepository(string) } +// HasProjectName allows a project name to be used on a template. +type HasProjectName interface { + // InjectProjectName sets the template project name. + InjectProjectName(string) +} + // HasMultiGroup allows the multi-group flag to be used on a template type HasMultiGroup interface { // InjectMultiGroup sets the template multi-group flag @@ -91,12 +97,6 @@ type HasResource interface { InjectResource(*resource.Resource) } -// HasProjectName allows a project name to be used on a template. -type HasProjectName interface { - // InjectProjectName sets the template project name. - InjectProjectName(string) -} - // UseCustomFuncMap allows a template to use a custom template.FuncMap instead of the default FuncMap. type UseCustomFuncMap interface { // GetFuncMap returns a custom FuncMap. diff --git a/pkg/model/file/marker.go b/pkg/machinery/marker.go similarity index 87% rename from pkg/model/file/marker.go rename to pkg/machinery/marker.go index a5b22c65502..e048615c6fa 100644 --- a/pkg/model/file/marker.go +++ b/pkg/machinery/marker.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "fmt" @@ -45,7 +45,11 @@ func NewMarkerFor(path string, value string) Marker { return Marker{comment, value} } - panic(fmt.Errorf("unknown file extension: '%s', expected '.go', '.yaml' or '.yml'", ext)) + extensions := make([]string, 0, len(commentsByExt)) + for extension := range commentsByExt { + extensions = append(extensions, fmt.Sprintf("%q", extension)) + } + panic(fmt.Errorf("unknown file extension: '%s', expected one of: %s", ext, strings.Join(extensions, ", "))) } // String implements Stringer diff --git a/pkg/machinery/marker_test.go b/pkg/machinery/marker_test.go new file mode 100644 index 00000000000..2f4468ff042 --- /dev/null +++ b/pkg/machinery/marker_test.go @@ -0,0 +1,47 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("NerMarkerFor", func() { + DescribeTable("should create valid markers for known extensions", + func(path, comment string) { Expect(NewMarkerFor(path, "").comment).To(Equal(comment)) }, + Entry("for go files", "file.go", "//"), + Entry("for yaml files", "file.yaml", "#"), + Entry("for yaml files (short version)", "file.yml", "#"), + ) + + It("should panic for unknown extensions", func() { + // testing panics require to use a function with no arguments + Expect(func() { NewMarkerFor("file.unkownext", "") }).To(Panic()) + }) +}) + +var _ = Describe("Marker", func() { + Context("String", func() { + DescribeTable("should return the right string representation", + func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, + Entry("for go files", Marker{comment: "//", value: "test"}, "//+kubebuilder:scaffold:test"), + Entry("for yaml files", Marker{comment: "#", value: "test"}, "#+kubebuilder:scaffold:test"), + ) + }) +}) diff --git a/pkg/model/file/mixins.go b/pkg/machinery/mixins.go similarity index 99% rename from pkg/model/file/mixins.go rename to pkg/machinery/mixins.go index fcf1dbbbf58..9529e687bf6 100644 --- a/pkg/model/file/mixins.go +++ b/pkg/machinery/mixins.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package file +package machinery import ( "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" @@ -64,7 +64,7 @@ type InserterMixin struct { // GetIfExistsAction implements Builder func (t *InserterMixin) GetIfExistsAction() IfExistsAction { // Inserter builders always need to overwrite previous files - return Overwrite + return OverwriteFile } // DomainMixin provides templates with a injectable domain field @@ -93,6 +93,18 @@ func (m *RepositoryMixin) InjectRepository(repository string) { } } +// ProjectNameMixin provides templates with an injectable project name field. +type ProjectNameMixin struct { + ProjectName string +} + +// InjectProjectName implements HasProjectName. +func (m *ProjectNameMixin) InjectProjectName(projectName string) { + if m.ProjectName == "" { + m.ProjectName = projectName + } +} + // MultiGroupMixin provides templates with a injectable multi-group flag field type MultiGroupMixin struct { // MultiGroup is the multi-group flag @@ -139,15 +151,3 @@ func (m *ResourceMixin) InjectResource(res *resource.Resource) { m.Resource = res } } - -// ProjectNameMixin provides templates with an injectable project name field. -type ProjectNameMixin struct { - ProjectName string -} - -// InjectProjectName implements HasProjectName. -func (m *ProjectNameMixin) InjectProjectName(projectName string) { - if m.ProjectName == "" { - m.ProjectName = projectName - } -} diff --git a/pkg/machinery/mixins_test.go b/pkg/machinery/mixins_test.go new file mode 100644 index 00000000000..944e4c0c195 --- /dev/null +++ b/pkg/machinery/mixins_test.go @@ -0,0 +1,188 @@ +/* +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 machinery + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +type mockTemplate struct { + TemplateMixin + DomainMixin + RepositoryMixin + ProjectNameMixin + MultiGroupMixin + ComponentConfigMixin + BoilerplateMixin + ResourceMixin +} + +type mockInserter struct { + // InserterMixin requires a different type because it collides with TemplateMixin + InserterMixin +} + +var _ = Describe("TemplateMixin", func() { + const ( + path = "path/to/file.go" + ifExistsAction = SkipFile + body = "content" + ) + + var tmp = mockTemplate{ + TemplateMixin: TemplateMixin{ + PathMixin: PathMixin{path}, + IfExistsActionMixin: IfExistsActionMixin{ifExistsAction}, + TemplateBody: body, + }, + } + + Context("GetPath", func() { + It("should return the path", func() { + Expect(tmp.GetPath()).To(Equal(path)) + }) + }) + + Context("GetIfExistsAction", func() { + It("should return the if-exists action", func() { + Expect(tmp.GetIfExistsAction()).To(Equal(ifExistsAction)) + }) + }) + + Context("GetBody", func() { + It("should return the body", func() { + Expect(tmp.GetBody()).To(Equal(body)) + }) + }) +}) + +var _ = Describe("InserterMixin", func() { + const path = "path/to/file.go" + + var tmp = mockInserter{ + InserterMixin: InserterMixin{ + PathMixin: PathMixin{path}, + }, + } + + Context("GetPath", func() { + It("should return the path", func() { + Expect(tmp.GetPath()).To(Equal(path)) + }) + }) + + Context("GetIfExistsAction", func() { + It("should return overwrite file always", func() { + Expect(tmp.GetIfExistsAction()).To(Equal(OverwriteFile)) + }) + }) +}) + +var _ = Describe("DomainMixin", func() { + const domain = "my.domain" + + var tmp = mockTemplate{} + + Context("InjectDomain", func() { + It("should inject the provided domain", func() { + tmp.InjectDomain(domain) + Expect(tmp.Domain).To(Equal(domain)) + }) + }) +}) + +var _ = Describe("RepositoryMixin", func() { + const repo = "test" + + var tmp = mockTemplate{} + + Context("InjectRepository", func() { + It("should inject the provided repository", func() { + tmp.InjectRepository(repo) + Expect(tmp.Repo).To(Equal(repo)) + }) + }) +}) + +var _ = Describe("ProjectNameMixin", func() { + const name = "my project" + + var tmp = mockTemplate{} + + Context("InjectProjectName", func() { + It("should inject the provided project name", func() { + tmp.InjectProjectName(name) + Expect(tmp.ProjectName).To(Equal(name)) + }) + }) +}) + +var _ = Describe("MultiGroupMixin", func() { + var tmp = mockTemplate{} + + Context("InjectMultiGroup", func() { + It("should inject the provided multi group flag", func() { + tmp.InjectMultiGroup(true) + Expect(tmp.MultiGroup).To(BeTrue()) + }) + }) +}) + +var _ = Describe("ComponentConfigMixin", func() { + var tmp = mockTemplate{} + + Context("InjectComponentConfig", func() { + It("should inject the provided component config flag", func() { + tmp.InjectComponentConfig(true) + Expect(tmp.ComponentConfig).To(BeTrue()) + }) + }) +}) + +var _ = Describe("BoilerplateMixin", func() { + const boilerplate = "Copyright" + + var tmp = mockTemplate{} + + Context("InjectBoilerplate", func() { + It("should inject the provided boilerplate", func() { + tmp.InjectBoilerplate(boilerplate) + Expect(tmp.Boilerplate).To(Equal(boilerplate)) + }) + }) +}) + +var _ = Describe("ResourceMixin", func() { + var res = &resource.Resource{GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }} + + var tmp = mockTemplate{} + + Context("InjectResource", func() { + It("should inject the provided resource", func() { + tmp.InjectResource(res) + Expect(tmp.Resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + }) + }) +}) diff --git a/pkg/machinery/scaffold.go b/pkg/machinery/scaffold.go index ba9cf93dba5..3b79104ad9b 100644 --- a/pkg/machinery/scaffold.go +++ b/pkg/machinery/scaffold.go @@ -29,8 +29,6 @@ import ( "golang.org/x/tools/imports" "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) @@ -50,9 +48,6 @@ var options = imports.Options{ // Scaffold uses templates to scaffold new files type Scaffold struct { - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin - // fs allows to mock the file system for tests fs afero.Fs @@ -60,10 +55,8 @@ type Scaffold struct { dirPerm os.FileMode filePerm os.FileMode - // fields to create the universe - config config.Config - boilerplate string - resource *resource.Resource + // injector is used to provide several fields to the templates + injector injector } // ScaffoldOption allows to provide optional arguments to the Scaffold @@ -84,13 +77,6 @@ func NewScaffold(fs Filesystem, options ...ScaffoldOption) *Scaffold { return s } -// WithPlugins sets the plugins to be used -func WithPlugins(plugins ...model.Plugin) ScaffoldOption { - return func(s *Scaffold) { - s.plugins = append(s.plugins, plugins...) - } -} - // WithDirectoryPermissions sets the permissions for new directories func WithDirectoryPermissions(dirPerm os.FileMode) ScaffoldOption { return func(s *Scaffold) { @@ -108,7 +94,7 @@ func WithFilePermissions(filePerm os.FileMode) ScaffoldOption { // WithConfig provides the project configuration to the Scaffold func WithConfig(cfg config.Config) ScaffoldOption { return func(s *Scaffold) { - s.config = cfg + s.injector.config = cfg if cfg != nil && cfg.GetRepository() != "" { imports.LocalPrefix = cfg.GetRepository() @@ -119,67 +105,50 @@ func WithConfig(cfg config.Config) ScaffoldOption { // WithBoilerplate provides the boilerplate to the Scaffold func WithBoilerplate(boilerplate string) ScaffoldOption { return func(s *Scaffold) { - s.boilerplate = boilerplate + s.injector.boilerplate = boilerplate } } // WithResource provides the resource to the Scaffold func WithResource(resource *resource.Resource) ScaffoldOption { return func(s *Scaffold) { - s.resource = resource + s.injector.resource = resource } } // Execute writes to disk the provided files -func (s *Scaffold) Execute(files ...file.Builder) error { - // Initialize the universe - universe := &model.Universe{ - Config: s.config, - Boilerplate: s.boilerplate, - Resource: s.resource, - Files: make(map[string]*file.File, len(files)), - } +func (s *Scaffold) Execute(builders ...Builder) error { + // Initialize the files + files := make(map[string]*File, len(builders)) - // Set the repo as the local prefix so that it knows how to group imports - if universe.Config != nil { - imports.LocalPrefix = universe.Config.GetRepository() - } - - for _, f := range files { + for _, builder := range builders { // Inject common fields - universe.InjectInto(f) + s.injector.injectInto(builder) // Validate file builders - if reqValFile, requiresValidation := f.(file.RequiresValidation); requiresValidation { - if err := reqValFile.Validate(); err != nil { + if reqValBuilder, requiresValidation := builder.(RequiresValidation); requiresValidation { + if err := reqValBuilder.Validate(); err != nil { return ValidateError{err} } } // Build models for Template builders - if t, isTemplate := f.(file.Template); isTemplate { - if err := s.buildFileModel(t, universe.Files); err != nil { + if t, isTemplate := builder.(Template); isTemplate { + if err := s.buildFileModel(t, files); err != nil { return err } } // Build models for Inserter builders - if i, isInserter := f.(file.Inserter); isInserter { - if err := s.updateFileModel(i, universe.Files); err != nil { + if i, isInserter := builder.(Inserter); isInserter { + if err := s.updateFileModel(i, files); err != nil { return err } } } - // Execute plugins - for _, plugin := range s.plugins { - if err := plugin.Pipe(universe); err != nil { - return PluginError{err} - } - } - // Persist the files to disk - for _, f := range universe.Files { + for _, f := range files { if err := s.writeFile(f); err != nil { return err } @@ -189,7 +158,7 @@ func (s *Scaffold) Execute(files ...file.Builder) error { } // buildFileModel scaffolds a single file -func (Scaffold) buildFileModel(t file.Template, models map[string]*file.File) error { +func (Scaffold) buildFileModel(t Template, models map[string]*File) error { // Set the template default values if err := t.SetTemplateDefaults(); err != nil { return SetTemplateDefaultsError{err} @@ -200,11 +169,11 @@ func (Scaffold) buildFileModel(t file.Template, models map[string]*file.File) er // Handle already existing models if _, found := models[path]; found { switch t.GetIfExistsAction() { - case file.Skip: + case SkipFile: return nil - case file.Error: + case Error: return ModelAlreadyExistsError{path} - case file.Overwrite: + case OverwriteFile: default: return UnknownIfExistsActionError{path, t.GetIfExistsAction()} } @@ -215,7 +184,7 @@ func (Scaffold) buildFileModel(t file.Template, models map[string]*file.File) er return err } - models[path] = &file.File{ + models[path] = &File{ Path: path, Contents: string(b), IfExistsAction: t.GetIfExistsAction(), @@ -224,13 +193,13 @@ func (Scaffold) buildFileModel(t file.Template, models map[string]*file.File) er } // doTemplate executes the template for a file using the input -func doTemplate(t file.Template) ([]byte, error) { +func doTemplate(t Template) ([]byte, error) { // Create a new template.Template using the type of the Template as the name temp := template.New(fmt.Sprintf("%T", t)) // Set the function map to be used - fm := file.DefaultFuncMap() - if templateWithFuncMap, hasCustomFuncMap := t.(file.UseCustomFuncMap); hasCustomFuncMap { + fm := DefaultFuncMap() + if templateWithFuncMap, hasCustomFuncMap := t.(UseCustomFuncMap); hasCustomFuncMap { fm = templateWithFuncMap.GetFuncMap() } temp.Funcs(fm) @@ -240,6 +209,7 @@ func doTemplate(t file.Template) ([]byte, error) { return nil, err } + // Execute the template out := &bytes.Buffer{} if err := temp.Execute(out, t); err != nil { return nil, err @@ -259,7 +229,7 @@ func doTemplate(t file.Template) ([]byte, error) { } // updateFileModel updates a single file -func (s Scaffold) updateFileModel(i file.Inserter, models map[string]*file.File) error { +func (s Scaffold) updateFileModel(i Inserter, models map[string]*File) error { m, err := s.loadPreviousModel(i, models) if err != nil { return err @@ -294,13 +264,13 @@ func (s Scaffold) updateFileModel(i file.Inserter, models map[string]*file.File) } m.Contents = string(formattedContent) - m.IfExistsAction = file.Overwrite + m.IfExistsAction = OverwriteFile models[m.Path] = m return nil } // loadPreviousModel gets the previous model from the models map or the actual file -func (s Scaffold) loadPreviousModel(i file.Inserter, models map[string]*file.File) (*file.File, error) { +func (s Scaffold) loadPreviousModel(i Inserter, models map[string]*File) (*File, error) { path := i.GetPath() // Lets see if we already have a model for this file @@ -318,17 +288,17 @@ func (s Scaffold) loadPreviousModel(i file.Inserter, models map[string]*file.Fil // If both a model and a file are found, check which has preference switch m.IfExistsAction { - case file.Skip: + case SkipFile: // File has preference fromFile, err := s.loadModelFromFile(path) if err != nil { return m, nil } return fromFile, nil - case file.Error: + case Error: // Writing will result in an error, so we can return error now return nil, FileAlreadyExistsError{path} - case file.Overwrite: + case OverwriteFile: // Model has preference return m, nil default: @@ -341,7 +311,7 @@ func (s Scaffold) loadPreviousModel(i file.Inserter, models map[string]*file.Fil } // loadModelFromFile gets the previous model from the actual file -func (s Scaffold) loadModelFromFile(path string) (f *file.File, err error) { +func (s Scaffold) loadModelFromFile(path string) (f *File, err error) { reader, err := s.fs.Open(path) if err != nil { return nil, OpenFileError{err} @@ -357,11 +327,11 @@ func (s Scaffold) loadModelFromFile(path string) (f *file.File, err error) { return nil, ReadFileError{err} } - return &file.File{Path: path, Contents: string(content)}, nil + return &File{Path: path, Contents: string(content)}, nil } // getValidCodeFragments obtains the code fragments from a file.Inserter -func getValidCodeFragments(i file.Inserter) file.CodeFragmentsMap { +func getValidCodeFragments(i Inserter) CodeFragmentsMap { // Get the code fragments codeFragments := i.GetCodeFragments() @@ -385,7 +355,7 @@ func getValidCodeFragments(i file.Inserter) file.CodeFragmentsMap { // filterExistingValues removes the single-line values that already exists // TODO: Add support for multi-line duplicate values -func filterExistingValues(content string, codeFragmentsMap file.CodeFragmentsMap) error { +func filterExistingValues(content string, codeFragmentsMap CodeFragmentsMap) error { scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := scanner.Text() @@ -406,7 +376,7 @@ func filterExistingValues(content string, codeFragmentsMap file.CodeFragmentsMap return nil } -func insertStrings(content string, codeFragmentsMap file.CodeFragmentsMap) ([]byte, error) { +func insertStrings(content string, codeFragmentsMap CodeFragmentsMap) ([]byte, error) { out := new(bytes.Buffer) scanner := bufio.NewScanner(strings.NewReader(content)) @@ -430,7 +400,7 @@ func insertStrings(content string, codeFragmentsMap file.CodeFragmentsMap) ([]by return out.Bytes(), nil } -func (s Scaffold) writeFile(f *file.File) (err error) { +func (s Scaffold) writeFile(f *File) (err error) { // Check if the file to write already exists exists, err := afero.Exists(s.fs, f.Path) if err != nil { @@ -438,12 +408,12 @@ func (s Scaffold) writeFile(f *file.File) (err error) { } if exists { switch f.IfExistsAction { - case file.Overwrite: + case OverwriteFile: // By not returning, the file is written as if it didn't exist - case file.Skip: + case SkipFile: // By returning nil, the file is not written but the process will carry on return nil - case file.Error: + case Error: // By returning an error, the file is not written and the process will fail return FileAlreadyExistsError{f.Path} } diff --git a/pkg/machinery/scaffold_test.go b/pkg/machinery/scaffold_test.go index 1f3e70ab8e8..96142e259f1 100644 --- a/pkg/machinery/scaffold_test.go +++ b/pkg/machinery/scaffold_test.go @@ -24,8 +24,6 @@ import ( "github.com/spf13/afero" cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) @@ -33,79 +31,61 @@ var _ = Describe("Scaffold", func() { Describe("NewScaffold", func() { It("should succeed for no option", func() { s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).To(BeNil()) - }) - - It("should succeed with plugins option", func() { - plugins := []model.Plugin{fakePlugin{}, fakePlugin{}} - - s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithPlugins(plugins...)) - Expect(s.plugins).To(Equal(plugins)) - Expect(s.fs).NotTo(BeNil()) - Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) - Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).To(BeNil()) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) }) It("should succeed with directory permissions option", func() { const dirPermissions os.FileMode = 0755 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithDirectoryPermissions(dirPermissions)) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(dirPermissions)) Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).To(BeNil()) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) }) It("should succeed with file permissions option", func() { const filePermissions os.FileMode = 0755 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithFilePermissions(filePermissions)) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) Expect(s.filePerm).To(Equal(filePermissions)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).To(BeNil()) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) }) It("should succeed with config option", func() { cfg := cfgv3.New() s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithConfig(cfg)) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).NotTo(BeNil()) - Expect(s.config.GetVersion().Compare(cfgv3.Version)).To(Equal(0)) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).To(BeNil()) + Expect(s.injector.config).NotTo(BeNil()) + Expect(s.injector.config.GetVersion().Compare(cfgv3.Version)).To(Equal(0)) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).To(BeNil()) }) It("should succeed with boilerplate option", func() { const boilerplate = "Copyright" s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithBoilerplate(boilerplate)) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal(boilerplate)) - Expect(s.resource).To(BeNil()) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal(boilerplate)) + Expect(s.injector.resource).To(BeNil()) }) It("should succeed with resource option", func() { @@ -117,14 +97,13 @@ var _ = Describe("Scaffold", func() { }} s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithResource(res)) - Expect(s.plugins).To(BeNil()) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) Expect(s.filePerm).To(Equal(defaultFilePermission)) - Expect(s.config).To(BeNil()) - Expect(s.boilerplate).To(Equal("")) - Expect(s.resource).NotTo(BeNil()) - Expect(s.resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) + Expect(s.injector.config).To(BeNil()) + Expect(s.injector.boilerplate).To(Equal("")) + Expect(s.injector.resource).NotTo(BeNil()) + Expect(s.injector.resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) }) }) @@ -147,7 +126,7 @@ var _ = Describe("Scaffold", func() { }) DescribeTable("successes", - func(path, expected string, files ...file.Builder) { + func(path, expected string, files ...Builder) { Expect(s.Execute(files...)).To(Succeed()) b, err := afero.ReadFile(s.fs, path) @@ -166,7 +145,7 @@ var _ = Describe("Scaffold", func() { Entry("should overwrite required models if already have one", path, content, fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, - fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: file.Overwrite}, body: content}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content}, ), Entry("should format a go file", pathGo, "package file\n", @@ -175,7 +154,7 @@ var _ = Describe("Scaffold", func() { ) DescribeTable("file builders related errors", - func(errType interface{}, files ...file.Builder) { + func(errType interface{}, files ...Builder) { err := s.Execute(files...) Expect(err).To(HaveOccurred()) Expect(errors.As(err, errType)).To(BeTrue()) @@ -191,7 +170,7 @@ var _ = Describe("Scaffold", func() { Entry("should fail if an unexpected previous model is found", &ModelAlreadyExistsError{}, fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, - fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: file.Error}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}}, ), Entry("should fail if behavior if-exists-action is not defined", &UnknownIfExistsActionError{}, @@ -202,7 +181,7 @@ var _ = Describe("Scaffold", func() { // Following errors are unwrapped, so we need to check for substrings DescribeTable("template related errors", - func(errMsg string, files ...file.Builder) { + func(errMsg string, files ...Builder) { err := s.Execute(files...) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(errMsg)) @@ -222,7 +201,7 @@ var _ = Describe("Scaffold", func() { ) DescribeTable("insert strings", - func(path, input, expected string, files ...file.Builder) { + func(path, input, expected string, files ...Builder) { Expect(afero.WriteFile(s.fs, path, []byte(input), 0666)).To(Succeed()) Expect(s.Execute(files...)).To(Succeed()) @@ -246,8 +225,8 @@ var b int `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathGo}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, }, }, ), @@ -263,8 +242,8 @@ var b int `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), @@ -276,13 +255,13 @@ var b int 2 #+kubebuilder:scaffold:- `, - fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: file.Overwrite}, body: ` + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` #+kubebuilder:scaffold:- `}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), @@ -294,13 +273,13 @@ var b int 2 #+kubebuilder:scaffold:- `, - fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: file.Overwrite}, body: ` + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` #+kubebuilder:scaffold:- `}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), @@ -317,8 +296,8 @@ var b int fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml}, body: content}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), @@ -336,10 +315,10 @@ var b int `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - markers: []file.Marker{file.NewMarkerFor(pathYaml, "-")}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, - file.NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + markers: []Marker{NewMarkerFor(pathYaml, "-")}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, }, }, ), @@ -362,9 +341,9 @@ var b int `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, - file.NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, }, }, ), @@ -378,15 +357,15 @@ var b int `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, - codeFragments: file.CodeFragmentsMap{ - file.NewMarkerFor(pathYaml, "-"): {}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {}, }, }, ), ) DescribeTable("insert strings related errors", - func(errType interface{}, files ...file.Builder) { + func(errType interface{}, files ...Builder) { Expect(afero.WriteFile(s.fs, path, []byte{}, 0666)).To(Succeed()) err := s.Execute(files...) @@ -395,7 +374,7 @@ var b int }, Entry("should fail if inserting into a model that fails when a file exists and it does exist", &FileAlreadyExistsError{}, - fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: Error}}, fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, ), Entry("should fail if inserting into a model with unknown behavior if the file exists and it does exist", @@ -405,14 +384,6 @@ var b int ), ) - It("should fail if a plugin fails", func() { - s.plugins = []model.Plugin{fakePlugin{testErr}} - - err := s.Execute(fakeTemplate{}) - Expect(err).To(HaveOccurred()) - Expect(errors.As(err, &PluginError{})).To(BeTrue()) - }) - Context("write when the file already exists", func() { BeforeEach(func() { _ = afero.WriteFile(s.fs, path, []byte{}, 0666) @@ -431,7 +402,7 @@ var b int It("should write the file if configured to do so", func() { Expect(s.Execute(fakeTemplate{ - fakeBuilder: fakeBuilder{path: path, ifExistsAction: file.Overwrite}, + fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content, })).To(Succeed()) @@ -442,7 +413,7 @@ var b int It("should error if configured to do so", func() { err := s.Execute(fakeTemplate{ - fakeBuilder: fakeBuilder{path: path, ifExistsAction: file.Error}, + fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}, body: content, }) Expect(err).To(HaveOccurred()) @@ -452,53 +423,41 @@ var b int }) }) -var _ model.Plugin = fakePlugin{} - -// fakePlugin is used to mock a model.Plugin in order to test Scaffold -type fakePlugin struct { - err error -} - -// Pipe implements model.Plugin -func (f fakePlugin) Pipe(_ *model.Universe) error { - return f.err -} - -var _ file.Builder = fakeBuilder{} +var _ Builder = fakeBuilder{} -// fakeBuilder is used to mock a file.Builder +// fakeBuilder is used to mock a Builder type fakeBuilder struct { path string - ifExistsAction file.IfExistsAction + ifExistsAction IfExistsAction } -// GetPath implements file.Builder +// GetPath implements Builder func (f fakeBuilder) GetPath() string { return f.path } -// GetIfExistsAction implements file.Builder -func (f fakeBuilder) GetIfExistsAction() file.IfExistsAction { +// GetIfExistsAction implements Builder +func (f fakeBuilder) GetIfExistsAction() IfExistsAction { return f.ifExistsAction } -var _ file.RequiresValidation = fakeRequiresValidation{} +var _ RequiresValidation = fakeRequiresValidation{} -// fakeRequiresValidation is used to mock a file.RequiresValidation in order to test Scaffold +// fakeRequiresValidation is used to mock a RequiresValidation in order to test Scaffold type fakeRequiresValidation struct { fakeBuilder validateErr error } -// Validate implements file.RequiresValidation +// Validate implements RequiresValidation func (f fakeRequiresValidation) Validate() error { return f.validateErr } -var _ file.Template = fakeTemplate{} +var _ Template = fakeTemplate{} -// fakeTemplate is used to mock a file.File in order to test Scaffold +// fakeTemplate is used to mock a File in order to test Scaffold type fakeTemplate struct { fakeBuilder @@ -506,12 +465,12 @@ type fakeTemplate struct { err error } -// GetBody implements file.Template +// GetBody implements Template func (f fakeTemplate) GetBody() string { return f.body } -// SetTemplateDefaults implements file.Template +// SetTemplateDefaults implements Template func (f fakeTemplate) SetTemplateDefaults() error { if f.err != nil { return f.err @@ -523,24 +482,24 @@ func (f fakeTemplate) SetTemplateDefaults() error { type fakeInserter struct { fakeBuilder - markers []file.Marker - codeFragments file.CodeFragmentsMap + markers []Marker + codeFragments CodeFragmentsMap } -// GetMarkers implements file.UpdatableTemplate -func (f fakeInserter) GetMarkers() []file.Marker { +// GetMarkers implements Inserter +func (f fakeInserter) GetMarkers() []Marker { if f.markers != nil { return f.markers } - markers := make([]file.Marker, 0, len(f.codeFragments)) + markers := make([]Marker, 0, len(f.codeFragments)) for marker := range f.codeFragments { markers = append(markers, marker) } return markers } -// GetCodeFragments implements file.UpdatableTemplate -func (f fakeInserter) GetCodeFragments() file.CodeFragmentsMap { +// GetCodeFragments implements Inserter +func (f fakeInserter) GetCodeFragments() CodeFragmentsMap { return f.codeFragments } diff --git a/pkg/model/resource/gvk.go b/pkg/model/resource/gvk.go index 46f1cb79ec8..c507a018ec4 100644 --- a/pkg/model/resource/gvk.go +++ b/pkg/model/resource/gvk.go @@ -26,6 +26,10 @@ import ( const ( versionPattern = "^v\\d+(?:alpha\\d+|beta\\d+)?$" + + groupRequired = "group cannot be empty if the domain is empty" + versionRequired = "version cannot be empty" + kindRequired = "kind cannot be empty" ) var ( @@ -44,17 +48,26 @@ type GVK struct { // Validate checks that the GVK is valid. func (gvk GVK) Validate() error { // Check if the qualified group has a valid DNS1123 subdomain value + if gvk.QualifiedGroup() == "" { + return fmt.Errorf(groupRequired) + } if err := validation.IsDNS1123Subdomain(gvk.QualifiedGroup()); err != nil { // NOTE: IsDNS1123Subdomain returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("either Group or Domain is invalid: %s", err) } // Check if the version follows the valid pattern + if gvk.Version == "" { + return fmt.Errorf(versionRequired) + } if !versionRegex.MatchString(gvk.Version) { return fmt.Errorf("Version must match %s (was %s)", versionPattern, gvk.Version) } // Check if kind has a valid DNS1035 label value + if gvk.Kind == "" { + return fmt.Errorf(kindRequired) + } if errors := validation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errors) != 0 { // NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("invalid Kind: %#v", errors) diff --git a/pkg/model/resource/resource_test.go b/pkg/model/resource/resource_test.go index 0d1151e8efc..fcd5364720c 100644 --- a/pkg/model/resource/resource_test.go +++ b/pkg/model/resource/resource_test.go @@ -66,6 +66,8 @@ var _ = Describe("Resource", func() { safeDomain = "testio" groupVersion = group + version domainVersion = safeDomain + version + safeGroup = "mygroup" + safeAlias = safeGroup + version ) var ( @@ -85,6 +87,22 @@ var _ = Describe("Resource", func() { Kind: kind, }, } + resHyphenGroup = Resource{ + GVK: GVK{ + Group: "my-group", + Domain: domain, + Version: version, + Kind: kind, + }, + } + resDotGroup = Resource{ + GVK: GVK{ + Group: "my.group", + Domain: domain, + Version: version, + Kind: kind, + }, + } ) DescribeTable("PackageName should return the correct string", @@ -92,6 +110,8 @@ var _ = Describe("Resource", func() { Entry("fully qualified resource", res, group), Entry("empty group name", resNoGroup, safeDomain), Entry("empty domain", resNoDomain, group), + Entry("hyphen-containing group", resHyphenGroup, safeGroup), + Entry("dot-containing group", resDotGroup, safeGroup), ) DescribeTable("ImportAlias", @@ -99,6 +119,8 @@ var _ = Describe("Resource", func() { Entry("fully qualified resource", res, groupVersion), Entry("empty group name", resNoGroup, domainVersion), Entry("empty domain", resNoDomain, groupVersion), + Entry("hyphen-containing group", resHyphenGroup, safeAlias), + Entry("dot-containing group", resDotGroup, safeAlias), ) }) @@ -284,6 +306,17 @@ var _ = Describe("Resource", func() { Expect(r.Update(other)).NotTo(Succeed()) }) + It("should work for a new path", func() { + const path = "api/v1" + r = Resource{GVK: gvk} + other = Resource{ + GVK: gvk, + Path: path, + } + Expect(r.Update(other)).To(Succeed()) + Expect(r.Path).To(Equal(path)) + }) + It("should fail for different Paths", func() { r = Resource{ GVK: gvk, diff --git a/pkg/model/universe.go b/pkg/model/universe.go deleted file mode 100644 index 79bd8dcb15b..00000000000 --- a/pkg/model/universe.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -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 model - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -// Universe describes the entire state of file generation -type Universe struct { - // Config stores the project configuration - Config config.Config `json:"config,omitempty"` - - // Boilerplate is the copyright comment added at the top of scaffolded files - Boilerplate string `json:"boilerplate,omitempty"` - - // Resource contains the information of the API that is being scaffolded - Resource *resource.Resource `json:"resource,omitempty"` - - // Files contains the model of the files that are being scaffolded - Files map[string]*file.File `json:"files,omitempty"` -} - -// InjectInto injects fields from the universe into the builder -func (u Universe) InjectInto(builder file.Builder) { - // Inject project configuration - if u.Config != nil { - if builderWithDomain, hasDomain := builder.(file.HasDomain); hasDomain { - builderWithDomain.InjectDomain(u.Config.GetDomain()) - } - if builderWithRepository, hasRepository := builder.(file.HasRepository); hasRepository { - builderWithRepository.InjectRepository(u.Config.GetRepository()) - } - if builderWithProjectName, hasProjectName := builder.(file.HasProjectName); hasProjectName { - builderWithProjectName.InjectProjectName(u.Config.GetProjectName()) - } - if builderWithMultiGroup, hasMultiGroup := builder.(file.HasMultiGroup); hasMultiGroup { - builderWithMultiGroup.InjectMultiGroup(u.Config.IsMultiGroup()) - } - if builderWithComponentConfig, hasComponentConfig := builder.(file.HasComponentConfig); hasComponentConfig { - builderWithComponentConfig.InjectComponentConfig(u.Config.IsComponentConfig()) - } - } - // Inject boilerplate - if builderWithBoilerplate, hasBoilerplate := builder.(file.HasBoilerplate); hasBoilerplate { - builderWithBoilerplate.InjectBoilerplate(u.Boilerplate) - } - // Inject resource - if u.Resource != nil { - if builderWithResource, hasResource := builder.(file.HasResource); hasResource { - builderWithResource.InjectResource(u.Resource) - } - } -} diff --git a/pkg/plugin/bundle.go b/pkg/plugin/bundle.go new file mode 100644 index 00000000000..cc341739027 --- /dev/null +++ b/pkg/plugin/bundle.go @@ -0,0 +1,67 @@ +/* +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 plugin + +import ( + "fmt" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +type bundle struct { + name string + version Version + plugins []Plugin + + supportedProjectVersions []config.Version +} + +// NewBundle creates a new Bundle with the provided name and version, and that wraps the provided plugins. +// The list of supported project versions is computed from the provided plugins. +func NewBundle(name string, version Version, plugins ...Plugin) (Bundle, error) { + supportedProjectVersions := CommonSupportedProjectVersions(plugins...) + if len(supportedProjectVersions) == 0 { + return nil, fmt.Errorf("in order to bundle plugins, they must all support at least one common project version") + } + + return bundle{ + name: name, + version: version, + plugins: plugins, + supportedProjectVersions: supportedProjectVersions, + }, nil +} + +// Name implements Plugin +func (b bundle) Name() string { + return b.name +} + +// Version implements Plugin +func (b bundle) Version() Version { + return b.version +} + +// SupportedProjectVersions implements Plugin +func (b bundle) SupportedProjectVersions() []config.Version { + return b.supportedProjectVersions +} + +// Plugins implements Bundle +func (b bundle) Plugins() []Plugin { + return b.plugins +} diff --git a/pkg/plugin/bundle_test.go b/pkg/plugin/bundle_test.go new file mode 100644 index 00000000000..4592b66680d --- /dev/null +++ b/pkg/plugin/bundle_test.go @@ -0,0 +1,101 @@ +/* +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 plugin + +import ( + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +var _ = Describe("Bundle", func() { + const ( + name = "bundle.kubebuilder.io" + ) + + var ( + version = Version{Number: 1} + + p1 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3}, + }} + p2 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 3, Stage: stage.Alpha}, + }} + p3 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3, Stage: stage.Beta}, + }} + p4 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 2}, + {Number: 3}, + }} + ) + + Context("NewBundle", func() { + It("should succeed for plugins with common supported project versions", func() { + for _, plugins := range [][]Plugin{ + {p1, p2}, + {p1, p3}, + {p1, p4}, + {p2, p3}, + {p3, p4}, + + {p1, p2, p3}, + {p1, p3, p4}, + } { + b, err := NewBundle(name, version, plugins...) + Expect(err).NotTo(HaveOccurred()) + Expect(b.Name()).To(Equal(name)) + Expect(b.Version().Compare(version)).To(Equal(0)) + versions := b.SupportedProjectVersions() + sort.Slice(versions, func(i int, j int) bool { + return versions[i].Compare(versions[j]) == -1 + }) + expectedVersions := CommonSupportedProjectVersions(plugins...) + sort.Slice(expectedVersions, func(i int, j int) bool { + return expectedVersions[i].Compare(expectedVersions[j]) == -1 + }) + Expect(versions).To(Equal(expectedVersions)) + Expect(b.Plugins()).To(Equal(plugins)) + } + }) + + It("should fail for plugins with no common supported project version", func() { + for _, plugins := range [][]Plugin{ + {p2, p4}, + + {p1, p2, p4}, + {p2, p3, p4}, + + {p1, p2, p3, p4}, + } { + _, err := NewBundle(name, version, plugins...) + Expect(err).To(HaveOccurred()) + } + }) + }) +}) diff --git a/pkg/model/plugin.go b/pkg/plugin/errors.go similarity index 58% rename from pkg/model/plugin.go rename to pkg/plugin/errors.go index 06f8854933e..4297ddca6ab 100644 --- a/pkg/model/plugin.go +++ b/pkg/plugin/errors.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +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. @@ -14,11 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package model +package plugin -// Plugin is the interface that a plugin must implement -// We will (later) have an ExecPlugin that implements this by exec-ing a binary -type Plugin interface { - // Pipe is the core plugin interface, that transforms a UniverseModel - Pipe(*Universe) error +import ( + "fmt" +) + +// ExitError is a typed error that is returned by a plugin when no further steps should be executed for itself. +type ExitError struct { + Plugin string + Reason string +} + +// Error implements error +func (e ExitError) Error() string { + return fmt.Sprintf("plugin %q exit early: %s", e.Plugin, e.Reason) } diff --git a/plugins/addon/channel.go b/pkg/plugin/errors_test.go similarity index 51% rename from plugins/addon/channel.go rename to pkg/plugin/errors_test.go index f4804e5d528..5d295b8212c 100644 --- a/plugins/addon/channel.go +++ b/pkg/plugin/errors_test.go @@ -1,5 +1,5 @@ /* -Copyright 2019 The Kubernetes Authors. +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. @@ -14,28 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -package addon +package plugin import ( - "path/filepath" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) -const exampleChannel = `# Versions for the stable channel -manifests: -- version: 0.0.1 -` - -// ExampleChannel adds a model file for the channel -func ExampleChannel(u *model.Universe) error { - m := &file.File{ - Path: filepath.Join("channels", "stable"), - Contents: exampleChannel, - IfExistsAction: file.Skip, +var _ = Describe("PluginKeyNotFoundError", func() { + var err = ExitError{ + Plugin: "go.kubebuilder.io/v1", + Reason: "skipping plugin", } - _, err := AddFile(u, m) - return err -} + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal("plugin \"go.kubebuilder.io/v1\" exit early: skipping plugin")) + }) + }) +}) diff --git a/pkg/plugin/filter.go b/pkg/plugin/filter.go new file mode 100644 index 00000000000..9a690263342 --- /dev/null +++ b/pkg/plugin/filter.go @@ -0,0 +1,61 @@ +/* +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 plugin + +import ( + "strings" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +// FilterPluginsByKey returns the set of plugins that match the provided key (may be not-fully qualified) +func FilterPluginsByKey(plugins []Plugin, key string) ([]Plugin, error) { + name, ver := SplitKey(key) + hasVersion := ver != "" + var version Version + if hasVersion { + if err := version.Parse(ver); err != nil { + return nil, err + } + } + + filtered := make([]Plugin, 0, len(plugins)) + for _, plugin := range plugins { + if !strings.HasPrefix(plugin.Name(), name) { + continue + } + if hasVersion && plugin.Version().Compare(version) != 0 { + continue + } + filtered = append(filtered, plugin) + } + return filtered, nil +} + +// FilterPluginsByProjectVersion returns the set of plugins that support the provided project version +func FilterPluginsByProjectVersion(plugins []Plugin, projectVersion config.Version) []Plugin { + filtered := make([]Plugin, 0, len(plugins)) + for _, plugin := range plugins { + for _, supportedVersion := range plugin.SupportedProjectVersions() { + if supportedVersion.Compare(projectVersion) == 0 { + filtered = append(filtered, plugin) + break + } + } + } + return filtered +} diff --git a/pkg/plugin/filter_test.go b/pkg/plugin/filter_test.go new file mode 100644 index 00000000000..2b45dcb6fed --- /dev/null +++ b/pkg/plugin/filter_test.go @@ -0,0 +1,84 @@ +/* +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 plugin + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +var ( + p1 = mockPlugin{ + name: "go.kubebuilder.io", + version: Version{Number: 2}, + supportedProjectVersions: []config.Version{{Number: 2}, {Number: 3}}, + } + p2 = mockPlugin{ + name: "go.kubebuilder.io", + version: Version{Number: 3}, + supportedProjectVersions: []config.Version{{Number: 3}}, + } + p3 = mockPlugin{ + name: "example.kubebuilder.io", + version: Version{Number: 1}, + supportedProjectVersions: []config.Version{{Number: 2}}, + } + p4 = mockPlugin{ + name: "test.kubebuilder.io", + version: Version{Number: 1}, + supportedProjectVersions: []config.Version{{Number: 3}}, + } + p5 = mockPlugin{ + name: "go.test.domain", + version: Version{Number: 2}, + supportedProjectVersions: []config.Version{{Number: 2}}, + } + + allPlugins = []Plugin{p1, p2, p3, p4, p5} +) + +var _ = Describe("FilterPluginsByKey", func() { + DescribeTable("should filter", + func(key string, plugins []Plugin) { + filtered, err := FilterPluginsByKey(allPlugins, key) + Expect(err).NotTo(HaveOccurred()) + Expect(filtered).To(Equal(plugins)) + }, + Entry("go plugins", "go", []Plugin{p1, p2, p5}), + Entry("go plugins (kubebuilder domain)", "go.kubebuilder", []Plugin{p1, p2}), + Entry("go v2 plugins", "go/v2", []Plugin{p1, p5}), + Entry("go v2 plugins (kubebuilder domain)", "go.kubebuilder/v2", []Plugin{p1}), + ) + + It("should fail for invalid versions", func() { + _, err := FilterPluginsByKey(allPlugins, "go/a") + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("FilterPluginsByKey", func() { + DescribeTable("should filter", + func(projectVersion config.Version, plugins []Plugin) { + Expect(FilterPluginsByProjectVersion(allPlugins, projectVersion)).To(Equal(plugins)) + }, + Entry("project v2 plugins", config.Version{Number: 2}, []Plugin{p1, p3, p5}), + Entry("project v3 plugins", config.Version{Number: 3}, []Plugin{p1, p2, p4}), + ) +}) diff --git a/pkg/plugin/helpers.go b/pkg/plugin/helpers.go index 3a2d43f4e4c..d2edfaa6db9 100644 --- a/pkg/plugin/helpers.go +++ b/pkg/plugin/helpers.go @@ -19,23 +19,16 @@ package plugin import ( "fmt" "path" + "sort" "strings" "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/internal/validation" ) -// Key returns a unique identifying string for a plugin's name and version. -func Key(name, version string) string { - if version == "" { - return name - } - return path.Join(name, "v"+strings.TrimLeft(version, "v")) -} - // KeyFor returns a Plugin's unique identifying string. func KeyFor(p Plugin) string { - return Key(p.Name(), p.Version().String()) + return path.Join(p.Name(), p.Version().String()) } // SplitKey returns a name and version for a plugin key. @@ -49,6 +42,7 @@ func SplitKey(key string) (string, string) { // GetShortName returns plugin's short name (name before domain) if name // is fully qualified (has a domain suffix), otherwise GetShortName returns name. +// Deprecated func GetShortName(name string) string { return strings.SplitN(name, ".", 2)[0] } @@ -96,7 +90,7 @@ func validateName(name string) error { return nil } -// SupportsVersion checks if a plugins supports a project version. +// SupportsVersion checks if a plugin supports a project version. func SupportsVersion(p Plugin, projectVersion config.Version) bool { for _, version := range p.SupportedProjectVersions() { if projectVersion.Compare(version) == 0 { @@ -105,3 +99,34 @@ func SupportsVersion(p Plugin, projectVersion config.Version) bool { } return false } + +// CommonSupportedProjectVersions returns the projects versions that are supported by all the provided Plugins +func CommonSupportedProjectVersions(plugins ...Plugin) []config.Version { + // Count how many times each supported project version appears + supportedProjectVersionCounter := make(map[config.Version]int) + for _, plugin := range plugins { + for _, supportedProjectVersion := range plugin.SupportedProjectVersions() { + if _, exists := supportedProjectVersionCounter[supportedProjectVersion]; !exists { + supportedProjectVersionCounter[supportedProjectVersion] = 1 + } else { + supportedProjectVersionCounter[supportedProjectVersion]++ + } + } + } + + // Check which versions are present the expected number of times + supportedProjectVersions := make([]config.Version, 0, len(supportedProjectVersionCounter)) + expectedTimes := len(plugins) + for supportedProjectVersion, times := range supportedProjectVersionCounter { + if times == expectedTimes { + supportedProjectVersions = append(supportedProjectVersions, supportedProjectVersion) + } + } + + // Sort the output to guarantee consistency + sort.Slice(supportedProjectVersions, func(i int, j int) bool { + return supportedProjectVersions[i].Compare(supportedProjectVersions[j]) == -1 + }) + + return supportedProjectVersions +} diff --git a/pkg/plugin/helpers_test.go b/pkg/plugin/helpers_test.go new file mode 100644 index 00000000000..60e311d335a --- /dev/null +++ b/pkg/plugin/helpers_test.go @@ -0,0 +1,190 @@ +/* +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 plugin + +import ( + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" +) + +const ( + short = "go" + name = "go.kubebuilder.io" + key = "go.kubebuilder.io/v1" +) + +var ( + version = Version{Number: 1} + supportedProjectVersions = []config.Version{ + {Number: 2}, + {Number: 3}, + } +) + +var _ = Describe("KeyFor", func() { + It("should join plugins name and version", func() { + plugin := mockPlugin{ + name: name, + version: version, + } + Expect(KeyFor(plugin)).To(Equal(key)) + }) +}) + +var _ = Describe("SplitKey", func() { + It("should split keys with versions", func() { + n, v := SplitKey(key) + Expect(n).To(Equal(name)) + Expect(v).To(Equal(version.String())) + }) + + It("should split keys without versions", func() { + n, v := SplitKey(name) + Expect(n).To(Equal(name)) + Expect(v).To(Equal("")) + }) +}) + +var _ = Describe("GetShortName", func() { + It("should extract base names from domains", func() { + Expect(GetShortName(name)).To(Equal(short)) + }) +}) + +var _ = Describe("Validate", func() { + It("should succeed for valid plugins", func() { + plugin := mockPlugin{ + name: name, + version: version, + supportedProjectVersions: supportedProjectVersions, + } + Expect(Validate(plugin)).To(Succeed()) + }) + + DescribeTable("should fail", + func(plugin Plugin) { + Expect(Validate(plugin)).NotTo(Succeed()) + }, + Entry("for invalid plugin names", mockPlugin{ + name: "go_kubebuilder.io", + version: version, + supportedProjectVersions: supportedProjectVersions, + }), + Entry("for invalid plugin versions", mockPlugin{ + name: name, + version: Version{Number: -1}, + supportedProjectVersions: supportedProjectVersions, + }), + Entry("for no supported project version", mockPlugin{ + name: name, + version: version, + supportedProjectVersions: nil, + }), + Entry("for invalid supported project version", mockPlugin{ + name: name, + version: version, + supportedProjectVersions: []config.Version{{Number: -1}}, + }), + ) +}) + +var _ = Describe("ValidateKey", func() { + It("should succeed for valid keys", func() { + Expect(ValidateKey(key)).To(Succeed()) + }) + + DescribeTable("should fail", + func(key string) { + Expect(ValidateKey(key)).NotTo(Succeed()) + }, + Entry("for invalid plugin names", "go_kubebuilder.io/v1"), + Entry("for invalid versions", "go.kubebuilder.io/a"), + ) +}) + +var _ = Describe("SupportsVersion", func() { + plugin := mockPlugin{ + supportedProjectVersions: supportedProjectVersions, + } + + It("should return true for supported versions", func() { + Expect(SupportsVersion(plugin, config.Version{Number: 2})).To(BeTrue()) + Expect(SupportsVersion(plugin, config.Version{Number: 3})).To(BeTrue()) + }) + + It("should return false for non-supported versions", func() { + Expect(SupportsVersion(plugin, config.Version{Number: 1})).To(BeFalse()) + Expect(SupportsVersion(plugin, config.Version{Number: 3, Stage: stage.Alpha})).To(BeFalse()) + }) +}) + +var _ = Describe("CommonSupportedProjectVersions", func() { + It("should return the common version", func() { + var ( + p1 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3}, + }} + p2 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2, Stage: stage.Beta}, + {Number: 3, Stage: stage.Alpha}, + }} + p3 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 1}, + {Number: 2}, + {Number: 3, Stage: stage.Beta}, + }} + p4 = mockPlugin{supportedProjectVersions: []config.Version{ + {Number: 2}, + {Number: 3}, + }} + ) + + for _, tc := range []struct { + plugins []Plugin + versions []config.Version + }{ + {plugins: []Plugin{p1, p2}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p1, p3}, versions: []config.Version{{Number: 1}, {Number: 2}}}, + {plugins: []Plugin{p1, p4}, versions: []config.Version{{Number: 2}, {Number: 3}}}, + {plugins: []Plugin{p2, p3}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p2, p4}, versions: []config.Version{}}, + {plugins: []Plugin{p3, p4}, versions: []config.Version{{Number: 2}}}, + + {plugins: []Plugin{p1, p2, p3}, versions: []config.Version{{Number: 1}}}, + {plugins: []Plugin{p1, p2, p4}, versions: []config.Version{}}, + {plugins: []Plugin{p1, p3, p4}, versions: []config.Version{{Number: 2}}}, + {plugins: []Plugin{p2, p3, p4}, versions: []config.Version{}}, + + {plugins: []Plugin{p1, p2, p3, p4}, versions: []config.Version{}}, + } { + versions := CommonSupportedProjectVersions(tc.plugins...) + sort.Slice(versions, func(i int, j int) bool { + return versions[i].Compare(versions[j]) == -1 + }) + Expect(versions).To(Equal(tc.versions)) + } + }) +}) diff --git a/pkg/plugin/metadata.go b/pkg/plugin/metadata.go new file mode 100644 index 00000000000..5a83d8bdf2c --- /dev/null +++ b/pkg/plugin/metadata.go @@ -0,0 +1,31 @@ +/* +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 plugin + +// CLIMetadata is the runtime meta-data of the CLI +type CLIMetadata struct { + // CommandName is the root command name. + CommandName string +} + +// SubcommandMetadata is the runtime meta-data for a subcommand +type SubcommandMetadata struct { + // Description is a description of what this command does. It is used to display help. + Description string + // Examples are one or more examples of the command-line usage of this command. It is used to display help. + Examples string +} diff --git a/pkg/plugin/interfaces.go b/pkg/plugin/plugin.go similarity index 58% rename from pkg/plugin/interfaces.go rename to pkg/plugin/plugin.go index 8b8e339d5b9..131cd555aa4 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/plugin.go @@ -17,10 +17,7 @@ limitations under the License. package plugin import ( - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) // Plugin is an interface that defines the common base for all plugins @@ -44,33 +41,6 @@ type Deprecated interface { DeprecationWarning() string } -// Subcommand is an interface that defines the common base for subcommands returned by plugins -type Subcommand interface { - // UpdateContext updates a Context with subcommand-specific help text, like description and examples. It also serves - // to pass context from the CLI to the subcommand, such as the command name. - // Can be a no-op if default help text is desired. - UpdateContext(*Context) - // BindFlags binds the subcommand's flags to the CLI. This allows each subcommand to define its own - // command line flags. - // NOTE(Adirio): by the time we bind flags, the config hasn't been injected, trying to use it panics - BindFlags(*pflag.FlagSet) - // InjectConfig passes a config to a plugin. The plugin may modify the config. - // Initializing, loading, and saving the config is managed by the cli package. - InjectConfig(config.Config) - // Run runs the subcommand. - Run(fs machinery.Filesystem) error -} - -// Context is the runtime context for a subcommand. -type Context struct { - // CommandName sets the command name for a subcommand. - CommandName string - // Description is a description of what this subcommand does. It is used to display help. - Description string - // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. - Examples string -} - // Init is an interface for plugins that provide an `init` subcommand type Init interface { Plugin @@ -78,11 +48,6 @@ type Init interface { GetInitSubcommand() InitSubcommand } -// InitSubcommand is an interface that represents an `init` subcommand -type InitSubcommand interface { - Subcommand -} - // CreateAPI is an interface for plugins that provide a `create api` subcommand type CreateAPI interface { Plugin @@ -90,11 +55,6 @@ type CreateAPI interface { GetCreateAPISubcommand() CreateAPISubcommand } -// CreateAPISubcommand is an interface that represents a `create api` subcommand -type CreateAPISubcommand interface { - Subcommand -} - // CreateWebhook is an interface for plugins that provide a `create webhook` subcommand type CreateWebhook interface { Plugin @@ -102,11 +62,6 @@ type CreateWebhook interface { GetCreateWebhookSubcommand() CreateWebhookSubcommand } -// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand -type CreateWebhookSubcommand interface { - Subcommand -} - // Edit is an interface for plugins that provide a `edit` subcommand type Edit interface { Plugin @@ -114,11 +69,6 @@ type Edit interface { GetEditSubcommand() EditSubcommand } -// EditSubcommand is an interface that represents an `edit` subcommand -type EditSubcommand interface { - Subcommand -} - // Full is an interface for plugins that provide `init`, `create api`, `create webhook` and `edit` subcommands type Full interface { Init @@ -126,3 +76,10 @@ type Full interface { CreateWebhook Edit } + +// Bundle allows to group plugins under a single key +type Bundle interface { + Plugin + // Plugins returns a list of the bundled plugins + Plugins() []Plugin +} diff --git a/pkg/plugin/subcommand.go b/pkg/plugin/subcommand.go new file mode 100644 index 00000000000..30985f3dd85 --- /dev/null +++ b/pkg/plugin/subcommand.go @@ -0,0 +1,94 @@ +/* +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 plugin + +import ( + "github.com/spf13/pflag" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +// UpdatesMetadata is an interface that implements the optional metadata update method. +type UpdatesMetadata interface { + // UpdateMetadata updates the subcommand metadata. + UpdateMetadata(CLIMetadata, *SubcommandMetadata) +} + +// HasFlags is an interface that implements the optional bind flags method. +type HasFlags interface { + // BindFlags binds flags to the CLI subcommand. + BindFlags(*pflag.FlagSet) +} + +// RequiresConfig is an interface that implements the optional inject config method. +type RequiresConfig interface { + // InjectConfig injects the configuration to a subcommand. + InjectConfig(config.Config) error +} + +// RequiresResource is an interface that implements the required inject resource method. +type RequiresResource interface { + // InjectResource injects the resource model to a subcommand. + InjectResource(*resource.Resource) error +} + +// HasPreScaffold is an interface that implements the optional pre-scaffold method. +type HasPreScaffold interface { + // PreScaffold executes tasks before the main scaffolding. + PreScaffold(machinery.Filesystem) error +} + +// Scaffolder is an interface that implements the required scaffold method. +type Scaffolder interface { + // Scaffold implements the main scaffolding. + Scaffold(machinery.Filesystem) error +} + +// HasPostScaffold is an interface that implements the optional post-scaffold method. +type HasPostScaffold interface { + // PostScaffold executes tasks after the main scaffolding. + PostScaffold() error +} + +// Subcommand is a base interface for all subcommands. +type Subcommand interface { + Scaffolder +} + +// InitSubcommand is an interface that represents an `init` subcommand. +type InitSubcommand interface { + Subcommand +} + +// CreateAPISubcommand is an interface that represents a `create api` subcommand. +type CreateAPISubcommand interface { + Subcommand + RequiresResource +} + +// CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand. +type CreateWebhookSubcommand interface { + Subcommand + RequiresResource +} + +// EditSubcommand is an interface that represents an `edit` subcommand. +type EditSubcommand interface { + Subcommand +} diff --git a/pkg/plugin/suite_test.go b/pkg/plugin/suite_test.go new file mode 100644 index 00000000000..059ac751444 --- /dev/null +++ b/pkg/plugin/suite_test.go @@ -0,0 +1,41 @@ +/* +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 plugin + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Suite") +} + +type mockPlugin struct { + name string + version Version + supportedProjectVersions []config.Version +} + +func (p mockPlugin) Name() string { return p.name } +func (p mockPlugin) Version() Version { return p.version } +func (p mockPlugin) SupportedProjectVersions() []config.Version { return p.supportedProjectVersions } diff --git a/pkg/plugin/version_test.go b/pkg/plugin/version_test.go index f1512dbf7d8..610676906aa 100644 --- a/pkg/plugin/version_test.go +++ b/pkg/plugin/version_test.go @@ -18,22 +18,16 @@ package plugin import ( "sort" - "testing" - g "github.com/onsi/ginkgo" // An alias is required because Context is defined elsewhere in this package. + . "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo/extensions/table" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v3/pkg/model/stage" ) -func TestPlugin(t *testing.T) { - RegisterFailHandler(g.Fail) - g.RunSpecs(t, "Plugin Suite") -} - -var _ = g.Describe("Version", func() { - g.Context("Parse", func() { +var _ = Describe("Version", func() { + Context("Parse", func() { DescribeTable("should be correctly parsed for valid version strings", func(str string, number int, s stage.Stage) { var v Version @@ -72,7 +66,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("String", func() { + Context("String", func() { DescribeTable("should return the correct string value", func(version Version, str string) { Expect(version.String()).To(Equal(str)) }, Entry("for version 0", Version{Number: 0}, "v0"), @@ -94,7 +88,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Validate", func() { + Context("Validate", func() { DescribeTable("should validate valid versions", func(version Version) { Expect(version.Validate()).To(Succeed()) }, Entry("for version 0", Version{Number: 0}), @@ -125,7 +119,7 @@ var _ = g.Describe("Version", func() { ) }) - g.Context("Compare", func() { + Context("Compare", func() { // Test Compare() by sorting a list. var ( versions = []Version{ @@ -155,7 +149,7 @@ var _ = g.Describe("Version", func() { } ) - g.It("sorts a valid list of versions correctly", func() { + It("sorts a valid list of versions correctly", func() { sort.Slice(versions, func(i int, j int) bool { return versions[i].Compare(versions[j]) == -1 }) @@ -164,7 +158,7 @@ var _ = g.Describe("Version", func() { }) - g.Context("IsStable", func() { + Context("IsStable", func() { DescribeTable("should return true for stable versions", func(version Version) { Expect(version.IsStable()).To(BeTrue()) }, Entry("for version 1", Version{Number: 1}), @@ -189,4 +183,5 @@ var _ = g.Describe("Version", func() { Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) }) + }) diff --git a/pkg/plugins/declarative/v1/api.go b/pkg/plugins/declarative/v1/api.go new file mode 100644 index 00000000000..48e9727ae74 --- /dev/null +++ b/pkg/plugins/declarative/v1/api.go @@ -0,0 +1,150 @@ +/* +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 ( + "errors" + "fmt" + "path/filepath" + + "github.com/spf13/afero" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins/declarative/v1/internal/templates" + goPluginV3 "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3" +) + +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) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = ` +Scaffold a Kubernetes API by writing a Resource definition and a Controller. + +After the scaffold is written, the dependencies will be updated and +make generate will be run. +` + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + %[1]s create api --group ship --version v1beta1 --kind Frigate --resource --controller + + # Edit the API Scheme + nano api/v1beta1/frigate_types.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 +`, cliMeta.CommandName) +} + +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 machinery.Filesystem) error { + fmt.Println("updating scaffold with declarative pattern...") + + // Load the boilerplate + bp, err := afero.ReadFile(fs.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) + + // Initialize the machinery.Scaffold that will write the files to disk + scaffold := machinery.NewScaffold(fs, + machinery.WithConfig(p.config), + machinery.WithBoilerplate(boilerplate), + machinery.WithResource(p.resource), + ) + + if err := scaffold.Execute( + &templates.Types{}, + &templates.Controller{}, + &templates.Channel{ManifestVersion: exampleManifestVersion}, + &templates.Manifest{ManifestVersion: exampleManifestVersion}, + ); err != nil { + return fmt.Errorf("error updating scaffold: %w", err) + } + + // Track the resources following a declarative approach + cfg := pluginConfig{} + if err := p.config.DecodePluginConfig(pluginKey, &cfg); errors.As(err, &config.UnsupportedFieldError{}) { + // Config doesn't support per-plugin configuration, so we can't track them + } else { + // Fail unless they key wasn't found, which just means it is the first resource tracked + if err != nil && !errors.As(err, &config.PluginKeyNotFoundError{}) { + return err + } + + cfg.Resources = append(cfg.Resources, p.resource.GVK) + if err := p.config.EncodePluginConfig(pluginKey, cfg); err != nil { + return err + } + } + + // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version + kbDeclarativePattern := kbDeclarativePatternForV2 + for _, pluginKey := range p.config.GetPluginChain() { + if pluginKey == 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..9c72df227a7 --- /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/machinery" +) + +var _ machinery.Template = &Channel{} + +// Channel scaffolds the file for the channel +type Channel struct { + machinery.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 = machinery.SkipFile + + return nil +} + +const channelTemplate = `# Versions for the stable channel +manifests: +- version: {{ .ManifestVersion }} +` diff --git a/plugins/addon/controller.go b/pkg/plugins/declarative/v1/internal/templates/controller.go similarity index 66% rename from plugins/addon/controller.go rename to pkg/plugins/declarative/v1/internal/templates/controller.go index d84615feafb..31e4f11bbfa 100644 --- a/plugins/addon/controller.go +++ b/pkg/plugins/declarative/v1/internal/templates/controller.go @@ -1,30 +1,52 @@ -package addon +/* +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" - "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -// ReplaceController replaces the controller with a modified version -func ReplaceController(u *model.Universe) error { - templateBody := controllerTemplate +var _ machinery.Template = &Controller{} - funcs := DefaultTemplateFunctions() - contents, err := RunTemplate("controller", templateBody, u, funcs) - if err != nil { - return err - } +// Controller scaffolds the file that defines the controller for a CRD or a builtin resource +// nolint:maligned +type Controller struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin +} - m := &file.File{ - Path: filepath.Join("controllers", strings.ToLower(u.Resource.Kind)+"_controller.go"), - Contents: contents, - IfExistsAction: file.Error, +// 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 - ReplaceFileIfExists(u, m) + f.IfExistsAction = machinery.OverwriteFile 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..252af3bf888 --- /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/machinery" +) + +var _ machinery.Template = &Manifest{} + +// Manifest scaffolds the file that acts as a placeholder for the manifest +type Manifest struct { + machinery.TemplateMixin + machinery.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 = machinery.SkipFile + + return nil +} + +const manifestTemplate = `# Placeholder manifest - replace with the manifest for your addon +` diff --git a/plugins/addon/type.go b/pkg/plugins/declarative/v1/internal/templates/types.go similarity index 63% rename from plugins/addon/type.go rename to pkg/plugins/declarative/v1/internal/templates/types.go index ebcbeab56b7..3a0c2b946c7 100644 --- a/plugins/addon/type.go +++ b/pkg/plugins/declarative/v1/internal/templates/types.go @@ -1,50 +1,67 @@ -package addon +/* +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" - "strings" + "text/template" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -// ReplaceTypes replaces the API types with a modified version -func ReplaceTypes(u *model.Universe) error { - funcs := DefaultTemplateFunctions() - funcs["JSONTag"] = JSONTag +var _ machinery.Template = &Types{} - contents, err := RunTemplate("types", typesTemplate, u, funcs) - if err != nil { - return err - } +// Types scaffolds the file that defines the schema for a CRD +// nolint:maligned +type Types struct { + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin +} - var path string - if u.Config.IsMultiGroup() { - path = filepath.Join("apis", u.Resource.Version, strings.ToLower(u.Resource.Kind)+"_types.go") - } else { - path = filepath.Join("api", u.Resource.Version, strings.ToLower(u.Resource.Kind)+"_types.go") +// 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) - m := &file.File{ - Path: path, - Contents: contents, - IfExistsAction: file.Error, - } + f.TemplateBody = typesTemplate - ReplaceFileIfExists(u, m) + f.IfExistsAction = machinery.OverwriteFile return nil } -// JSONTag is a helper to build the json tag for a struct -// It works around escaping problems for the json tag syntax -func JSONTag(tag string) string { - return fmt.Sprintf("`json:\"%s\"`", tag) +// GetFuncMap implements file.UseCustomFuncMap +func (f Types) GetFuncMap() template.FuncMap { + funcMap := machinery.DefaultFuncMap() + funcMap["JSONTag"] = func(tag string) string { + return fmt.Sprintf("`json:%q`", tag) + } + return funcMap } -// Resource.Resource - const typesTemplate = `{{ .Boilerplate }} package {{ .Resource.Version }} diff --git a/pkg/plugins/declarative/v1/plugin.go b/pkg/plugins/declarative/v1/plugin.go new file mode 100644 index 00000000000..fdd291aca80 --- /dev/null +++ b/pkg/plugins/declarative/v1/plugin.go @@ -0,0 +1,57 @@ +/* +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/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" +) + +const pluginName = "declarative" + plugins.DefaultNameQualifier + +var ( + pluginVersion = plugin.Version{Number: 1} + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} + pluginKey = plugin.KeyFor(Plugin{}) +) + +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 } + +type pluginConfig struct { + Resources []resource.GVK `json:"resources,omitempty"` +} diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index 892217da7ca..230dc1c3383 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -17,23 +17,13 @@ limitations under the License. package golang import ( - "fmt" "path" - "strings" - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" ) -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty if the domain is empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - var ( coreGroups = map[string]string{ "admission": "k8s.io", @@ -64,17 +54,7 @@ var ( // Options contains the information required to build a new resource.Resource. type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - // Plural is the resource's kind plural form. - // Optional Plural string // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. @@ -93,82 +73,36 @@ type Options struct { DoConversion bool } -// Validate verifies that all the fields have valid values -func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 && len(opts.Domain) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - return nil -} - -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - -// NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Controller: opts.DoController, - } - +// UpdateResource updates the provided resource with the options +func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { if opts.Plural != "" { res.Plural = opts.Plural - } else { - // If not provided, compute a plural for Kind - res.Plural = resource.RegularPlural(opts.Kind) } if opts.DoAPI { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) + res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup()) res.API = &resource.API{ CRDVersion: opts.CRDVersion, Namespaced: opts.Namespaced, } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} + } + + if opts.DoController { + res.Controller = true } if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, + res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup()) + res.Webhooks.WebhookVersion = opts.WebhookVersion + if opts.DoDefaulting { + res.Webhooks.Defaulting = true + } + if opts.DoValidation { + res.Webhooks.Validation = true + } + if opts.DoConversion { + res.Webhooks.Conversion = true } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} } // domain and path may need to be changed in case we are referring to a builtin core resource: @@ -178,15 +112,18 @@ func (opts Options) NewResource(c newconfig.Config) resource.Resource { // - In any other case, default to => project resource // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath if !opts.DoAPI { - loadedRes, err := c.GetResource(opts.GVK()) - alreadyHasAPI := err == nil && loadedRes.HasAPI() + var alreadyHasAPI bool + if c.GetVersion().Compare(cfgv2.Version) == 0 { + alreadyHasAPI = c.HasResource(res.GVK) + } else { + loadedRes, err := c.GetResource(res.GVK) + alreadyHasAPI = err == nil && loadedRes.HasAPI() + } if !alreadyHasAPI { - if domain, found := coreGroups[opts.Group]; found { + if domain, found := coreGroups[res.Group]; found { res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) + res.Path = path.Join("k8s.io", "api", res.Group, res.Version) } } } - - return res } diff --git a/pkg/plugins/golang/options_test.go b/pkg/plugins/golang/options_test.go index 8faa101d975..7df8d828907 100644 --- a/pkg/plugins/golang/options_test.go +++ b/pkg/plugins/golang/options_test.go @@ -24,41 +24,37 @@ import ( . "github.com/onsi/gomega" "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/model/resource" ) var _ = Describe("Options", func() { - Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), + Context("UpdateResource", func() { + const ( + group = "crew" + domain = "test.io" + version = "v1" + kind = "FirstMate" ) + var ( + gvk = resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + } - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group and domain", Options{Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), + cfg config.Config ) - }) - - Context("NewResource", func() { - var cfg config.Config BeforeEach(func() { cfg = cfgv3.New() _ = cfg.SetRepository("test") }) - DescribeTable("should succeed if the Resource is valid", + DescribeTable("should succeed", func(options Options) { - Expect(options.Validate()).To(Succeed()) - for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -66,86 +62,65 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.API).NotTo(BeNil()) + res := resource.Resource{ + GVK: gvk, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, + } + + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + Expect(res.GVK.IsEqualTo(gvk)).To(BeTrue()) + if options.Plural != "" { + Expect(res.Plural).To(Equal(options.Plural)) + } if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion { if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) + Expect(res.Path).To(Equal( + path.Join(cfg.GetRepository(), "apis", gvk.Group, gvk.Version))) } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + Expect(res.Path).To(Equal(path.Join(cfg.GetRepository(), "api", gvk.Version))) } } else { // Core-resources have a path despite not having an API/Webhook but they are not tested here - Expect(resource.Path).To(Equal("")) + Expect(res.Path).To(Equal("")) } + Expect(res.API).NotTo(BeNil()) if options.DoAPI { - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.API.IsEmpty()).To(BeFalse()) + Expect(res.API.CRDVersion).To(Equal(options.CRDVersion)) + Expect(res.API.Namespaced).To(Equal(options.Namespaced)) + Expect(res.API.IsEmpty()).To(BeFalse()) } else { - Expect(resource.API.IsEmpty()).To(BeTrue()) + Expect(res.API.IsEmpty()).To(BeTrue()) } - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks).NotTo(BeNil()) + Expect(res.Controller).To(Equal(options.DoController)) + Expect(res.Webhooks).NotTo(BeNil()) if options.DoDefaulting || options.DoValidation || options.DoConversion { - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.Webhooks.IsEmpty()).To(BeFalse()) + Expect(res.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) + Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) + Expect(res.Webhooks.Validation).To(Equal(options.DoValidation)) + Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion)) + Expect(res.Webhooks.IsEmpty()).To(BeFalse()) } else { - Expect(resource.Webhooks.IsEmpty()).To(BeTrue()) + Expect(res.Webhooks.IsEmpty()).To(BeTrue()) } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) + Expect(res.QualifiedGroup()).To(Equal(gvk.Group + "." + gvk.Domain)) + Expect(res.PackageName()).To(Equal(gvk.Group)) + Expect(res.ImportAlias()).To(Equal(gvk.Group + gvk.Version)) } }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - WebhookVersion: "v1", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - }), + Entry("when updating nothing", Options{}), + Entry("when updating the plural", Options{Plural: "mates"}), + Entry("when updating the API", Options{DoAPI: true, CRDVersion: "v1", Namespaced: true}), + Entry("when updating the Controller", Options{DoController: true}), + Entry("when updating Webhooks", + Options{WebhookVersion: "v1", DoDefaulting: true, DoValidation: true, DoConversion: true}), ) - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - + DescribeTable("should use core apis", + func(group, qualified string) { + options := Options{} for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -153,48 +128,39 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) + res := resource.Resource{ + GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) + + Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version))) + Expect(res.HasAPI()).To(BeFalse()) + Expect(res.QualifiedGroup()).To(Equal(qualified)) } }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), + Entry("for `apps`", "apps", "apps"), + Entry("for `authentication`", "authentication", "authentication.k8s.io"), ) - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) + DescribeTable("should use core apis with project version 2", + // This needs a separate test because project version 2 didn't store API and therefore + // the `HasAPI` method of the resource obtained with `GetResource` will always return false. + // Instead, the existence of a resource in the list means the API was scaffolded. + func(group, qualified string) { + cfg = cfgv2.New() + _ = cfg.SetRepository("test") + options := Options{} for _, multiGroup := range []bool{false, true} { if multiGroup { Expect(cfg.SetMultiGroup()).To(Succeed()) @@ -202,100 +168,28 @@ var _ = Describe("Options", func() { Expect(cfg.ClearMultiGroup()).To(Succeed()) } - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) + res := resource.Resource{ + GVK: resource.GVK{ + Group: group, + Domain: domain, + Version: version, + Kind: kind, + }, + Plural: "firstmates", + API: &resource.API{}, + Webhooks: &resource.Webhooks{}, } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), - ) - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) + options.UpdateResource(&res, cfg) + Expect(res.Validate()).To(Succeed()) - DescribeTable("should use core apis", - func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API).NotTo(BeNil()) - Expect(resource.API.IsEmpty()).To(BeTrue()) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) + Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version))) + Expect(res.HasAPI()).To(BeFalse()) + Expect(res.QualifiedGroup()).To(Equal(qualified)) } }, Entry("for `apps`", "apps", "apps"), Entry("for `authentication`", "authentication", "authentication.k8s.io"), ) - - It("should use domain if the group is empty", func() { - safeDomain := "testio" - - options := Options{ - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal("")) - if multiGroup { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "apis", options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Domain)) - Expect(resource.PackageName()).To(Equal(safeDomain)) - Expect(resource.ImportAlias()).To(Equal(safeDomain + options.Version)) - } - }) }) }) diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go index b4458cdad85..99704870d37 100644 --- a/pkg/plugins/golang/v2/api.go +++ b/pkg/plugins/golang/v2/api.go @@ -21,30 +21,26 @@ import ( "errors" "fmt" "os" - "strings" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "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/plugin/util" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config - // pattern indicates that we should use a plugin to build according to a pattern - pattern string - - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -57,22 +53,17 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by writing 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. +If information about whether the resource and controller should be scaffolded +was not explicitly provided, it will prompt the user if they should be. -After the scaffold is written, api will run make on the project. +After the scaffold is written, the dependencies will be updated and +make generate will be run. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate - %s create api --group ship --version v1beta1 --kind Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + %[1]s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme nano api/v1beta1/frigate_types.go @@ -88,30 +79,21 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run -`, ctx.CommandName) +`, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") - if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { - fs.StringVar(&p.pattern, "pattern", "", - "generates an API following an extension pattern (addon)") - } - fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") - p.options = &Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + p.options = &goPlugin.Options{CRDVersion: "v1beta1"} // p.options.Plural can be set to specify an irregular plural form fs.BoolVar(&p.options.DoAPI, "resource", true, "if set, generate the resource without prompting the user") p.resourceFlag = fs.Lookup("resource") - p.options.CRDVersion = "v1beta1" fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced") fs.BoolVar(&p.options.DoController, "controller", true, @@ -119,13 +101,19 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c - p.options.Domain = c.GetDomain() + return nil } -func (p *createAPISubcommand) Run(fs machinery.Filesystem) error { +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") + } + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { @@ -137,16 +125,7 @@ func (p *createAPISubcommand) Run(fs machinery.Filesystem) error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} - -func (p *createAPISubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err @@ -169,44 +148,24 @@ func (p *createAPISubcommand) Validate() error { return nil } -func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the requested plugins - plugins := make([]model.Plugin, 0) - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - plugins = append(plugins, &addon.Plugin{}) - default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) - } - - return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil +func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } func (p *createAPISubcommand) PostScaffold() error { - // Load the requested plugins - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version - err := util.RunCmd("Get controller runtime", "go", "get", - "sigs.k8s.io/kubebuilder-declarative-pattern@"+scaffolds.KbDeclarativePattern) - if err != nil { - return err - } - default: - return fmt.Errorf("unknown pattern %q", p.pattern) - } - err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } - if p.runMake { // TODO: check if API was scaffolded - return util.RunCmd("Running make", "make", "generate") + if p.runMake && p.resource.HasAPI() { + err = util.RunCmd("Running make", "make", "generate") + if err != nil { + return err + } } + return nil } diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go index e709d28d59f..bdab9054400 100644 --- a/pkg/plugins/golang/v2/edit.go +++ b/pkg/plugins/golang/v2/edit.go @@ -25,51 +25,41 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %[1]s edit --multigroup - - # Disable the multigroup layout - %[1]s edit --multigroup=false -`, ctx.CommandName) +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout + %[1]s edit --multigroup + + # Disable the multigroup layout + %[1]s edit --multigroup=false +`, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run(fs machinery.Filesystem) error { - return cmdutil.Run(p, fs) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v2/init.go b/pkg/plugins/golang/v2/init.go index 550e7d6d75c..6c9c7bbdb3d 100644 --- a/pkg/plugins/golang/v2/init.go +++ b/pkg/plugins/golang/v2/init.go @@ -32,9 +32,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config @@ -55,29 +56,22 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. - -Writes the following files: -- a boilerplate license file -- a PROJECT file with the domain and repo -- 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 main.go to run + subcmdMeta.Description = `Initialize a new project including the following files: + - a "go.mod" with project dependencies + - a "PROJECT" file that stores project configuration + - a "Makefile" with several useful make targets for the project + - several YAML files for project deployment under the "config" directory + - a "main.go" file that creates the manager that will run the project controllers ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, ctx.CommandName) + subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright + %[1]s init --plugins go/v2 --domain example.org --owner "Your name" - p.commandName = ctx.CommandName + # Initialize a new project defining an specific project version + %[1]s init --plugins go/v2 --project-version 2 +`, cliMeta.CommandName) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -99,25 +93,23 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.name, "project-name", "", "name of this project") } -func (p *initSubcommand) InjectConfig(c config.Config) { - // v2+ project configs get a 'layout' value. - if c.GetVersion().Compare(cfgv2.Version) > 0 { - _ = c.SetPluginChain([]string{plugin.KeyFor(Plugin{})}) - } - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run(fs machinery.Filesystem) error { - return cmdutil.Run(p, fs) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := golang.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := golang.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath + } + if err := p.config.SetRepository(p.repo); err != nil { + return err } if p.config.GetVersion().Compare(cfgv2.Version) > 0 { @@ -133,37 +125,33 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } - } - - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := golang.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if err := p.config.SetProjectName(p.name); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if p.config.GetVersion().Compare(cfgv2.Version) > 0 { - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(machinery.Filesystem) error { + // Validate the supported go versions + if !p.skipGoVersionCheck { + if err := golang.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + return nil } -func (p *initSubcommand) PostScaffold() error { +func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + err := scaffolder.Scaffold() + if err != nil { + return err + } + if !p.fetchDeps { fmt.Println("Skipping fetching dependencies.") return nil @@ -171,13 +159,17 @@ func (p *initSubcommand) PostScaffold() error { // Ensure that we are pinning controller-runtime version // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 - err := util.RunCmd("Get controller runtime", "go", "get", + err = util.RunCmd("Get controller runtime", "go", "get", "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion) if err != nil { return err } - err = util.RunCmd("Update dependencies", "go", "mod", "tidy") + return nil +} + +func (p *initSubcommand) PostScaffold() error { + err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } diff --git a/pkg/plugins/golang/v2/options.go b/pkg/plugins/golang/v2/options.go deleted file mode 100644 index 52f4410b838..00000000000 --- a/pkg/plugins/golang/v2/options.go +++ /dev/null @@ -1,198 +0,0 @@ -/* -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 v2 - -import ( - "fmt" - "path" - "strings" - - newconfig "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" - "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" -) - -const ( - groupPresent = "group flag present but empty" - versionPresent = "version flag present but empty" - kindPresent = "kind flag present but empty" - groupRequired = "group cannot be empty" - versionRequired = "version cannot be empty" - kindRequired = "kind cannot be empty" -) - -var ( - coreGroups = map[string]string{ - "admission": "k8s.io", - "admissionregistration": "k8s.io", - "apps": "", - "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", - } -) - -// Options contains the information required to build a new resource.Resource. -type Options struct { - // Group is the resource's group. Does not contain the domain. - Group string - // Domain is the resource's domain. - Domain string - // Version is the resource's version. - Version string - // Kind is the resource's kind. - Kind string - - // Plural is the resource's kind plural form. - // Optional - Plural string - - // CRDVersion is the CustomResourceDefinition API version that will be used for the resource. - CRDVersion string - // WebhookVersion is the {Validating,Mutating}WebhookConfiguration API version that will be used for the resource. - WebhookVersion string - - // Namespaced is true if the resource should be namespaced. - Namespaced bool - - // Flags that define which parts should be scaffolded - DoAPI bool - DoController bool - DoDefaulting bool - DoValidation bool - DoConversion bool -} - -// Validate verifies that all the fields have valid values -func (opts Options) Validate() error { - // Check that the required flags did not get a flag as their value - // We can safely look for a '-' as the first char as none of the fields accepts it - // NOTE: We must do this for all the required flags first or we may output the wrong - // error as flags may seem to be missing because Cobra assigned them to another flag. - if strings.HasPrefix(opts.Group, "-") { - return fmt.Errorf(groupPresent) - } - if strings.HasPrefix(opts.Version, "-") { - return fmt.Errorf(versionPresent) - } - if strings.HasPrefix(opts.Kind, "-") { - return fmt.Errorf(kindPresent) - } - - // Now we can check that all the required flags are not empty - if len(opts.Group) == 0 { - return fmt.Errorf(groupRequired) - } - if len(opts.Version) == 0 { - return fmt.Errorf(versionRequired) - } - if len(opts.Kind) == 0 { - return fmt.Errorf(kindRequired) - } - - return nil -} - -// GVK returns the GVK identifier of a resource. -func (opts Options) GVK() resource.GVK { - return resource.GVK{ - Group: opts.Group, - Domain: opts.Domain, - Version: opts.Version, - Kind: opts.Kind, - } -} - -// NewResource creates a new resource from the options -func (opts Options) NewResource(c newconfig.Config) resource.Resource { - res := resource.Resource{ - GVK: opts.GVK(), - Controller: opts.DoController, - } - - if opts.Plural != "" { - res.Plural = opts.Plural - } else { - // If not provided, compute a plural for for Kind - res.Plural = resource.RegularPlural(opts.Kind) - } - - if opts.DoAPI { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.API = &resource.API{ - CRDVersion: opts.CRDVersion, - Namespaced: opts.Namespaced, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.API = &resource.API{} - } - - if opts.DoDefaulting || opts.DoValidation || opts.DoConversion { - res.Path = resource.APIPackagePath(c.GetRepository(), opts.Group, opts.Version, c.IsMultiGroup()) - res.Webhooks = &resource.Webhooks{ - WebhookVersion: opts.WebhookVersion, - Defaulting: opts.DoDefaulting, - Validation: opts.DoValidation, - Conversion: opts.DoConversion, - } - } else { - // Make sure that the pointer is not nil to prevent pointer dereference errors - res.Webhooks = &resource.Webhooks{} - } - - // domain and path may need to be changed in case we are referring to a builtin core resource: - // - Check if we are scaffolding the resource now => project resource - // - Check if we already scaffolded the resource => project resource - // - Check if the resource group is a well-known core group => builtin core resource - // - In any other case, default to => project resource - // TODO: need to support '--resource-pkg-path' flag for specifying resourcePath - if !opts.DoAPI { - var alreadyHasAPI bool - if c.GetVersion().Compare(cfgv2.Version) == 0 { - alreadyHasAPI = c.HasResource(opts.GVK()) - } else { - loadedRes, err := c.GetResource(opts.GVK()) - alreadyHasAPI = err == nil && loadedRes.HasAPI() - } - if !alreadyHasAPI { - if domain, found := coreGroups[opts.Group]; found { - res.Domain = domain - res.Path = path.Join("k8s.io", "api", opts.Group, opts.Version) - } - } - } - - return res -} diff --git a/pkg/plugins/golang/v2/options_test.go b/pkg/plugins/golang/v2/options_test.go deleted file mode 100644 index 692a262e5ca..00000000000 --- a/pkg/plugins/golang/v2/options_test.go +++ /dev/null @@ -1,268 +0,0 @@ -/* -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 v2 - -import ( - "path" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - . "github.com/onsi/gomega" - - "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" -) - -var _ = Describe("Options", func() { - Context("Validate", func() { - DescribeTable("should succeed for valid options", - func(options Options) { Expect(options.Validate()).To(Succeed()) }, - Entry("full GVK", Options{Group: "crew", Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing domain", Options{Group: "crew", Version: "v1", Kind: "FirstMate"}), - ) - - DescribeTable("should fail for invalid options", - func(options Options) { Expect(options.Validate()).NotTo(Succeed()) }, - Entry("group flag captured another flag", Options{Group: "--version"}), - Entry("version flag captured another flag", Options{Version: "--kind"}), - Entry("kind flag captured another flag", Options{Kind: "--group"}), - Entry("missing group", Options{Domain: "test.io", Version: "v1", Kind: "FirstMate"}), - Entry("missing version", Options{Group: "crew", Domain: "test.io", Kind: "FirstMate"}), - Entry("missing kind", Options{Group: "crew", Domain: "test.io", Version: "v1"}), - ) - }) - - Context("NewResource", func() { - var cfg config.Config - - BeforeEach(func() { - cfg = cfgv2.New() - _ = cfg.SetRepository("test") - }) - - DescribeTable("should succeed if the Resource is valid", - func(options Options) { - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - Expect(resource.Domain).To(Equal(options.Domain)) - Expect(resource.Version).To(Equal(options.Version)) - Expect(resource.Kind).To(Equal(options.Kind)) - Expect(resource.API).NotTo(BeNil()) - if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion { - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - } else { - // Core-resources have a path despite not having an API/Webhook but they are not tested here - Expect(resource.Path).To(Equal("")) - } - if options.DoAPI { - Expect(resource.API.CRDVersion).To(Equal(options.CRDVersion)) - Expect(resource.API.Namespaced).To(Equal(options.Namespaced)) - Expect(resource.API.IsEmpty()).To(BeFalse()) - } else { - Expect(resource.API.IsEmpty()).To(BeTrue()) - } - Expect(resource.Controller).To(Equal(options.DoController)) - Expect(resource.Webhooks).NotTo(BeNil()) - if options.DoDefaulting || options.DoValidation || options.DoConversion { - Expect(resource.Webhooks.WebhookVersion).To(Equal(options.WebhookVersion)) - Expect(resource.Webhooks.Defaulting).To(Equal(options.DoDefaulting)) - Expect(resource.Webhooks.Validation).To(Equal(options.DoValidation)) - Expect(resource.Webhooks.Conversion).To(Equal(options.DoConversion)) - Expect(resource.Webhooks.IsEmpty()).To(BeFalse()) - } else { - Expect(resource.Webhooks.IsEmpty()).To(BeTrue()) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(options.Group)) - Expect(resource.ImportAlias()).To(Equal(options.Group + options.Version)) - } - }, - Entry("basic", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - }), - Entry("API", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, - CRDVersion: "v1beta1", - Namespaced: true, - }), - Entry("Controller", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoController: true, - }), - Entry("Webhooks", Options{ - Group: "crew", - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoDefaulting: true, - DoValidation: true, - DoConversion: true, - WebhookVersion: "v1beta1", - }), - ) - - DescribeTable("should default the Plural by pluralizing the Kind", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "firstmates"), - Entry("for `Fish`", "Fish", "fish"), - Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), - ) - - DescribeTable("should keep the Plural if specified", - func(kind, plural string) { - options := Options{Group: "crew", Version: "v1", Kind: kind, Plural: plural} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Plural).To(Equal(plural)) - } - }, - Entry("for `FirstMate`", "FirstMate", "mates"), - Entry("for `Fish`", "Fish", "shoal"), - ) - - DescribeTable("should allow hyphens and dots in group names", - func(group, safeGroup string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - DoAPI: true, // Scaffold the API so that the path is saved - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Group).To(Equal(options.Group)) - if multiGroup { - Expect(resource.Path).To(Equal( - path.Join(cfg.GetRepository(), "apis", options.Group, options.Version))) - } else { - Expect(resource.Path).To(Equal(path.Join(cfg.GetRepository(), "api", options.Version))) - } - Expect(resource.QualifiedGroup()).To(Equal(options.Group + "." + options.Domain)) - Expect(resource.PackageName()).To(Equal(safeGroup)) - Expect(resource.ImportAlias()).To(Equal(safeGroup + options.Version)) - } - }, - Entry("for hyphen-containing group", "my-project", "myproject"), - Entry("for dot-containing group", "my.project", "myproject"), - ) - - It("should not append '.' if provided an empty domain", func() { - options := Options{Group: "crew", Version: "v1", Kind: "FirstMate"} - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.QualifiedGroup()).To(Equal(options.Group)) - } - }) - - DescribeTable("should use core apis", - func(group, qualified string) { - options := Options{ - Group: group, - Domain: "test.io", - Version: "v1", - Kind: "FirstMate", - } - Expect(options.Validate()).To(Succeed()) - - for _, multiGroup := range []bool{false, true} { - if multiGroup { - Expect(cfg.SetMultiGroup()).To(Succeed()) - } else { - Expect(cfg.ClearMultiGroup()).To(Succeed()) - } - - resource := options.NewResource(cfg) - Expect(resource.Validate()).To(Succeed()) - Expect(resource.Path).To(Equal(path.Join("k8s.io", "api", options.Group, options.Version))) - Expect(resource.API).NotTo(BeNil()) - Expect(resource.API.IsEmpty()).To(BeTrue()) - Expect(resource.QualifiedGroup()).To(Equal(qualified)) - } - }, - Entry("for `apps`", "apps", "apps"), - Entry("for `authentication`", "authentication", "authentication.k8s.io"), - ) - }) -}) diff --git a/pkg/plugins/golang/v2/plugin.go b/pkg/plugins/golang/v2/plugin.go index e04d35701af..dd38b95ddda 100644 --- a/pkg/plugins/golang/v2/plugin.go +++ b/pkg/plugins/golang/v2/plugin.go @@ -27,8 +27,8 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} pluginVersion = plugin.Version{Number: 2} + supportedProjectVersions = []config.Version{cfgv2.Version, cfgv3.Version} ) var _ plugin.Full = Plugin{} diff --git a/pkg/plugins/golang/v2/scaffolds/api.go b/pkg/plugins/golang/v2/scaffolds/api.go index 01ff4a74af5..46d7f20fc42 100644 --- a/pkg/plugins/golang/v2/scaffolds/api.go +++ b/pkg/plugins/golang/v2/scaffolds/api.go @@ -24,8 +24,8 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd" @@ -34,14 +34,9 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) -// KbDeclarativePattern is the sigs.k8s.io/kubebuilder-declarative-pattern version -// (used only to gen api with --pattern=addon) -const KbDeclarativePattern = "v0.0.0-20200522144838-848d48e5b073" - -var _ cmdutil.Scaffolder = &apiScaffolder{} +var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. @@ -52,24 +47,15 @@ type apiScaffolder struct { // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin - // force indicates whether to scaffold controller files even if it exists or not force bool } // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations -func NewAPIScaffolder( - config config.Config, - res resource.Resource, - force bool, - plugins []model.Plugin, -) cmdutil.Scaffolder { +func NewAPIScaffolder(config config.Config, res resource.Resource, force bool) plugins.Scaffolder { return &apiScaffolder{ config: config, resource: res, - plugins: plugins, force: force, } } @@ -91,7 +77,6 @@ func (s *apiScaffolder) Scaffold() error { // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, - machinery.WithPlugins(s.plugins...), machinery.WithConfig(s.config), machinery.WithBoilerplate(string(boilerplate)), machinery.WithResource(&s.resource), diff --git a/pkg/plugins/golang/v2/scaffolds/edit.go b/pkg/plugins/golang/v2/scaffolds/edit.go index 623123be150..0e4f39751c2 100644 --- a/pkg/plugins/golang/v2/scaffolds/edit.go +++ b/pkg/plugins/golang/v2/scaffolds/edit.go @@ -24,10 +24,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) -var _ cmdutil.Scaffolder = &editScaffolder{} +var _ plugins.Scaffolder = &editScaffolder{} type editScaffolder struct { config config.Config @@ -38,7 +38,7 @@ type editScaffolder struct { } // NewEditScaffolder returns a new Scaffolder for configuration edit operations -func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder { +func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, diff --git a/pkg/plugins/golang/v2/scaffolds/init.go b/pkg/plugins/golang/v2/scaffolds/init.go index 1c61b4e994a..4651a109914 100644 --- a/pkg/plugins/golang/v2/scaffolds/init.go +++ b/pkg/plugins/golang/v2/scaffolds/init.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault" @@ -31,7 +32,6 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) const ( @@ -45,7 +45,7 @@ const ( imageName = "controller:latest" ) -var _ cmdutil.Scaffolder = &initScaffolder{} +var _ plugins.Scaffolder = &initScaffolder{} type initScaffolder struct { config config.Config @@ -58,7 +58,7 @@ type initScaffolder struct { } // NewInitScaffolder returns a new Scaffolder for project initialization operations -func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaffolder { +func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: hack.DefaultBoilerplatePath, @@ -104,15 +104,19 @@ func (s *initScaffolder) Scaffold() error { ) return scaffold.Execute( - &templates.GitIgnore{}, + &rbac.Kustomization{}, &rbac.AuthProxyRole{}, &rbac.AuthProxyRoleBinding{}, - &kdefault.ManagerAuthProxyPatch{}, &rbac.AuthProxyService{}, &rbac.AuthProxyClientRole{}, + &rbac.RoleBinding{}, + &rbac.LeaderElectionRole{}, + &rbac.LeaderElectionRoleBinding{}, + &manager.Kustomization{}, &manager.Config{Image: imageName}, &templates.Main{}, &templates.GoMod{ControllerRuntimeVersion: ControllerRuntimeVersion}, + &templates.GitIgnore{}, &templates.Makefile{ Image: imageName, BoilerplatePath: s.boilerplatePath, @@ -121,16 +125,12 @@ func (s *initScaffolder) Scaffold() error { }, &templates.Dockerfile{}, &kdefault.Kustomization{}, + &kdefault.ManagerAuthProxyPatch{}, &kdefault.ManagerWebhookPatch{}, - &rbac.RoleBinding{}, - &rbac.LeaderElectionRole{}, - &rbac.LeaderElectionRoleBinding{}, - &rbac.Kustomization{}, - &manager.Kustomization{}, + &kdefault.WebhookCAInjectionPatch{}, &webhook.Kustomization{}, &webhook.KustomizeConfig{}, &webhook.Service{}, - &kdefault.WebhookCAInjectionPatch{}, &prometheus.Kustomization{}, &prometheus.Monitor{}, &certmanager.Certificate{}, diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go index bccba858d49..ba9f21f6acc 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go @@ -19,17 +19,17 @@ package api import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Group{} +var _ machinery.Template = &Group{} // Group scaffolds the file that defines the registration methods for a certain group and version type Group struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go index cd0907121cf..d80e5f33b93 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/types.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Types{} +var _ machinery.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 + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin Force bool } @@ -51,9 +51,9 @@ func (f *Types) SetTemplateDefaults() error { f.TemplateBody = typesTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go index 806a37d5524..5def3986d32 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/api/webhook.go @@ -21,17 +21,17 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Webhook{} +var _ machinery.Template = &Webhook{} // Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource type Webhook struct { // nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' QualifiedGroupWithDash string @@ -58,7 +58,7 @@ func (f *Webhook) SetTemplateDefaults() error { } f.TemplateBody = webhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go index c8ced013fc8..4d17b8dff60 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/certificate.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Certificate{} +var _ machinery.Template = &Certificate{} // Certificate scaffolds a file that defines the issuer CR and the certificate CR type Certificate struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go index 7437ab18bf1..78b2c5e9484 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomization.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go index 8eada694a10..e7dbcf8986d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go index fd06bdf7848..37ec7556cb1 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomization.go @@ -20,16 +20,16 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} -var _ file.Inserter = &Kustomization{} +var _ machinery.Template = &Kustomization{} +var _ machinery.Inserter = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the crd folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -40,9 +40,9 @@ func (f *Kustomization) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(kustomizationTemplate, - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), ) return nil @@ -55,11 +55,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *Kustomization) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), +func (f *Kustomization) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), } } @@ -73,8 +73,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate resource code fragments res := make([]string, 0) @@ -90,13 +90,13 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(res) != 0 { - fragments[file.NewMarkerFor(f.Path, resourceMarker)] = res + fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res } if len(webhookPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch + fragments[machinery.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch } if len(caInjectionPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch + fragments[machinery.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go index 84025c65e3a..0013026f81c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go @@ -19,14 +19,14 @@ package crd import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go index 00d03f090d1..376a058a01c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableCAInjectionPatch{} +var _ machinery.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go index 8e3fc5d051a..1472028b627 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableWebhookPatch{} +var _ machinery.Template = &EnableWebhookPatch{} // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD type EnableWebhookPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go index c51474d4cb5..3cee6b8595d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookCAInjectionPatch{} +var _ machinery.Template = &WebhookCAInjectionPatch{} // WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks type WebhookCAInjectionPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error { f.TemplateBody = injectCAPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go index 1250bcfcdaf..68ecc351cd7 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/kustomization.go @@ -21,15 +21,15 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder type Kustomization struct { - file.TemplateMixin - file.ProjectNameMixin + machinery.TemplateMixin + machinery.ProjectNameMixin } // SetTemplateDefaults implements file.Template @@ -40,7 +40,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.ProjectName == "" { // Use directory name as project name, which will be empty if the project version is < v3. diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go index 8f8be890a7e..7d49171c16f 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerAuthProxyPatch{} +var _ machinery.Template = &ManagerAuthProxyPatch{} // ManagerAuthProxyPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager type ManagerAuthProxyPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *ManagerAuthProxyPatch) SetTemplateDefaults() error { f.TemplateBody = kustomizeAuthProxyPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go index 65e34c7d793..a9e0844bf18 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerWebhookPatch{} +var _ machinery.Template = &ManagerWebhookPatch{} // ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager type ManagerWebhookPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go index 0a1bc1cdeb7..1e08e134923 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/config.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Config{} +var _ machinery.Template = &Config{} // Config scaffolds a file that defines the namespace and the manager deployment type Config struct { - file.TemplateMixin + machinery.TemplateMixin // Image is controller manager image name Image string diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go index 1832137b95b..f8d5ecd7ec9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/manager/kustomization.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the manager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeManagerTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go index 16b9bb4b563..c271a6a3dbb 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/kustomization.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go index 601a10b28e1..261282075ec 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/prometheus/monitor.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Monitor{} +var _ machinery.Template = &Monitor{} // Monitor scaffolds a file that defines the prometheus service monitor type Monitor struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go index 2b6d77e596f..1eee0af2031 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyClientRole{} +var _ machinery.Template = &AuthProxyClientRole{} // AuthProxyClientRole scaffolds a file that defines the role for the metrics reader type AuthProxyClientRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go index 01b8413b855..df22ef8dc39 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRole{} +var _ machinery.Template = &AuthProxyRole{} // AuthProxyRole scaffolds a file that defines the role for the auth proxy type AuthProxyRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go index 1aaf9ebbf85..eafc45f6ee9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRoleBinding{} +var _ machinery.Template = &AuthProxyRoleBinding{} // AuthProxyRoleBinding scaffolds a file that defines the role binding for the auth proxy type AuthProxyRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go index e84ec0e322c..6287d360ebb 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/auth_proxy_service.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyService{} +var _ machinery.Template = &AuthProxyService{} // AuthProxyService scaffolds a file that defines the service for the auth proxy type AuthProxyService struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go index 3de358308bd..7024549629d 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDEditorRole{} +var _ machinery.Template = &CRDEditorRole{} // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals type CRDEditorRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go index 5898a8fe334..74177476661 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDViewerRole{} +var _ machinery.Template = &CRDViewerRole{} // CRDViewerRole scaffolds a file that defines the role that allows to view plurals type CRDViewerRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go index d8d9b41b180..f5b164e5b79 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/kustomization.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeRBACTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go index b787f441f59..6de4d48b784 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRole{} +var _ machinery.Template = &LeaderElectionRole{} // LeaderElectionRole scaffolds a file that defines the role that allows leader election type LeaderElectionRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go index f196868a3f8..9dd75b7ff6b 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRoleBinding{} +var _ machinery.Template = &LeaderElectionRoleBinding{} // LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election type LeaderElectionRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go index 6786223d648..0cc6687e8c3 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/rbac/role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &RoleBinding{} +var _ machinery.Template = &RoleBinding{} // RoleBinding scaffolds a file that defines the role binding for the manager type RoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go index b9b349ff336..af9e29f6f6c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/samples/crd_sample.go @@ -19,15 +19,15 @@ package samples import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDSample{} +var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -40,9 +40,9 @@ func (f *CRDSample) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.TemplateBody = crdSampleTemplate diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go index c16ee89b9f6..7157dd8380c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomization.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeWebhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go index 1b43d4ee825..ac2c92cc89e 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/kustomizeconfig.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the webhook folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *KustomizeConfig) SetTemplateDefaults() error { f.TemplateBody = kustomizeConfigWebhookTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go index 3a2689c4d42..7783aa136c4 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/config/webhook/service.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Service{} +var _ machinery.Template = &Service{} // Service scaffolds a file that defines the webhook service type Service struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Service) SetTemplateDefaults() error { f.TemplateBody = serviceTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go index ee059001455..3d400d399e1 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Controller{} +var _ machinery.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 + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin ControllerRuntimeVersion string @@ -53,9 +53,9 @@ func (f *Controller) SetTemplateDefaults() error { f.TemplateBody = controllerTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go index 61bd1d584d7..31cef7a8650 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers/controller_suitetest.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &SuiteTest{} -var _ file.Inserter = &SuiteTest{} +var _ machinery.Template = &SuiteTest{} +var _ machinery.Inserter = &SuiteTest{} // SuiteTest scaffolds the file that sets up the controller tests // nolint:maligned type SuiteTest struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string @@ -52,8 +52,8 @@ func (f *SuiteTest) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), ) // If is multigroup the path needs to be ../../ since it has @@ -64,7 +64,7 @@ func (f *SuiteTest) SetTemplateDefaults() error { } if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } return nil @@ -76,10 +76,10 @@ const ( ) // GetMarkers implements file.Inserter -func (f *SuiteTest) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), +func (f *SuiteTest) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), } } @@ -93,8 +93,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 2) +func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 2) // Generate import code fragments imports := make([]string, 0) @@ -110,10 +110,10 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go index 1d20cafef78..38e4c47167c 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/dockerfile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Dockerfile{} +var _ machinery.Template = &Dockerfile{} // Dockerfile scaffolds a file that defines the containerized build process type Dockerfile struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go index 4505799177b..a60f46d6760 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/gitignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GitIgnore{} +var _ machinery.Template = &GitIgnore{} // GitIgnore scaffolds a file that defines which files should be ignored by git type GitIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go index f40e8eda54d..e369bc98567 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/gomod.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GoMod{} +var _ machinery.Template = &GoMod{} // GoMod scaffolds a file that defines the project dependencies type GoMod struct { - file.TemplateMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.RepositoryMixin ControllerRuntimeVersion string } @@ -38,7 +38,7 @@ func (f *GoMod) SetTemplateDefaults() error { f.TemplateBody = goModTemplate - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile return nil } diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go index a7b6dac612e..1d07c79b45a 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/hack/boilerplate.go @@ -21,18 +21,18 @@ import ( "path/filepath" "time" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) // DefaultBoilerplatePath is the default path to the boilerplate file var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") -var _ file.Template = &Boilerplate{} +var _ machinery.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files type Boilerplate struct { - file.TemplateMixin - file.BoilerplateMixin + machinery.TemplateMixin + machinery.BoilerplateMixin // License is the License type to write License string diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go index f234d80970a..c2bd6928d85 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) const defaultMainPath = "main.go" -var _ file.Template = &Main{} +var _ machinery.Template = &Main{} // Main scaffolds a file that defines the controller manager entry point type Main struct { - file.TemplateMixin - file.BoilerplateMixin - file.DomainMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.BoilerplateMixin + machinery.DomainMixin + machinery.RepositoryMixin } // SetTemplateDefaults implements file.Template @@ -42,21 +42,21 @@ func (f *Main) SetTemplateDefaults() error { } f.TemplateBody = fmt.Sprintf(mainTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, setupMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, setupMarker), ) return nil } -var _ file.Inserter = &MainUpdater{} +var _ machinery.Inserter = &MainUpdater{} // MainUpdater updates main.go to run Controllers type MainUpdater struct { //nolint:maligned - file.RepositoryMixin - file.MultiGroupMixin - file.ResourceMixin + machinery.RepositoryMixin + machinery.MultiGroupMixin + machinery.ResourceMixin // Flags to indicate which parts need to be included when updating the file WireResource, WireController, WireWebhook bool @@ -68,8 +68,8 @@ func (*MainUpdater) GetPath() string { } // GetIfExistsAction implements file.Builder -func (*MainUpdater) GetIfExistsAction() file.IfExistsAction { - return file.Overwrite +func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction { + return machinery.OverwriteFile } const ( @@ -79,11 +79,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *MainUpdater) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(defaultMainPath, importMarker), - file.NewMarkerFor(defaultMainPath, addSchemeMarker), - file.NewMarkerFor(defaultMainPath, setupMarker), +func (f *MainUpdater) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(defaultMainPath, importMarker), + machinery.NewMarkerFor(defaultMainPath, addSchemeMarker), + machinery.NewMarkerFor(defaultMainPath, setupMarker), } } @@ -122,8 +122,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // If resource is not being provided we are creating the file, not updating it if f.Resource == nil { @@ -169,13 +169,13 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, importMarker)] = imports + fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme } if len(setup) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, setupMarker)] = setup + fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup } return fragments diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go index cea72da1141..e799bd05cc9 100644 --- a/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go +++ b/pkg/plugins/golang/v2/scaffolds/internal/templates/makefile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Makefile{} +var _ machinery.Template = &Makefile{} // Makefile scaffolds a file that defines project management CLI commands type Makefile struct { - file.TemplateMixin + machinery.TemplateMixin // Image is controller manager image name Image string @@ -44,7 +44,7 @@ func (f *Makefile) SetTemplateDefaults() error { f.TemplateBody = makefileTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.Image == "" { f.Image = "controller:latest" diff --git a/pkg/plugins/golang/v2/scaffolds/webhook.go b/pkg/plugins/golang/v2/scaffolds/webhook.go index 72c58907d16..d3cef711cad 100644 --- a/pkg/plugins/golang/v2/scaffolds/webhook.go +++ b/pkg/plugins/golang/v2/scaffolds/webhook.go @@ -24,13 +24,13 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) -var _ cmdutil.Scaffolder = &webhookScaffolder{} +var _ plugins.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { config config.Config @@ -41,7 +41,7 @@ type webhookScaffolder struct { } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations -func NewWebhookScaffolder(config config.Config, resource resource.Resource) cmdutil.Scaffolder { +func NewWebhookScaffolder(config config.Config, resource resource.Resource) plugins.Scaffolder { return &webhookScaffolder{ config: config, resource: resource, diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go index a22bd624285..d142298850b 100644 --- a/pkg/plugins/golang/v2/webhook.go +++ b/pkg/plugins/golang/v2/webhook.go @@ -26,48 +26,43 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" + goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { config config.Config // For help text. commandName string - options *Options + options *goPlugin.Options - resource resource.Resource + resource *resource.Resource } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, -validating and (or) conversion webhooks. + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, +validating and/or conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 - # and kind Frigate. + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1 + # and Kind: Frigate %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation - # Create conversion webhook for CRD of group shio, version v1beta1 and kind Frigate. + # Create conversion webhook for Group: ship, Version: v1beta1 + # and Kind: Frigate %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, ctx.CommandName) - - p.commandName = ctx.CommandName +`, cliMeta.CommandName) } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { - p.options = &Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + p.options = &goPlugin.Options{WebhookVersion: "v1beta1"} + fs.StringVar(&p.options.Plural, "resource", "", "resource irregular plural form") - p.options.WebhookVersion = "v1beta1" fs.BoolVar(&p.options.DoDefaulting, "defaulting", false, "if set, scaffold the defaulting webhook") fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false, @@ -76,24 +71,21 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "if set, scaffold the conversion webhook") } -func (p *createWebhookSubcommand) InjectConfig(c config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c - p.options.Domain = c.GetDomain() + return nil } -func (p *createWebhookSubcommand) Run(fs machinery.Filesystem) error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res -func (p *createWebhookSubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err + if p.resource.Group == "" { + return fmt.Errorf("group cannot be empty") } + p.options.UpdateResource(p.resource, p.config) + if err := p.resource.Validate(); err != nil { return err } @@ -119,10 +111,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewWebhookScaffolder(p.config, p.resource), nil -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/api.go b/pkg/plugins/golang/v3/api.go index 5c0882f637a..71d4443e93d 100644 --- a/pkg/plugins/golang/v3/api.go +++ b/pkg/plugins/golang/v3/api.go @@ -21,28 +21,19 @@ import ( "errors" "fmt" "os" - "strings" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "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/plugin/util" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" - "sigs.k8s.io/kubebuilder/v3/plugins/addon" ) const ( - // KbDeclarativePatternVersion is the sigs.k8s.io/kubebuilder-declarative-pattern version - // (used only to gen api with --pattern=addon) - // TODO: remove this when a better solution for using addons is implemented. - KbDeclarativePatternVersion = "b84d99da021778217217885dd9582ed3cc879ebe" - // defaultCRDVersion is the default CRD API version to scaffold. defaultCRDVersion = "v1" ) @@ -50,15 +41,14 @@ const ( // DefaultMainPath is default file path of main.go const DefaultMainPath = "main.go" +var _ plugin.CreateAPISubcommand = &createAPISubcommand{} + type createAPISubcommand struct { config config.Config - // pattern indicates that we should use a plugin to build according to a pattern - pattern string - options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // Check if we have to scaffold resource and/or controller resourceFlag *pflag.Flag @@ -71,22 +61,17 @@ type createAPISubcommand struct { runMake bool } -var ( - _ plugin.CreateAPISubcommand = &createAPISubcommand{} - _ cmdutil.RunOptions = &createAPISubcommand{} -) - -func (p createAPISubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a Kubernetes API by creating a Resource definition and / or a Controller. +func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `Scaffold a Kubernetes API by writing 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. +If information about whether the resource and controller should be scaffolded +was not explicitly provided, it will prompt the user if they should be. -After the scaffold is written, api will run make on the project. +After the scaffold is written, the dependencies will be updated and +make generate will be run. ` - ctx.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate - %s create api --group ship --version v1beta1 --kind Frigate + subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate + %[1]s create api --group ship --version v1beta1 --kind Frigate # Edit the API Scheme nano api/v1beta1/frigate_types.go @@ -102,25 +87,17 @@ After the scaffold is written, api will run make on the project. # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run -`, ctx.CommandName) +`, cliMeta.CommandName) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") - // TODO: remove this when a better solution for using addons is implemented. - if os.Getenv("KUBEBUILDER_ENABLE_PLUGINS") != "" { - fs.StringVar(&p.pattern, "pattern", "", - "generates an API following an extension pattern (addon)") - } - fs.BoolVar(&p.force, "force", false, "attempt to create resource even if it already exists") p.options = &goPlugin.Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") fs.BoolVar(&p.options.DoAPI, "resource", true, @@ -135,16 +112,18 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { p.controllerFlag = fs.Lookup("controller") } -func (p *createAPISubcommand) InjectConfig(c config.Config) { +func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c - // TODO: offer a flag instead of hard-coding the project-wide domain - p.options.Domain = c.GetDomain() + return nil } -func (p *createAPISubcommand) Run(fs machinery.Filesystem) error { +func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { + p.resource = res + // TODO: re-evaluate whether y/n input still makes sense. We should probably always - // scaffold the resource and controller. + // scaffold the resource and controller. + // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { fmt.Println("Create Resource [y/n]") @@ -155,30 +134,16 @@ func (p *createAPISubcommand) Run(fs machinery.Filesystem) error { p.options.DoController = util.YesNo(reader) } - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} - -func (p *createAPISubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err } - // check if main.go is present in the root directory - if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { - return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) - } - // In case we want to scaffold a resource API we need to do some checks if p.options.DoAPI { // Check that resource doesn't have the API scaffolded or flag force was set - if res, err := p.config.GetResource(p.resource.GVK); err == nil && res.HasAPI() && !p.force { + if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force { return errors.New("API resource already exists") } @@ -198,45 +163,32 @@ func (p *createAPISubcommand) Validate() error { return nil } -func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - // Load the requested plugins - plugins := make([]model.Plugin, 0) - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - plugins = append(plugins, &addon.Plugin{}) - default: - return nil, fmt.Errorf("unknown pattern %q", p.pattern) +func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error { + // check if main.go is present in the root directory + if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) { + return fmt.Errorf("%s file should present in the root directory", DefaultMainPath) } - return scaffolds.NewAPIScaffolder(p.config, p.resource, p.force, plugins), nil + return nil } -func (p *createAPISubcommand) PostScaffold() error { - // Load the requested plugins - switch strings.ToLower(p.pattern) { - case "": - // Default pattern - case "addon": - // Ensure that we are pinning sigs.k8s.io/kubebuilder-declarative-pattern version - // TODO: either find a better way to inject this version (ex. tools.go). - err := util.RunCmd("Get kubebuilder-declarative-pattern dependency", "go", "get", - "sigs.k8s.io/kubebuilder-declarative-pattern@"+KbDeclarativePatternVersion) - if err != nil { - return err - } - default: - return fmt.Errorf("unknown pattern %q", p.pattern) - } +func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() +} +func (p *createAPISubcommand) PostScaffold() error { err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } - - if p.runMake { // TODO: check if API was scaffolded - return util.RunCmd("Running make", "make", "generate") + if p.runMake && p.resource.HasAPI() { + err = util.RunCmd("Running make", "make", "generate") + if err != nil { + return err + } } + return nil } diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go index 934f2d42b5c..b75e7339bd6 100644 --- a/pkg/plugins/golang/v3/edit.go +++ b/pkg/plugins/golang/v3/edit.go @@ -25,51 +25,41 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.EditSubcommand = &editSubcommand{} + type editSubcommand struct { config config.Config multigroup bool } -var ( - _ plugin.EditSubcommand = &editSubcommand{} - _ cmdutil.RunOptions = &editSubcommand{} -) - -func (p *editSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `This command will edit the project configuration. You can have single or multi group project.` - - ctx.Examples = fmt.Sprintf(`# Enable the multigroup layout - %[1]s edit --multigroup - - # Disable the multigroup layout - %[1]s edit --multigroup=false -`, ctx.CommandName) +func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + subcmdMeta.Description = `This command will edit the project configuration. +Features supported: + - Toggle between single or multi group projects. +` + subcmdMeta.Examples = fmt.Sprintf(` # Enable the multigroup layout + %[1]s edit --multigroup + + # Disable the multigroup layout + %[1]s edit --multigroup=false +`, cliMeta.CommandName) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout") } -func (p *editSubcommand) InjectConfig(c config.Config) { +func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *editSubcommand) Run(fs machinery.Filesystem) error { - return cmdutil.Run(p, fs) -} - -func (p *editSubcommand) Validate() error { return nil } -func (p *editSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewEditScaffolder(p.config, p.multigroup), nil -} - -func (p *editSubcommand) PostScaffold() error { - return nil +func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/golang/v3/init.go b/pkg/plugins/golang/v3/init.go index 8cc17170fcb..3c9883e7254 100644 --- a/pkg/plugins/golang/v3/init.go +++ b/pkg/plugins/golang/v3/init.go @@ -31,9 +31,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) +var _ plugin.InitSubcommand = &initSubcommand{} + type initSubcommand struct { config config.Config // For help text. @@ -54,29 +55,22 @@ type initSubcommand struct { skipGoVersionCheck bool } -var ( - _ plugin.InitSubcommand = &initSubcommand{} - _ cmdutil.RunOptions = &initSubcommand{} -) +func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *initSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Initialize a new project including vendor/ directory and Go package directories. - -Writes the following files: -- a boilerplate license file -- a PROJECT file with the domain and repo -- 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 main.go to run + subcmdMeta.Description = `Initialize a new project including the following files: + - a "go.mod" with project dependencies + - a "PROJECT" file that stores project configuration + - a "Makefile" with several useful make targets for the project + - several YAML files for project deployment under the "config" directory + - a "main.go" file that creates the manager that will run the project controllers ` - ctx.Examples = fmt.Sprintf(` # Scaffold a project using the apache2 license with "The Kubernetes authors" as owners - %s init --project-version=2 --domain example.org --license apache2 --owner "The Kubernetes authors" -`, ctx.CommandName) + subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project with your domain and name in copyright + %[1]s init --plugins go/v3 --domain example.org --owner "Your name" - p.commandName = ctx.CommandName + # Initialize a new project defining an specific project version + %[1]s init --plugins go/v3 --project-version 3 +`, cliMeta.CommandName) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { @@ -100,26 +94,22 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { "create a versioned ComponentConfig file, may be 'true' or 'false'") } -func (p *initSubcommand) InjectConfig(c config.Config) { - _ = c.SetPluginChain([]string{plugin.KeyFor(Plugin{})}) - +func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c -} -func (p *initSubcommand) Run(fs machinery.Filesystem) error { - return cmdutil.Run(p, fs) -} + if err := p.config.SetDomain(p.domain); err != nil { + return err + } -func (p *initSubcommand) Validate() error { - // Requires go1.11+ - if !p.skipGoVersionCheck { - if err := golang.ValidateGoVersion(); err != nil { - return err + // Try to guess repository if flag is not set. + if p.repo == "" { + repoPath, err := golang.FindCurrentRepo() + if err != nil { + return fmt.Errorf("error finding current repository: %v", err) } + p.repo = repoPath } - - // Check if the current directory has not files or directories which does not allow to init the project - if err := checkDir(); err != nil { + if err := p.config.SetRepository(p.repo); err != nil { return err } @@ -135,39 +125,43 @@ func (p *initSubcommand) Validate() error { if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name (%s) is invalid: %v", p.name, err) } + if err := p.config.SetProjectName(p.name); err != nil { + return err + } - // Try to guess repository if flag is not set. - if p.repo == "" { - repoPath, err := golang.FindCurrentRepo() - if err != nil { - return fmt.Errorf("error finding current repository: %v", err) + if p.componentConfig { + if err := p.config.SetComponentConfig(); err != nil { + return err } - p.repo = repoPath } return nil } -func (p *initSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - if err := p.config.SetDomain(p.domain); err != nil { - return nil, err - } - if err := p.config.SetRepository(p.repo); err != nil { - return nil, err - } - if err := p.config.SetProjectName(p.name); err != nil { - return nil, err - } - if p.componentConfig { - if err := p.config.SetComponentConfig(); err != nil { - return nil, err +func (p *initSubcommand) PreScaffold(machinery.Filesystem) error { + // Requires go1.11+ + if !p.skipGoVersionCheck { + if err := golang.ValidateGoVersion(); err != nil { + return err } } - return scaffolds.NewInitScaffolder(p.config, p.license, p.owner), nil + // Check if the current directory has not files or directories which does not allow to init the project + if err := checkDir(); err != nil { + return err + } + + return nil } -func (p *initSubcommand) PostScaffold() error { +func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner) + scaffolder.InjectFS(fs) + err := scaffolder.Scaffold() + if err != nil { + return err + } + if !p.fetchDeps { fmt.Println("Skipping fetching dependencies.") return nil @@ -175,13 +169,17 @@ func (p *initSubcommand) PostScaffold() error { // Ensure that we are pinning controller-runtime version // xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997 - err := util.RunCmd("Get controller runtime", "go", "get", + err = util.RunCmd("Get controller runtime", "go", "get", "sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion) if err != nil { return err } - err = util.RunCmd("Update dependencies", "go", "mod", "tidy") + return nil +} + +func (p *initSubcommand) PostScaffold() error { + err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return err } @@ -229,7 +227,3 @@ func checkDir() error { } return nil } - -// The go.mod is allowed because user might run -// go mod init before use the plugin it for not be required inform -// the go module via the repo --flag. diff --git a/pkg/plugins/golang/v3/plugin.go b/pkg/plugins/golang/v3/plugin.go index f109b5d1223..1d6a2c381cb 100644 --- a/pkg/plugins/golang/v3/plugin.go +++ b/pkg/plugins/golang/v3/plugin.go @@ -26,8 +26,8 @@ import ( const pluginName = "go" + plugins.DefaultNameQualifier var ( - supportedProjectVersions = []config.Version{cfgv3.Version} pluginVersion = plugin.Version{Number: 3} + supportedProjectVersions = []config.Version{cfgv3.Version} ) var _ plugin.Full = Plugin{} diff --git a/pkg/plugins/golang/v3/scaffolds/api.go b/pkg/plugins/golang/v3/scaffolds/api.go index da60222bad7..5b31798200a 100644 --- a/pkg/plugins/golang/v3/scaffolds/api.go +++ b/pkg/plugins/golang/v3/scaffolds/api.go @@ -23,8 +23,8 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/model" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd" @@ -33,10 +33,9 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) -var _ cmdutil.Scaffolder = &apiScaffolder{} +var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. @@ -47,24 +46,15 @@ type apiScaffolder struct { // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin - // force indicates whether to scaffold controller files even if it exists or not force bool } // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations -func NewAPIScaffolder( - config config.Config, - res resource.Resource, - force bool, - plugins []model.Plugin, -) cmdutil.Scaffolder { +func NewAPIScaffolder(config config.Config, res resource.Resource, force bool) plugins.Scaffolder { return &apiScaffolder{ config: config, resource: res, - plugins: plugins, force: force, } } @@ -86,7 +76,6 @@ func (s *apiScaffolder) Scaffold() error { // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, - machinery.WithPlugins(s.plugins...), machinery.WithConfig(s.config), machinery.WithBoilerplate(string(boilerplate)), machinery.WithResource(&s.resource), diff --git a/pkg/plugins/golang/v3/scaffolds/edit.go b/pkg/plugins/golang/v3/scaffolds/edit.go index 91da66e9846..e099a8a7b16 100644 --- a/pkg/plugins/golang/v3/scaffolds/edit.go +++ b/pkg/plugins/golang/v3/scaffolds/edit.go @@ -24,10 +24,10 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" ) -var _ cmdutil.Scaffolder = &editScaffolder{} +var _ plugins.Scaffolder = &editScaffolder{} type editScaffolder struct { config config.Config @@ -38,7 +38,7 @@ type editScaffolder struct { } // NewEditScaffolder returns a new Scaffolder for configuration edit operations -func NewEditScaffolder(config config.Config, multigroup bool) cmdutil.Scaffolder { +func NewEditScaffolder(config config.Config, multigroup bool) plugins.Scaffolder { return &editScaffolder{ config: config, multigroup: multigroup, diff --git a/pkg/plugins/golang/v3/scaffolds/init.go b/pkg/plugins/golang/v3/scaffolds/init.go index e86b4e7379c..1b7b6ece584 100644 --- a/pkg/plugins/golang/v3/scaffolds/init.go +++ b/pkg/plugins/golang/v3/scaffolds/init.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault" @@ -30,7 +31,6 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) const ( @@ -44,7 +44,7 @@ const ( imageName = "controller:latest" ) -var _ cmdutil.Scaffolder = &initScaffolder{} +var _ plugins.Scaffolder = &initScaffolder{} type initScaffolder struct { config config.Config @@ -57,7 +57,7 @@ type initScaffolder struct { } // NewInitScaffolder returns a new Scaffolder for project initialization operations -func NewInitScaffolder(config config.Config, license, owner string) cmdutil.Scaffolder { +func NewInitScaffolder(config config.Config, license, owner string) plugins.Scaffolder { return &initScaffolder{ config: config, boilerplatePath: hack.DefaultBoilerplatePath, diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go index f8c1faa7e2b..8c93af689a0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/group.go @@ -19,17 +19,17 @@ package api import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Group{} +var _ machinery.Template = &Group{} // Group scaffolds the file that defines the registration methods for a certain group and version type Group struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go index 4c253344db0..a76e3cb1391 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/types.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Types{} +var _ machinery.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 + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin Force bool } @@ -55,9 +55,9 @@ func (f *Types) SetTemplateDefaults() error { f.TemplateBody = typesTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go index f3f9c6ccd76..f51ef49bda0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook.go @@ -21,17 +21,17 @@ import ( "path/filepath" "strings" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Webhook{} +var _ machinery.Template = &Webhook{} // Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource type Webhook struct { // nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // Is the Group domain for the Resource replacing '.' with '-' QualifiedGroupWithDash string @@ -65,9 +65,9 @@ func (f *Webhook) SetTemplateDefaults() error { f.TemplateBody = webhookTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.QualifiedGroupWithDash = strings.Replace(f.Resource.QualifiedGroup(), ".", "-", -1) diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go index f708a8afbb8..7f30dd89f37 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/api/webhook_suitetest.go @@ -4,18 +4,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookSuite{} -var _ file.Inserter = &WebhookSuite{} +var _ machinery.Template = &WebhookSuite{} +var _ machinery.Inserter = &WebhookSuite{} // WebhookSuite scaffolds the file that sets up the webhook tests type WebhookSuite struct { //nolint:maligned - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // todo: currently is not possible to know if an API was or not scaffolded. We can fix it when #1826 be addressed WireResource bool @@ -40,9 +40,9 @@ func (f *WebhookSuite) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, addWebhookManagerMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), "%s", "%d", ) @@ -66,11 +66,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *WebhookSuite) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, addWebhookManagerMarker), +func (f *WebhookSuite) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, addWebhookManagerMarker), } } @@ -88,8 +88,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *WebhookSuite) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate import code fragments imports := make([]string, 0) @@ -105,13 +105,13 @@ func (f *WebhookSuite) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(addWebhookManager) != 0 { - fragments[file.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager + fragments[machinery.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager } if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go index a654f82951f..76c2b186f65 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/certificate.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Certificate{} +var _ machinery.Template = &Certificate{} // Certificate scaffolds a file that defines the issuer CR and the certificate CR type Certificate struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go index 4210f6f133b..522bdd9b67f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomization.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go index 8ade67534fc..573a7e8f602 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go @@ -19,14 +19,14 @@ package certmanager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go index 9a87af34668..23690588e9f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomization.go @@ -20,16 +20,16 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} -var _ file.Inserter = &Kustomization{} +var _ machinery.Template = &Kustomization{} +var _ machinery.Inserter = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the crd folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -40,9 +40,9 @@ func (f *Kustomization) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(kustomizationTemplate, - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), ) return nil @@ -55,11 +55,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *Kustomization) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, resourceMarker), - file.NewMarkerFor(f.Path, webhookPatchMarker), - file.NewMarkerFor(f.Path, caInjectionPatchMarker), +func (f *Kustomization) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, resourceMarker), + machinery.NewMarkerFor(f.Path, webhookPatchMarker), + machinery.NewMarkerFor(f.Path, caInjectionPatchMarker), } } @@ -73,8 +73,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // Generate resource code fragments res := make([]string, 0) @@ -90,13 +90,13 @@ func (f *Kustomization) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(res) != 0 { - fragments[file.NewMarkerFor(f.Path, resourceMarker)] = res + fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res } if len(webhookPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch + fragments[machinery.NewMarkerFor(f.Path, webhookPatchMarker)] = webhookPatch } if len(caInjectionPatch) != 0 { - fragments[file.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch + fragments[machinery.NewMarkerFor(f.Path, caInjectionPatchMarker)] = caInjectionPatch } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go index 7b56a21c9df..428bfde8b88 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/kustomizeconfig.go @@ -19,15 +19,15 @@ package crd import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go index c954670d1db..cc688e50f63 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableCAInjectionPatch{} +var _ machinery.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go index 7cc1da1d65e..1bf0e7ac071 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go @@ -19,15 +19,15 @@ package patches import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &EnableWebhookPatch{} +var _ machinery.Template = &EnableWebhookPatch{} // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD type EnableWebhookPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go index a3ba80da2b9..d93780f7dfa 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/enablecainection_patch.go @@ -19,15 +19,15 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &WebhookCAInjectionPatch{} +var _ machinery.Template = &WebhookCAInjectionPatch{} // WebhookCAInjectionPatch scaffolds a file that defines the patch that adds annotation to webhooks type WebhookCAInjectionPatch struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template @@ -39,7 +39,7 @@ func (f *WebhookCAInjectionPatch) SetTemplateDefaults() error { f.TemplateBody = injectCAPatchTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go index c7aea4259fb..b6860c307d0 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/kustomization.go @@ -19,16 +19,16 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder type Kustomization struct { - file.TemplateMixin - file.ProjectNameMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ProjectNameMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -39,7 +39,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go index 8cf97588bc9..c47ab74426f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_auth_proxy_patch.go @@ -19,15 +19,15 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerAuthProxyPatch{} +var _ machinery.Template = &ManagerAuthProxyPatch{} // ManagerAuthProxyPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager type ManagerAuthProxyPatch struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *ManagerAuthProxyPatch) SetTemplateDefaults() error { f.TemplateBody = kustomizeAuthProxyPatchTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go index 8b32276fe7b..fb620573bbe 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/manager_config_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerConfigPatch{} +var _ machinery.Template = &ManagerConfigPatch{} // ManagerConfigPatch scaffolds a ManagerConfigPatch for a Resource type ManagerConfigPatch struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements input.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go index 3d86dd4f091..7f993dbd31c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go @@ -19,14 +19,14 @@ package kdefault import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ManagerWebhookPatch{} +var _ machinery.Template = &ManagerWebhookPatch{} // ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager type ManagerWebhookPatch struct { - file.TemplateMixin + machinery.TemplateMixin Force bool } @@ -40,10 +40,10 @@ func (f *ManagerWebhookPatch) SetTemplateDefaults() error { f.TemplateBody = managerWebhookPatchTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go index f8657a4401d..5527d331be4 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/config.go @@ -19,15 +19,15 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Config{} +var _ machinery.Template = &Config{} // Config scaffolds a file that defines the namespace and the manager deployment type Config struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin // Image is controller manager image name Image string diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go index ea18f2145a0..fa977d90ecb 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/controller_manager_config.go @@ -19,16 +19,16 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ControllerManagerConfig{} +var _ machinery.Template = &ControllerManagerConfig{} // ControllerManagerConfig scaffolds the config file in config/manager folder. type ControllerManagerConfig struct { - file.TemplateMixin - file.DomainMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.DomainMixin + machinery.RepositoryMixin } // SetTemplateDefaults implements input.Template @@ -39,7 +39,7 @@ func (f *ControllerManagerConfig) SetTemplateDefaults() error { f.TemplateBody = controllerManagerConfigTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go index 5b3f307acfb..eec807da3d1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/manager/kustomization.go @@ -19,14 +19,14 @@ package manager import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the manager folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeManagerTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go index e2cd771253a..76bf6e1c5e1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/kustomization.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go index 601a10b28e1..261282075ec 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/prometheus/monitor.go @@ -19,14 +19,14 @@ package prometheus import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Monitor{} +var _ machinery.Template = &Monitor{} // Monitor scaffolds a file that defines the prometheus service monitor type Monitor struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go index d7bfee31882..e41eeceb55e 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_client_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyClientRole{} +var _ machinery.Template = &AuthProxyClientRole{} // AuthProxyClientRole scaffolds a file that defines the role for the metrics reader type AuthProxyClientRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go index f5900ec2b8e..0f359d78e4c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRole{} +var _ machinery.Template = &AuthProxyRole{} // AuthProxyRole scaffolds a file that defines the role for the auth proxy type AuthProxyRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go index 5003d1e4d8d..9bbc09da9a2 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyRoleBinding{} +var _ machinery.Template = &AuthProxyRoleBinding{} // AuthProxyRoleBinding scaffolds a file that defines the role binding for the auth proxy type AuthProxyRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go index ffd9cd2ab19..f9c7249c829 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/auth_proxy_service.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &AuthProxyService{} +var _ machinery.Template = &AuthProxyService{} // AuthProxyService scaffolds a file that defines the service for the auth proxy type AuthProxyService struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go index a099b595e9b..a8832864277 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_editor_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDEditorRole{} +var _ machinery.Template = &CRDEditorRole{} // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals type CRDEditorRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go index 0b3311650b7..d83c3295f53 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/crd_viewer_role.go @@ -19,15 +19,15 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDViewerRole{} +var _ machinery.Template = &CRDViewerRole{} // CRDViewerRole scaffolds a file that defines the role that allows to view plurals type CRDViewerRole struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go index c87e412896c..d3ea9b22fd9 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/kustomization.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder type Kustomization struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -37,7 +37,7 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeRBACTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go index 3f85432601f..1008bf2c387 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRole{} +var _ machinery.Template = &LeaderElectionRole{} // LeaderElectionRole scaffolds a file that defines the role that allows leader election type LeaderElectionRole struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go index c523bed20f6..8148f9ab393 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &LeaderElectionRoleBinding{} +var _ machinery.Template = &LeaderElectionRoleBinding{} // LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election type LeaderElectionRoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go index 5407455f47d..9473660e03c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/role_binding.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &RoleBinding{} +var _ machinery.Template = &RoleBinding{} // RoleBinding scaffolds a file that defines the role binding for the manager type RoleBinding struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go index cc33daf3e41..14227777734 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go @@ -19,14 +19,14 @@ package rbac import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &ServiceAccount{} +var _ machinery.Template = &ServiceAccount{} // ServiceAccount scaffolds a file that defines the service account the manager is deployed in. type ServiceAccount struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go index ab68ba16e4c..f813a2353b7 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/samples/crd_sample.go @@ -19,15 +19,15 @@ package samples import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &CRDSample{} +var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -40,9 +40,9 @@ func (f *CRDSample) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } f.TemplateBody = crdSampleTemplate diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go index 74546ab10ae..3f55f70a12f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomization.go @@ -19,15 +19,15 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Kustomization{} +var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { - file.TemplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.ResourceMixin Force bool } @@ -41,10 +41,10 @@ func (f *Kustomization) SetTemplateDefaults() error { f.TemplateBody = kustomizeWebhookTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go index a719ae63c46..524f11e71c1 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/kustomizeconfig.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &KustomizeConfig{} +var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the webhook folder type KustomizeConfig struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *KustomizeConfig) SetTemplateDefaults() error { f.TemplateBody = kustomizeConfigWebhookTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go index f05e6bc719a..a7052dbb3bc 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook/service.go @@ -19,14 +19,14 @@ package webhook import ( "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Service{} +var _ machinery.Template = &Service{} // Service scaffolds a file that defines the webhook service type Service struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template @@ -38,7 +38,7 @@ func (f *Service) SetTemplateDefaults() error { f.TemplateBody = serviceTemplate // If file exists (ex. because a webhook was already created), skip creation. - f.IfExistsAction = file.Skip + f.IfExistsAction = machinery.SkipFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go index 779edb04b45..32d64bcf006 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller.go @@ -20,18 +20,18 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Controller{} +var _ machinery.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 + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin ControllerRuntimeVersion string @@ -53,9 +53,9 @@ func (f *Controller) SetTemplateDefaults() error { f.TemplateBody = controllerTemplate if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } else { - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error } return nil diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go index d6e6ac95af5..1dbb649a40c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers/controller_suitetest.go @@ -20,19 +20,19 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &SuiteTest{} -var _ file.Inserter = &SuiteTest{} +var _ machinery.Template = &SuiteTest{} +var _ machinery.Inserter = &SuiteTest{} // SuiteTest scaffolds the file that sets up the controller tests // nolint:maligned type SuiteTest struct { - file.TemplateMixin - file.MultiGroupMixin - file.BoilerplateMixin - file.ResourceMixin + machinery.TemplateMixin + machinery.MultiGroupMixin + machinery.BoilerplateMixin + machinery.ResourceMixin // CRDDirectoryRelativePath define the Path for the CRD CRDDirectoryRelativePath string @@ -52,8 +52,8 @@ func (f *SuiteTest) SetTemplateDefaults() error { f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), ) // If is multigroup the path needs to be ../../ since it has @@ -64,7 +64,7 @@ func (f *SuiteTest) SetTemplateDefaults() error { } if f.Force { - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile } return nil @@ -76,10 +76,10 @@ const ( ) // GetMarkers implements file.Inserter -func (f *SuiteTest) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), +func (f *SuiteTest) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), } } @@ -93,8 +93,8 @@ Expect(err).NotTo(HaveOccurred()) ) // GetCodeFragments implements file.Inserter -func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 2) +func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 2) // Generate import code fragments imports := make([]string, 0) @@ -110,10 +110,10 @@ func (f *SuiteTest) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(f.Path, importMarker)] = imports + fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go index 359a0a2ebc5..7794ab4e08c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerfile.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Dockerfile{} +var _ machinery.Template = &Dockerfile{} // Dockerfile scaffolds a file that defines the containerized build process type Dockerfile struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go index 7aa0150303d..2051adc956c 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/dockerignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &DockerIgnore{} +var _ machinery.Template = &DockerIgnore{} // DockerIgnore scaffolds a file that defines which files should be ignored by the containerized build process type DockerIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go index bbf1e92bf53..2b5ef060f98 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/gitignore.go @@ -17,14 +17,14 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GitIgnore{} +var _ machinery.Template = &GitIgnore{} // GitIgnore scaffolds a file that defines which files should be ignored by git type GitIgnore struct { - file.TemplateMixin + machinery.TemplateMixin } // SetTemplateDefaults implements file.Template diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go index fdb3ce26764..e79e17bf47a 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/gomod.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &GoMod{} +var _ machinery.Template = &GoMod{} // GoMod scaffolds a file that defines the project dependencies type GoMod struct { - file.TemplateMixin - file.RepositoryMixin + machinery.TemplateMixin + machinery.RepositoryMixin ControllerRuntimeVersion string } @@ -38,7 +38,7 @@ func (f *GoMod) SetTemplateDefaults() error { f.TemplateBody = goModTemplate - f.IfExistsAction = file.Overwrite + f.IfExistsAction = machinery.OverwriteFile return nil } diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go index 8603a6cd699..3efbe592a54 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/hack/boilerplate.go @@ -21,18 +21,18 @@ import ( "path/filepath" "time" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) // DefaultBoilerplatePath is the default path to the boilerplate file var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt") -var _ file.Template = &Boilerplate{} +var _ machinery.Template = &Boilerplate{} // Boilerplate scaffolds a file that defines the common header for the rest of the files type Boilerplate struct { - file.TemplateMixin - file.BoilerplateMixin + machinery.TemplateMixin + machinery.BoilerplateMixin // License is the License type to write License string diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go index 65863974cb7..df7abb4751f 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/main.go @@ -20,20 +20,20 @@ import ( "fmt" "path/filepath" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) const defaultMainPath = "main.go" -var _ file.Template = &Main{} +var _ machinery.Template = &Main{} // Main scaffolds a file that defines the controller manager entry point type Main struct { - file.TemplateMixin - file.BoilerplateMixin - file.DomainMixin - file.RepositoryMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.BoilerplateMixin + machinery.DomainMixin + machinery.RepositoryMixin + machinery.ComponentConfigMixin } // SetTemplateDefaults implements file.Template @@ -43,21 +43,21 @@ func (f *Main) SetTemplateDefaults() error { } f.TemplateBody = fmt.Sprintf(mainTemplate, - file.NewMarkerFor(f.Path, importMarker), - file.NewMarkerFor(f.Path, addSchemeMarker), - file.NewMarkerFor(f.Path, setupMarker), + machinery.NewMarkerFor(f.Path, importMarker), + machinery.NewMarkerFor(f.Path, addSchemeMarker), + machinery.NewMarkerFor(f.Path, setupMarker), ) return nil } -var _ file.Inserter = &MainUpdater{} +var _ machinery.Inserter = &MainUpdater{} // MainUpdater updates main.go to run Controllers type MainUpdater struct { //nolint:maligned - file.RepositoryMixin - file.MultiGroupMixin - file.ResourceMixin + machinery.RepositoryMixin + machinery.MultiGroupMixin + machinery.ResourceMixin // Flags to indicate which parts need to be included when updating the file WireResource, WireController, WireWebhook bool @@ -69,8 +69,8 @@ func (*MainUpdater) GetPath() string { } // GetIfExistsAction implements file.Builder -func (*MainUpdater) GetIfExistsAction() file.IfExistsAction { - return file.Overwrite +func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction { + return machinery.OverwriteFile } const ( @@ -80,11 +80,11 @@ const ( ) // GetMarkers implements file.Inserter -func (f *MainUpdater) GetMarkers() []file.Marker { - return []file.Marker{ - file.NewMarkerFor(defaultMainPath, importMarker), - file.NewMarkerFor(defaultMainPath, addSchemeMarker), - file.NewMarkerFor(defaultMainPath, setupMarker), +func (f *MainUpdater) GetMarkers() []machinery.Marker { + return []machinery.Marker{ + machinery.NewMarkerFor(defaultMainPath, importMarker), + machinery.NewMarkerFor(defaultMainPath, addSchemeMarker), + machinery.NewMarkerFor(defaultMainPath, setupMarker), } } @@ -123,8 +123,8 @@ const ( ) // GetCodeFragments implements file.Inserter -func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { - fragments := make(file.CodeFragmentsMap, 3) +func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { + fragments := make(machinery.CodeFragmentsMap, 3) // If resource is not being provided we are creating the file, not updating it if f.Resource == nil { @@ -170,13 +170,13 @@ func (f *MainUpdater) GetCodeFragments() file.CodeFragmentsMap { // Only store code fragments in the map if the slices are non-empty if len(imports) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, importMarker)] = imports + fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports } if len(addScheme) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme + fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme } if len(setup) != 0 { - fragments[file.NewMarkerFor(defaultMainPath, setupMarker)] = setup + fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup } return fragments diff --git a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go b/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go index 09b983942fa..787c1ef34f2 100644 --- a/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go +++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go @@ -17,15 +17,15 @@ limitations under the License. package templates import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -var _ file.Template = &Makefile{} +var _ machinery.Template = &Makefile{} // Makefile scaffolds a file that defines project management CLI commands type Makefile struct { - file.TemplateMixin - file.ComponentConfigMixin + machinery.TemplateMixin + machinery.ComponentConfigMixin // Image is controller manager image name Image string @@ -47,7 +47,7 @@ func (f *Makefile) SetTemplateDefaults() error { f.TemplateBody = makefileTemplate - f.IfExistsAction = file.Error + f.IfExistsAction = machinery.Error if f.Image == "" { f.Image = "controller:latest" diff --git a/pkg/plugins/golang/v3/scaffolds/webhook.go b/pkg/plugins/golang/v3/scaffolds/webhook.go index 20a1a0ab825..51a020e1495 100644 --- a/pkg/plugins/golang/v3/scaffolds/webhook.go +++ b/pkg/plugins/golang/v3/scaffolds/webhook.go @@ -24,15 +24,15 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/config" "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" + "sigs.k8s.io/kubebuilder/v3/pkg/plugins" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/kdefault" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/config/webhook" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) -var _ cmdutil.Scaffolder = &webhookScaffolder{} +var _ plugins.Scaffolder = &webhookScaffolder{} type webhookScaffolder struct { config config.Config @@ -46,7 +46,7 @@ type webhookScaffolder struct { } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations -func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) cmdutil.Scaffolder { +func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) plugins.Scaffolder { return &webhookScaffolder{ config: config, resource: resource, diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go index 0a31e78e91a..5151b11d65b 100644 --- a/pkg/plugins/golang/v3/webhook.go +++ b/pkg/plugins/golang/v3/webhook.go @@ -28,12 +28,13 @@ import ( pluginutil "sigs.k8s.io/kubebuilder/v3/pkg/plugin/util" goPlugin "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil" ) // defaultWebhookVersion is the default mutating/validating webhook config API version to scaffold. const defaultWebhookVersion = "v1" +var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} + type createWebhookSubcommand struct { config config.Config // For help text. @@ -41,37 +42,31 @@ type createWebhookSubcommand struct { options *goPlugin.Options - resource resource.Resource + resource *resource.Resource // force indicates that the resource should be created even if it already exists force bool } -var ( - _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} - _ cmdutil.RunOptions = &createWebhookSubcommand{} -) +func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { + p.commandName = cliMeta.CommandName -func (p *createWebhookSubcommand) UpdateContext(ctx *plugin.Context) { - ctx.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, -validating and (or) conversion webhooks. + subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting, +validating and/or conversion webhooks. ` - ctx.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for CRD of group ship, version v1beta1 - # and kind Frigate. + subcmdMeta.Examples = fmt.Sprintf(` # Create defaulting and validating webhooks for Group: ship, Version: v1beta1 + # and Kind: Frigate %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation - # Create conversion webhook for CRD of group ship, version v1beta1 and kind Frigate. + # Create conversion webhook for Group: ship, Version: v1beta1 + # and Kind: Frigate %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion -`, ctx.CommandName) - - p.commandName = ctx.CommandName +`, cliMeta.CommandName) } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { p.options = &goPlugin.Options{} - fs.StringVar(&p.options.Group, "group", "", "resource Group") - fs.StringVar(&p.options.Version, "version", "", "resource Version") - fs.StringVar(&p.options.Kind, "kind", "", "resource Kind") + fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") fs.StringVar(&p.options.WebhookVersion, "webhook-version", defaultWebhookVersion, @@ -87,24 +82,16 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { "attempt to create resource even if it already exists") } -func (p *createWebhookSubcommand) InjectConfig(c config.Config) { +func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c - // TODO: offer a flag instead of hard-coding the project-wide domain - p.options.Domain = c.GetDomain() + return nil } -func (p *createWebhookSubcommand) Run(fs machinery.Filesystem) error { - // Create the resource from the options - p.resource = p.options.NewResource(p.config) - - return cmdutil.Run(p, fs) -} +func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error { + p.resource = res -func (p *createWebhookSubcommand) Validate() error { - if err := p.options.Validate(); err != nil { - return err - } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return err @@ -130,10 +117,8 @@ func (p *createWebhookSubcommand) Validate() error { return nil } -func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) { - return scaffolds.NewWebhookScaffolder(p.config, p.resource, p.force), nil -} - -func (p *createWebhookSubcommand) PostScaffold() error { - return nil +func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error { + scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force) + scaffolder.InjectFS(fs) + return scaffolder.Scaffold() } diff --git a/pkg/plugins/internal/cmdutil/cmdutil.go b/pkg/plugins/internal/cmdutil/cmdutil.go deleted file mode 100644 index 4c752d0e966..00000000000 --- a/pkg/plugins/internal/cmdutil/cmdutil.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -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 cmdutil - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/machinery" -) - -// Scaffolder interface creates files to set up a controller manager -type Scaffolder interface { - InjectFS(filesystem machinery.Filesystem) - // Scaffold performs the scaffolding - Scaffold() error -} - -// RunOptions represent the types used to implement the different commands -type RunOptions interface { - // - Step 1: verify that the command can be run (e.g., go version, project version, arguments, ...). - Validate() error - // - Step 2: create the Scaffolder instance. - GetScaffolder() (Scaffolder, error) - // - Step 3: inject the filesystem into the Scaffolder instance. Doesn't need any method. - // - Step 4: call the Scaffold method of the Scaffolder instance. Doesn't need any method. - // - Step 5: finish the command execution. - PostScaffold() error -} - -// Run executes a command -func Run(options RunOptions, fs machinery.Filesystem) error { - // Step 1: validate - if err := options.Validate(); err != nil { - return err - } - - // Step 2: get scaffolder - scaffolder, err := options.GetScaffolder() - if err != nil { - return err - } - // Step 3: inject filesystem - scaffolder.InjectFS(fs) - // Step 4: scaffold - if scaffolder != nil { - if err := scaffolder.Scaffold(); err != nil { - return err - } - } - // Step 5: finish - if err := options.PostScaffold(); err != nil { - return err - } - - return nil -} diff --git a/pkg/plugins/golang/v2/suite_test.go b/pkg/plugins/scaffolder.go similarity index 65% rename from pkg/plugins/golang/v2/suite_test.go rename to pkg/plugins/scaffolder.go index de06fb4cf53..b738caa999a 100644 --- a/pkg/plugins/golang/v2/suite_test.go +++ b/pkg/plugins/scaffolder.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +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. @@ -14,16 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package plugins import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" ) -func TestGoPluginV2(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Go Plugin v2 Suite") +// Scaffolder interface creates files to set up a controller manager +type Scaffolder interface { + InjectFS(machinery.Filesystem) + // Scaffold performs the scaffolding + Scaffold() error } diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 4cab5423bc2..00000000000 --- a/plugins/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Kubebuilder plugins - -**Status: Experimental** - -We are developing a plugin system to kubebuilder, so that we can generate -operators that follow other patterns. - -While plugins remain experimental, you must pass the `KUBEBUILDER_ENABLE_PLUGINS=1` -environment variable to enable plugin functionality. (Any non-empty -value will work!) - -When you specify `KUBEBUILDER_ENABLE_PLUGINS=1`, a flag `--pattern` will become -available for resource generation. Specifying `--pattern=addon` will change -resource code generation to generate code that follows the addon pattern, as -being developed in the -[cluster-addons](https://github.com/kubernetes-sigs/cluster-addons) -subproject. - -The `pattern=addon` plugin is intended to serve both as an example of a plugin, -and as a real-world use case for driving development of the plugin system. We -don't intend for the plugin system to become an emacs competitor, but it must be -sufficiently flexible to support the various patterns of operators that -kubebuilder will generate. - -## Plugin model - -We intend for plugins to be packaged in a separate binary, which will be -executed by the `kubebuilder` main binary. Data will be piped to the binary via -stdin, and returned over stdout. The serialization format will likely either be -yaml or json (to be determined!). - -While we are developing this functionality though, we are developing it using an -in-process golang interface named `Plugin`, defined in -[pkg/plugin/scaffold/scaffold.go](../pkg/plugin/scaffold/scaffold.go). The interface is a -simple single-method interface that is intended to mirror the data-in / data-out -approach that will be used when executing a plugin in a separate binary. When -we have more stability of the plugin, we intend to replace the in-process -implementation with a implementation that `exec`s a plugin in a separate binary. - -The approach being prototyped is that we pass a model of the full state of the -generation world to the Plugin, which returns the full state of the generation -world after making appropriate changes. We are starting to define a `model` -package which includes a `Universe` comprising the various `File`s that are -being generated, along with the inputs like the `Boilerplate` and the `Resource` -we are currently generating. A plugin can change the `Contents` of `File`s, or -add/remove `File`s entirely. diff --git a/plugins/addon/helpers.go b/plugins/addon/helpers.go deleted file mode 100644 index 48f91e89d8d..00000000000 --- a/plugins/addon/helpers.go +++ /dev/null @@ -1,88 +0,0 @@ -package addon - -import ( - "bytes" - "fmt" - "strings" - "text/template" - - "github.com/gobuffalo/flect" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -// This file gathers functions that are likely to be useful to other -// plugins. Once we have validated they are used in more than one -// place, we can promote them to a shared location. - -// PluginFunc executes a step of Plugin -type PluginFunc func(u *model.Universe) error - -// AddFile adds the specified file to the model. -// If the file exists the function returns false and does not modify the Universe -// If the file does not exist, the function returns true and adds the file to the Universe -// If there is a problem with the file the function returns an error -func AddFile(u *model.Universe, add *file.File) (bool, error) { - p := add.Path - if p == "" { - return false, fmt.Errorf("path must be set") - } - - if _, found := u.Files[p]; found { - return false, nil - } - - u.Files[p] = add - return true, nil -} - -// ReplaceFileIfExists replaces the specified file in the model by path -// Returns true if the file was replaced. -func ReplaceFileIfExists(u *model.Universe, add *file.File) bool { - p := add.Path - if p == "" { - panic("path must be set") - } - - if _, found := u.Files[p]; found { - u.Files[p] = add - return true - } - - return false -} - -// ReplaceFile replaces the specified file in the model by path -// If the file does not exist, it returns an error -func ReplaceFile(u *model.Universe, add *file.File) error { - found := ReplaceFileIfExists(u, add) - if !found { - return fmt.Errorf("file not found %q", add.Path) - } - return nil -} - -// DefaultTemplateFunctions returns a map of template helpers -func DefaultTemplateFunctions() template.FuncMap { - return template.FuncMap{ - "title": strings.Title, - "lower": strings.ToLower, - "plural": flect.Pluralize, - } -} - -// RunTemplate parses a template -func RunTemplate(templateName, templateValue string, data interface{}, funcMap template.FuncMap) (string, error) { - t, err := template.New(templateName).Funcs(funcMap).Parse(templateValue) - if err != nil { - return "", fmt.Errorf("error building template %s: %v", templateName, err) - } - - var b bytes.Buffer - if err := t.Execute(&b, data); err != nil { - return "", fmt.Errorf("error rending template %s: %v", templateName, err) - } - - return b.String(), nil -} diff --git a/plugins/addon/manifest.go b/plugins/addon/manifest.go deleted file mode 100644 index 227f1300ef1..00000000000 --- a/plugins/addon/manifest.go +++ /dev/null @@ -1,50 +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 addon - -import ( - "path/filepath" - "strings" - - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" -) - -const exampleManifestVersion = "0.0.1" - -const exampleManifestContents = `# Placeholder manifest - replace with the manifest for your addon -` - -// ExampleManifest adds a model file for the manifest placeholder -func ExampleManifest(u *model.Universe) error { - packageName := getPackageName(u) - - m := &file.File{ - Path: filepath.Join("channels", "packages", packageName, exampleManifestVersion, "manifest.yaml"), - Contents: exampleManifestContents, - IfExistsAction: file.Skip, - } - - _, err := AddFile(u, m) - - return err -} - -// getPackageName returns the (default) name of the declarative package -func getPackageName(u *model.Universe) string { - return strings.ToLower(u.Resource.Kind) -} diff --git a/plugins/addon/plugin.go b/plugins/addon/plugin.go deleted file mode 100644 index 3da0363b929..00000000000 --- a/plugins/addon/plugin.go +++ /dev/null @@ -1,28 +0,0 @@ -package addon - -import ( - "sigs.k8s.io/kubebuilder/v3/pkg/model" -) - -// Plugin implements model.Plugin -type Plugin struct { -} - -// Pipe implements model.Plugin -func (p *Plugin) Pipe(u *model.Universe) error { - functions := []PluginFunc{ - ExampleManifest, - ExampleChannel, - ReplaceController, - ReplaceTypes, - } - - for _, fn := range functions { - if err := fn(u); err != nil { - return err - } - - } - - return nil -} diff --git a/test/e2e/v2/plugin_cluster_test.go b/test/e2e/v2/plugin_cluster_test.go index 6832259788a..e38527b9e39 100644 --- a/test/e2e/v2/plugin_cluster_test.go +++ b/test/e2e/v2/plugin_cluster_test.go @@ -66,6 +66,7 @@ var _ = Describe("kubebuilder", func() { var controllerPodName string By("init v2 project") err := kbc.Init( + "--plugins", "go/v2", "--project-version", "2", "--domain", kbc.Domain, "--fetch-deps=false") diff --git a/test/e2e/v3/generate_test.go b/test/e2e/v3/generate_test.go index e754973743f..ed4f6117097 100644 --- a/test/e2e/v3/generate_test.go +++ b/test/e2e/v3/generate_test.go @@ -34,8 +34,8 @@ func GenerateV2(kbc *utils.TestContext) { By("initializing a project") err = kbc.Init( - "--project-version", "3", "--plugins", "go/v2", + "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) @@ -129,8 +129,8 @@ func GenerateV3(kbc *utils.TestContext, crdAndWebhookVersion string) { By("initializing a project") err = kbc.Init( - "--project-version", "3", "--plugins", "go/v3", + "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index 608bc757ce0..01dc515a14f 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -54,7 +54,11 @@ function scaffold_test_project { $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --force fi - $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + if [ $project == "project-v2" ]; then + $kb create api --plugins="go/v2,declarative" --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + else + $kb create api --plugins="go/v3,declarative" --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false + fi $kb create webhook --group crew --version v1 --kind FirstMate --conversion if [ $project == "project-v3" ]; then @@ -96,13 +100,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 +117,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..236cb6a4aa7 100644 --- a/testdata/project-v2-addon/PROJECT +++ b/testdata/project-v2-addon/PROJECT @@ -1,13 +1,47 @@ domain: testproject.org +layout: go.kubebuilder.io/v2,declarative.kubebuilder.io/v1 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: Captain + version: v1 + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 + - domain: testproject.org + group: crew + kind: Admiral + version: 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-v2/api/v1/firstmate_types.go b/testdata/project-v2/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v2/api/v1/firstmate_types.go +++ b/testdata/project-v2/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 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! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v2/api/v1/zz_generated.deepcopy.go b/testdata/project-v2/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v2/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v2/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v2/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v2/channels/stable b/testdata/project-v2/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v2/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml index 499d4131a73..c2f3d91c436 100644 --- a/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v2/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,30 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + required: + - healthy type: object type: object version: v1 diff --git a/testdata/project-v2/controllers/firstmate_controller.go b/testdata/project-v2/controllers/firstmate_controller.go index 00a899419b8..fb4111d9517 100644 --- a/testdata/project-v2/controllers/firstmate_controller.go +++ b/testdata/project-v2/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "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" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v2/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.6.4/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { - _ = context.Background() - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} - // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + 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("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &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/testdata/project-v2/go.mod b/testdata/project-v2/go.mod index d12efdcca14..0d462758f6c 100644 --- a/testdata/project-v2/go.mod +++ b/testdata/project-v2/go.mod @@ -9,4 +9,5 @@ require ( k8s.io/apimachinery v0.18.6 k8s.io/client-go v0.18.6 sigs.k8s.io/controller-runtime v0.6.4 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20200522144838-848d48e5b073 ) diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT index 7a6103d2fcf..8dc17cbd77c 100644 --- a/testdata/project-v3-addon/PROJECT +++ b/testdata/project-v3-addon/PROJECT @@ -1,6 +1,22 @@ domain: testproject.org layout: - go.kubebuilder.io/v3 +- declarative.kubebuilder.io/v1 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: Captain + version: v1 + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 + - domain: testproject.org + group: crew + kind: Admiral + version: v1 projectName: project-v3-addon repo: sigs.k8s.io/kubebuilder/testdata/project-v3-addon resources: diff --git a/testdata/project-v3-config/PROJECT b/testdata/project-v3-config/PROJECT index 83e7f357591..a16b39f303d 100644 --- a/testdata/project-v3-config/PROJECT +++ b/testdata/project-v3-config/PROJECT @@ -2,6 +2,13 @@ componentConfig: true domain: testproject.org layout: - go.kubebuilder.io/v3 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 projectName: project-v3-config repo: sigs.k8s.io/kubebuilder/testdata/project-v3-config resources: diff --git a/testdata/project-v3-config/api/v1/firstmate_types.go b/testdata/project-v3-config/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v3-config/api/v1/firstmate_types.go +++ b/testdata/project-v3-config/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 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! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go b/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v3-config/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v3-config/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v3-config/channels/stable b/testdata/project-v3-config/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v3-config/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml index be577063e51..dac655c8885 100644 --- a/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v3-config/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,32 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + phase: + type: string + required: + - healthy type: object type: object served: true diff --git a/testdata/project-v3-config/config/rbac/role.yaml b/testdata/project-v3-config/config/rbac/role.yaml index 1d105256811..e6f9fed07ad 100644 --- a/testdata/project-v3-config/config/rbac/role.yaml +++ b/testdata/project-v3-config/config/rbac/role.yaml @@ -70,12 +70,6 @@ rules: - patch - update - watch -- apiGroups: - - crew.testproject.org - resources: - - firstmates/finalizers - verbs: - - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v3-config/controllers/firstmate_controller.go b/testdata/project-v3-config/controllers/firstmate_controller.go index 0263db8d5df..2f822ecd193 100644 --- a/testdata/project-v3-config/controllers/firstmate_controller.go +++ b/testdata/project-v3-config/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "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" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-config/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + 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("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &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/testdata/project-v3-config/go.mod b/testdata/project-v3-config/go.mod index 13552682304..073e09a9498 100644 --- a/testdata/project-v3-config/go.mod +++ b/testdata/project-v3-config/go.mod @@ -10,4 +10,5 @@ require ( k8s.io/apimachinery v0.19.2 k8s.io/client-go v0.19.2 sigs.k8s.io/controller-runtime v0.7.2 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210113160450-b84d99da0217 ) diff --git a/testdata/project-v3/PROJECT b/testdata/project-v3/PROJECT index 8ef4c5b14ac..5767a104d67 100644 --- a/testdata/project-v3/PROJECT +++ b/testdata/project-v3/PROJECT @@ -1,6 +1,13 @@ domain: testproject.org layout: - go.kubebuilder.io/v3 +plugins: + declarative.kubebuilder.io/v1: + resources: + - domain: testproject.org + group: crew + kind: FirstMate + version: v1 projectName: project-v3 repo: sigs.k8s.io/kubebuilder/testdata/project-v3 resources: diff --git a/testdata/project-v3/api/v1/firstmate_types.go b/testdata/project-v3/api/v1/firstmate_types.go index 99387e72e30..375efbca120 100644 --- a/testdata/project-v3/api/v1/firstmate_types.go +++ b/testdata/project-v3/api/v1/firstmate_types.go @@ -18,6 +18,7 @@ package v1 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! @@ -25,15 +26,17 @@ import ( // FirstMateSpec defines the desired state of FirstMate type FirstMateSpec struct { + addonv1alpha1.CommonSpec `json:",inline"` + addonv1alpha1.PatchSpec `json:",inline"` + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of FirstMate. Edit firstmate_types.go to remove/update - Foo string `json:"foo,omitempty"` } // FirstMateStatus defines the observed state of FirstMate type FirstMateStatus struct { + addonv1alpha1.CommonStatus `json:",inline"` + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } @@ -50,6 +53,28 @@ type FirstMate struct { Status FirstMateStatus `json:"status,omitempty"` } +var _ addonv1alpha1.CommonObject = &FirstMate{} + +func (o *FirstMate) ComponentName() string { + return "firstmate" +} + +func (o *FirstMate) CommonSpec() addonv1alpha1.CommonSpec { + return o.Spec.CommonSpec +} + +func (o *FirstMate) PatchSpec() addonv1alpha1.PatchSpec { + return o.Spec.PatchSpec +} + +func (o *FirstMate) GetCommonStatus() addonv1alpha1.CommonStatus { + return o.Status.CommonStatus +} + +func (o *FirstMate) SetCommonStatus(s addonv1alpha1.CommonStatus) { + o.Status.CommonStatus = s +} + //+kubebuilder:object:root=true // FirstMateList contains a list of FirstMate diff --git a/testdata/project-v3/api/v1/zz_generated.deepcopy.go b/testdata/project-v3/api/v1/zz_generated.deepcopy.go index aa7f8f665cc..9e7e808d608 100644 --- a/testdata/project-v3/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v3/api/v1/zz_generated.deepcopy.go @@ -207,8 +207,8 @@ func (in *FirstMate) DeepCopyInto(out *FirstMate) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate. @@ -264,6 +264,8 @@ func (in *FirstMateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) { *out = *in + out.CommonSpec = in.CommonSpec + in.PatchSpec.DeepCopyInto(&out.PatchSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec. @@ -279,6 +281,7 @@ func (in *FirstMateSpec) DeepCopy() *FirstMateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) { *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus. diff --git a/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml b/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml new file mode 100644 index 00000000000..af9a253c582 --- /dev/null +++ b/testdata/project-v3/channels/packages/firstmate/0.0.1/manifest.yaml @@ -0,0 +1 @@ +# Placeholder manifest - replace with the manifest for your addon diff --git a/testdata/project-v3/channels/stable b/testdata/project-v3/channels/stable new file mode 100644 index 00000000000..31216a4aca9 --- /dev/null +++ b/testdata/project-v3/channels/stable @@ -0,0 +1,3 @@ +# Versions for the stable channel +manifests: +- version: 0.0.1 diff --git a/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml b/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml index be577063e51..dac655c8885 100644 --- a/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml +++ b/testdata/project-v3/config/crd/bases/crew.testproject.org_firstmates.yaml @@ -36,13 +36,32 @@ spec: spec: description: FirstMateSpec defines the desired state of FirstMate properties: - foo: - description: Foo is an example field of FirstMate. Edit firstmate_types.go - to remove/update + channel: + description: 'Channel specifies a channel that can be used to resolve + a specific addon, eg: stable It will be ignored if Version is specified' + type: string + patches: + items: + type: object + type: array + version: + description: Version specifies the exact addon version to be deployed, + eg 1.2.3 It should not be specified if Channel is specified type: string type: object status: description: FirstMateStatus defines the observed state of FirstMate + properties: + errors: + items: + type: string + type: array + healthy: + type: boolean + phase: + type: string + required: + - healthy type: object type: object served: true diff --git a/testdata/project-v3/config/rbac/role.yaml b/testdata/project-v3/config/rbac/role.yaml index a02d192e82e..780572f0f90 100644 --- a/testdata/project-v3/config/rbac/role.yaml +++ b/testdata/project-v3/config/rbac/role.yaml @@ -70,12 +70,6 @@ rules: - patch - update - watch -- apiGroups: - - crew.testproject.org - resources: - - firstmates/finalizers - verbs: - - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v3/controllers/firstmate_controller.go b/testdata/project-v3/controllers/firstmate_controller.go index 51a1224eb57..e2ea33adb18 100644 --- a/testdata/project-v3/controllers/firstmate_controller.go +++ b/testdata/project-v3/controllers/firstmate_controller.go @@ -17,47 +17,73 @@ limitations under the License. package controllers import ( - "context" - "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" crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v3/api/v1" ) +var _ reconcile.Reconciler = &FirstMateReconciler{} + // FirstMateReconciler reconciles a FirstMate object type FirstMateReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + declarative.Reconciler } //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the FirstMate object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile -func (r *FirstMateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = r.Log.WithValues("firstmate", req.NamespacedName) - - // your logic here - - return ctrl.Result{}, nil -} // SetupWithManager sets up the controller with the Manager. func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&crewv1.FirstMate{}). - Complete(r) + addon.Init() + + labels := map[string]string{ + "k8s-app": "firstmate", + } + + watchLabels := declarative.SourceLabel(mgr.GetScheme()) + + if err := r.Reconciler.Init(mgr, &crewv1.FirstMate{}, + 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("firstmate-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + + // Watch for changes to FirstMate + err = c.Watch(&source.Kind{Type: &crewv1.FirstMate{}}, &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/testdata/project-v3/go.mod b/testdata/project-v3/go.mod index 92e85f54740..705bc66c458 100644 --- a/testdata/project-v3/go.mod +++ b/testdata/project-v3/go.mod @@ -10,4 +10,5 @@ require ( k8s.io/apimachinery v0.19.2 k8s.io/client-go v0.19.2 sigs.k8s.io/controller-runtime v0.7.2 + sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210113160450-b84d99da0217 )