diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc1e2479b9..5feb3ca6992 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Execute golangci-lint uses: golangci/golangci-lint-action@v2 with: - version: v1.29 # Always uses the latest patch version. + version: v1.37 # Always uses the latest patch version. only-new-issues: true # Show only new issues if it's a pull request testdata: @@ -34,7 +34,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: '^1.15' + go-version: '1.15' # This step is needed as the following one tries to remove # kustomize for each test but has no permission to do so - name: Remove pre-installed kustomize @@ -57,7 +57,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: '^1.15' + go-version: '1.15' # This step is needed as the following one tries to remove # kustomize for each test but has no permission to do so - name: Remove pre-installed kustomize @@ -80,7 +80,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v2 with: - go-version: '^1.15' + go-version: '1.15' - name: Generate the coverage output run: make test-coverage - name: Send the coverage output diff --git a/Makefile b/Makefile index 0f0e1c8cb34..5f0173b0776 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint golangci-lint: @[ -f $(GOLANGCI_LINT) ] || { \ set -e ;\ - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) v1.29.0 ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) v1.37.1 ;\ } ##@ Tests @@ -97,7 +97,7 @@ test-unit: ## Run the unit tests .PHONY: test-coverage test-coverage: ## Run unit tests creating the output to report coverage - rm -rf *.out # Remove all coverage files if exists - go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang,./pkg/plugins/internal/..." ./pkg/... + go test -race -failfast -tags=integration -coverprofile=coverage-all.out -coverpkg="./pkg/cli/...,./pkg/config/...,./pkg/internal/...,./pkg/machinery/...,./pkg/model/...,./pkg/plugin/...,./pkg/plugins/golang" ./pkg/... .PHONY: test-integration test-integration: ## Run the integration tests diff --git a/README.md b/README.md index c516995dbb1..23cceba922c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://travis-ci.org/kubernetes-sigs/kubebuilder.svg?branch=master)](https://travis-ci.org/kubernetes-sigs/kubebuilder "Travis") [![Go Report Card](https://goreportcard.com/badge/sigs.k8s.io/kubebuilder)](https://goreportcard.com/report/sigs.k8s.io/kubebuilder) +[![Coverage Status](https://coveralls.io/repos/github/kubernetes-sigs/kubebuilder/badge.svg?branch=master)](https://coveralls.io/github/kubernetes-sigs/kubebuilder?branch=master) ## Kubebuilder diff --git a/RELEASE.md b/RELEASE.md index 40199f7018f..5eda684f2fe 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,19 +2,25 @@ The Kubebuilder Project is released on an as-needed basis. The process is as follows: -1. An issue is proposing a new release with a changelog since the last release +1. An issue is proposing a new release with a changelog since the last release. You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools] to generate the notes. See [here][release-notes-generation] 1. All [OWNERS](OWNERS) must LGTM this release -1. An OWNER runs `git tag -s $VERSION` and inserts the changelog and pushes the tag with `git push $VERSION` -1. A PR needs to be created to merge `master` branch into `book-v2` to pick up the new docs. +1. An OWNER runs `git tag -s $VERSION` and pushes the tag with `git push $VERSION`. Note that after the OWNER push the tag the CI will automatically add the release notes and the assets. +1. A PR needs to be created to merge `release-X` branch into `book-vX` to pick up the new docs. 1. The release issue is closed 1. An announcement email is sent to `kubebuilder@googlegroups.com` with the subject `[ANNOUNCE] kubebuilder $VERSION is released` -Note: This process does not apply to EAP or alpha (pre-)releases which may be cut at any time for development +**Notes:** This process does not apply to EAP or alpha (pre-)releases which may be cut at any time for development and testing. +For further information about versioning and update the Kubebuilder binaries check the [versioning][release-process] doc. + ## HEAD releases The binaries releases for HEAD are available here: - [kubebuilder-release-master-head-darwin-amd64.tar.gz](https://storage.googleapis.com/kubebuilder-release/kubebuilder-release-master-head-darwin-amd64.tar.gz) - [kubebuilder-release-master-head-linux-amd64.tar.gz](https://storage.googleapis.com/kubebuilder-release/kubebuilder-release-master-head-linux-amd64.tar.gz) + +[kubebuilder-release-tools]: https://github.com/kubernetes-sigs/kubebuilder-release-tools +[release-notes-generation]: https://github.com/kubernetes-sigs/kubebuilder-release-tools/blob/master/README.md#release-notes-generation +[release-process]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#releasing \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index a91a49a7821..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,14 +31,15 @@ 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.WithCompletion, + cli.WithDefaultProjectVersion(cfgv3.Version), + cli.WithCompletion(), ) if err != nil { log.Fatal(err) 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 +} +``` diff --git a/pkg/cli/internal/config/config_suite_test.go b/doc.go similarity index 75% rename from pkg/cli/internal/config/config_suite_test.go rename to doc.go index fffe2f8bb2f..f9a0cc4d13f 100644 --- a/pkg/cli/internal/config/config_suite_test.go +++ b/doc.go @@ -14,16 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -package config +//go:generate go run github.com/markbates/pkger/cmd/pkger -import ( - "testing" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" -) - -func TestCLI(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Config Suite") -} +// Package kubebuilder contains pkged files compiled into the +// go binaries. +package kubebuilder 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/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) /* diff --git a/docs/book/src/migration/multi-group.md b/docs/book/src/migration/multi-group.md index 6527e045859..95a80543826 100644 --- a/docs/book/src/migration/multi-group.md +++ b/docs/book/src/migration/multi-group.md @@ -77,6 +77,15 @@ Also, note that the controllers will be created under `controllers/` inst That is the reason why we moved the previously generated APIs with the provided scripts in the previous steps. Remember to update the references afterwards. +For envtest to install CRDs correctly into the test environment, the relative path to the CRD directory needs to be updated accordingly in each `controllers//suite_test.go` file. We need to add additional `".."` to our CRD directory relative path as shown below. + +```go + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + } +``` + The [CronJob tutorial][cronjob-tutorial] explains each of these changes in more detail (in the context of how they're generated by KubeBuilder for single-group projects). 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/quick-start.md b/docs/book/src/quick-start.md index d0e825a2740..0da69c0b503 100644 --- a/docs/book/src/quick-start.md +++ b/docs/book/src/quick-start.md @@ -9,7 +9,7 @@ This Quick Start guide will cover: ## Prerequisites -- [go](https://golang.org/dl/) version v1.15+. +- [go](https://golang.org/dl/) version v1.15+ and < 1.16. - [docker](https://docs.docker.com/install/) version 17.03+. - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. @@ -22,7 +22,7 @@ Projects created by Kubebuilder contain a Makefile that will install tools at ve - [controller-gen](https://github.com/kubernetes-sigs/controller-tools) The versions which are defined in the `Makefile` and `go.mod` files are the versions tested and therefore is recommend to use the specified versions. - + ## Installation @@ -30,26 +30,15 @@ The versions which are defined in the `Makefile` and `go.mod` files are the vers Install [kubebuilder](https://sigs.k8s.io/kubebuilder): ```bash -os=$(go env GOOS) -arch=$(go env GOARCH) - -# download kubebuilder and extract it to tmp -curl -L https://go.kubebuilder.io/dl/2.3.1/${os}/${arch} | tar -xz -C /tmp/ -``` - -If you are using a Kubebuilder plugin version less than version `v3+`, you must configure the Kubernetes binaries required for the [envtest][envtest], run: - -```bash -# move to a long-term location and put it on your path -# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else) -sudo mv /tmp/kubebuilder_2.3.1_${os}_${arch} /usr/local/kubebuilder -export PATH=$PATH:/usr/local/kubebuilder/bin +# download kubebuilder and install locally. +curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH) +chmod +x kubebuilder && mv kubebuilder /usr/local/bin/ ``` @@ -65,25 +54,18 @@ Kubebuilder provides autocompletion support for Bash and Zsh via the command `ku Create a directory, and then run the init command inside of it to initialize a new project. Follows an example. ```bash -mkdir $GOPATH/src/example -cd $GOPATH/src/example -kubebuilder init --domain my.domain +mkdir -p ~/projects/guestbook +cd ~/projects/guestbook +kubebuilder init --domain my.domain --repo my.domain/guestbook ``` - - @@ -99,7 +81,7 @@ kubebuilder create api --group webapp --version v1 --kind Guestbook @@ -162,7 +144,7 @@ type Guestbook struct { -## Test It Out +## Test It Out You'll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or @@ -174,7 +156,7 @@ run against a remote cluster. Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). - + Install the CRDs into the cluster: ```bash @@ -216,7 +198,7 @@ make deploy IMG=/:tag If you encounter RBAC errors, you may need to grant yourself cluster-admin privileges or be logged in as admin. See [Prerequisites for using Kubernetes RBAC on GKE cluster v1.11.x and older][pre-rbc-gke] which may be your case. - + ## Uninstall CRDs @@ -234,13 +216,13 @@ UnDeploy the controller to the cluster: make undeploy ``` -## Next Step +## Next Step -Now, see the [architecture concept diagram][architecture-concept-diagram] for a better overview and follow up the [CronJob tutorial][cronjob-tutorial] to better understand how it works by developing a demo example project. +Now, see the [architecture concept diagram][architecture-concept-diagram] for a better overview and follow up the [CronJob tutorial][cronjob-tutorial] to better understand how it works by developing a demo example project. [pre-rbc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control#iam-rolebinding-bootstrap [cronjob-tutorial]: https://book.kubebuilder.io/cronjob-tutorial/cronjob-tutorial.html [GOPATH-golang-docs]: https://golang.org/doc/code.html#GOPATH -[how-to-write-go-code-golang-docs]: https://golang.org/doc/code.html +[go-module-blogpost]:https://blog.golang.org/using-go-modules [envtest]: https://book.kubebuilder.io/reference/testing/envtest.html -[architecture-concept-diagram]: architecture.md \ No newline at end of file +[architecture-concept-diagram]: architecture.md diff --git a/docs/book/src/reference/artifacts.md b/docs/book/src/reference/artifacts.md index f4a84911518..99d1b040a2d 100644 --- a/docs/book/src/reference/artifacts.md +++ b/docs/book/src/reference/artifacts.md @@ -5,12 +5,12 @@ to the main binary releases. ## Test Binaries -You can find all of the test binaries at `https://go.kubebuilder.io/test-tools`. -You can find individual test binaries at `https://go.kubebuilder.io/test-tools/${version}/${os}/${arch}`. +You can find test binary tarballs for all Kubernetes versions and host platforms at `https://go.kubebuilder.io/test-tools`. +You can find a test binary tarball for a particular Kubernetes version and host platform at `https://go.kubebuilder.io/test-tools/${version}/${os}/${arch}`. ## Container Images -You can find all container images for your os at `https://go.kubebuilder.io/images/${os}` -or at `gcr.io/kubebuilder/thirdparty-${os}`. -You can find individual container images at `https://go.kubebuilder.io/images/${os}/${version}` -or at `gcr.io/kubebuilder/thirdparty-${os}:${version}`. +You can find all container image versions for a particular platform at `https://go.kubebuilder.io/images/${os}/${arch}` +or at `gcr.io/kubebuilder/thirdparty-${os}-${arch}`. +You can find the container image for a particular version and platform at `https://go.kubebuilder.io/images/${os}/${arch}/${version}` +or at `gcr.io/kubebuilder/thirdparty-${os}-${arch}:${version}`. 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/go.mod b/go.mod index 909387bf224..0561f0a6e33 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,21 @@ module sigs.k8s.io/kubebuilder/v3 go 1.15 require ( + github.com/cloudflare/cfssl v1.5.0 // for `kubebuilder alpha config-gen` github.com/gobuffalo/flect v0.2.2 - github.com/onsi/ginkgo v1.12.0 - github.com/onsi/gomega v1.9.0 + // TODO: remove this in favor of embed once using 1.16 + github.com/markbates/pkger v0.17.1 // for `kubebuilder alpha config-gen` + github.com/onsi/ginkgo v1.15.0 + github.com/onsi/gomega v1.10.5 github.com/spf13/afero v1.2.2 - github.com/spf13/cobra v0.0.7 + github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 - golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b + golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e + // for `kubebuilder alpha config-gen` + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect + k8s.io/apimachinery v0.20.2 // for `kubebuilder alpha config-gen` + k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 // indirect + sigs.k8s.io/controller-tools v0.3.0 // for `kubebuilder alpha config-gen` + sigs.k8s.io/kustomize/kyaml v0.10.10 // for `kubebuilder alpha config-gen` sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 50ca1db89b0..05046d780a8 100644 --- a/go.sum +++ b/go.sum @@ -1,193 +1,758 @@ +bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/360EntSecGroup-Skylar/excelize v1.4.1/go.mod h1:vnax29X2usfl7HHkBrX5EvSCJcmH3dT9luvxzu8iGAE= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= +github.com/cloudflare/cfssl v1.5.0 h1:vFJDAvQgFSRbCn9zg8KpSrrEZrBAQ4KO5oNK7SXEyb0= +github.com/cloudflare/cfssl v1.5.0/go.mod h1:sPPkBS5L8l8sRc/IOO1jG51Xb34u+TYhL6P//JdODMQ= +github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= +github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5 h1:8b2ZgKfKIUTVQpTb77MoRDIMEIwvDVw40o3aOXdfYzI= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2 h1:a2kIyV3w+OS3S97zxUndRVD46+FhGOUBDFY7nmu4CsY= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4 h1:5I4CCSqoWzT+82bBkNIvmLc0UOsoKKQ4Fz+3VxOB7SY= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4 h1:csnOgcgAiuGoM/Po7PEpKDoNulCcF3FGbSnbHfxgjMI= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-openapi/validate v0.19.8 h1:YFzsdWIDfVuLvIOF+ZmKjVg1MbPJ1QgY9PihMwei1ys= +github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.2 h1:PAVD7sp0KOdfswjAw9BpLCU9hXo7wFSzgpQ+zNeks/A= github.com/gobuffalo/flect v0.2.2/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= +github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= +github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= +github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49 h1:o/c0aWEP/m6n61xlYW2QP4t9424qlJOsxugn5Zds2Rg= +github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= +github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/paulmach/orb v0.1.3/go.mod h1:VFlX/8C+IQ1p6FTRRKzKoOPJnvEtA5G0Veuqwbu//Vk= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/qri-io/starlib v0.4.2-0.20200213133954-ff2e8cd5ef8d/go.mod h1:7DPO4domFU579Ga6E61sB9VFNaniPVwJP5C4bBCu3wA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU= -github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.13.0 h1:0Tu1uzLBd1jPn4k6OnMmOPZH/l/9bj9kUOMMkoRs6Gg= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= +github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcrypto v0.0.0-20200513165325-16679db567ff/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21 h1:PIpcdSOg3pMdFJUBg5yR9xxcj5rm/SGAyaWT/wK6Kco= +github.com/zmap/zcrypto v0.0.0-20200911161511-43ff0ea04f21/go.mod h1:TxpejqcVKQjQaVVmMGfzx5HnmFMdIU+vLtaCyPBfGI4= +github.com/zmap/zlint/v2 v2.2.1 h1:b2kI/ToXX16h2wjV2c6Da65eT6aTMtkLHKetXuM9EtI= +github.com/zmap/zlint/v2 v2.2.1/go.mod h1:ixPWsdq8qLxYRpNUTbcKig3R7WgmspsHGLhCCs6rFAM= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2 h1:jxcFYjlkl8xaERsgLo+RNquI0epW6zuy/ZRQs6jnrFA= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.starlark.net v0.0.0-20190528202925-30ae18b8564f/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= +go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200124225646-8b5121be2f68/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee h1:4yd7jl+vXjalO5ztz6Vc1VADv+S/80LGJmyl1ROJ2AI= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091 h1:DMyOG0U+gKfu8JZzg2UQe9MeaC1X+xQWlAKcRnjxjCw= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b h1:AFZdJUT7jJYXQEC29hYH/WZkoV7+KhwxQGmdZ19yYoY= -golang.org/x/tools v0.0.0-20200403190813-44a64ad78b9b/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= +k8s.io/apiextensions-apiserver v0.18.2 h1:I4v3/jAuQC+89L3Z7dDgAiN4EOjN6sbm6iBqQwHTah8= +k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= +k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= +k8s.io/apimachinery v0.20.2/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= +k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= +k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmDfUM= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ= +k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= +sigs.k8s.io/controller-tools v0.3.0 h1:y3YD99XOyWaXkiF1kd41uRvfp/64teWcrEZFuHxPhJ4= +sigs.k8s.io/controller-tools v0.3.0/go.mod h1:enhtKGfxZD1GFEoMgP8Fdbu+uKQ/cq1/WGJhdVChfvI= +sigs.k8s.io/kustomize/kyaml v0.10.10 h1:caAxDDkaXZp+0kDsZVik4leFJV8LCy09PdVqpaoNeF4= +sigs.k8s.io/kustomize/kyaml v0.10.10/go.mod h1:K9yg1k/HB/6xNOf5VH3LhTo1DK9/5ykSZO5uIv+Y/1k= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/netlify.toml b/netlify.toml index 5e4403066e1..2f7374c593d 100644 --- a/netlify.toml +++ b/netlify.toml @@ -24,7 +24,7 @@ status = 301 force = true -# Go Links +# kubebuilder binary (v3+) and tarball (< v3) redirects. [[redirects]] from = "https://go.kubebuilder.io/dl/*" to = "https://go.kubebuilder.io/releases/:splat" @@ -37,9 +37,17 @@ status = 302 force = true +# Development branch redirect. [[redirects]] - from = "https://go.kubebuilder.io/releases/:version" - to = "https://github.com/kubernetes-sigs/kubebuilder/releases/v:version" + from = "https://go.kubebuilder.io/releases/master/:os/:arch" + to = "https://storage.googleapis.com/kubebuilder-release/kubebuilder_master_:os_:arch.tar.gz" + status = 302 + force = true + +# Latest redirects. +[[redirects]] + from = "https://go.kubebuilder.io/releases/latest" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/latest" status = 302 force = true @@ -50,23 +58,45 @@ force = true [[redirects]] - from = "https://go.kubebuilder.io/releases/:version/:os" - to = "https://go.kubebuilder.io/releases/:version/:os/amd64" + from = "https://go.kubebuilder.io/releases/latest/:os/:arch" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/latest/download/kubebuilder_:os_:arch" status = 302 force = true +# v1 redirects. [[redirects]] - from = "https://go.kubebuilder.io/releases/latest/:os/:arch" - to = "https://storage.googleapis.com/kubebuilder-release/kubebuilder_master_:os_:arch.tar.gz" + from = "https://go.kubebuilder.io/releases/1.:minorpatch/:os/:arch" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v1.:minorpatch/kubebuilder_1.:minorpatch_:os_:arch.tar.gz" + status = 302 + force = true + +# v2 redirects. +[[redirects]] + from = "https://go.kubebuilder.io/releases/2.:minorpatch/:os/:arch" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.:minorpatch/kubebuilder_2.:minorpatch_:os_:arch.tar.gz" + status = 302 + force = true + +# v3+ redirects. +[[redirects]] + from = "https://go.kubebuilder.io/releases/:version" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/v:version" + status = 302 + force = true + +[[redirects]] + from = "https://go.kubebuilder.io/releases/:version/:os" + to = "https://go.kubebuilder.io/releases/:version/:os/amd64" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/releases/:version/:os/:arch" - to = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v:version/kubebuilder_:version_:os_:arch.tar.gz" + to = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/v:version/kubebuilder_:os_:arch" status = 302 force = true +# Tools redirects. [[redirects]] from = "https://go.kubebuilder.io/test-tools" to = "https://storage.googleapis.com/kubebuilder-tools" @@ -74,23 +104,24 @@ force = true [[redirects]] - from = "https://go.kubebuilder.io/test-tools/:version" - to = "https://storage.googleapis.com/kubebuilder-tools/?prefix=kubebuilder-tools-:version" + from = "https://go.kubebuilder.io/test-tools/:k8sversion" + to = "https://storage.googleapis.com/kubebuilder-tools/?prefix=kubebuilder-tools-:k8sversion" status = 302 force = true [[redirects]] - from = "https://go.kubebuilder.io/test-tools/:version/:os" - to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:version-:os-amd64.tar.gz" + from = "https://go.kubebuilder.io/test-tools/:k8sversion/:os" + to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:k8sversion-:os-amd64.tar.gz" status = 302 force = true [[redirects]] - from = "https://go.kubebuilder.io/test-tools/:version/:os/:arch" - to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:version-:os-:arch.tar.gz" + from = "https://go.kubebuilder.io/test-tools/:k8sversion/:os/:arch" + to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:k8sversion-:os-:arch.tar.gz" status = 302 force = true +# Image redirects. [[redirects]] from = "https://go.kubebuilder.io/images" to = "gcr.io/kubebuilder" @@ -99,20 +130,19 @@ [[redirects]] from = "https://go.kubebuilder.io/images/:os" - to = "gcr.io/kubebuilder/thirdparty-:os" + to = "https://go.kubebuilder.io/images/:os/amd64" status = 302 force = true [[redirects]] - from = "https://go.kubebuilder.io/images/:os/:version" - to = "gcr.io/kubebuilder/thirdparty-:os::version" + from = "https://go.kubebuilder.io/images/:os/:arch" + to = "gcr.io/kubebuilder/thirdparty-:os-:arch" status = 302 force = true -# TODO(directxman12): change this to standard kustomize when the next version is released (2.1.0) [[redirects]] - from = "https://go.kubebuilder.io/kustomize/:os/:arch" - to = "https://storage.googleapis.com/kubebuilder-kustomize/kustomize_:os_:arch" + from = "https://go.kubebuilder.io/images/:os/:arch/:k8sversion" + to = "gcr.io/kubebuilder/thirdparty-:os-:arch::k8sversion" status = 302 force = true diff --git a/pkg/cli/alpha.go b/pkg/cli/alpha.go new file mode 100644 index 00000000000..c37b1a99a25 --- /dev/null +++ b/pkg/cli/alpha.go @@ -0,0 +1,81 @@ +/* +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/cobra" + configgen "sigs.k8s.io/kubebuilder/v3/pkg/cli/alpha/config-gen" +) + +const ( + alphaCommand = "alpha" +) + +var alphaCommands = []*cobra.Command{ + configgen.NewCommand(), +} + +func (c *CLI) newAlphaCmd() *cobra.Command { + alpha := &cobra.Command{ + Use: alphaCommand, + SuggestFor: []string{"experimental"}, + Short: "Alpha-stage subcommands", + Long: strings.TrimSpace(` +Alpha subcommands are for unstable features. + +- Alpha subcommands are exploratory and may be removed without warning. +- No backwards compatibility is provided for any alpha subcommands. +`), + } + for i := range alphaCommands { + alpha.AddCommand(alphaCommands[i]) + } + return alpha +} + +func (c *CLI) addAlphaCmd() { + if (len(alphaCommands) + len(c.extraAlphaCommands)) > 0 { + c.cmd.AddCommand(c.newAlphaCmd()) + } +} + +func (c *CLI) addExtraAlphaCommands() error { + // Search for the alpha subcommand + var alpha *cobra.Command + for _, subCmd := range c.cmd.Commands() { + if subCmd.Name() == alphaCommand { + alpha = subCmd + break + } + } + if alpha == nil { + return fmt.Errorf("no %q command found", alphaCommand) + } + + for _, cmd := range c.extraAlphaCommands { + for _, subCmd := range alpha.Commands() { + if cmd.Name() == subCmd.Name() { + return fmt.Errorf("command %q already exists", fmt.Sprintf("%s %s", alphaCommand, cmd.Name())) + } + } + alpha.AddCommand(cmd) + } + return nil +} diff --git a/pkg/cli/alpha/config-gen/README.md b/pkg/cli/alpha/config-gen/README.md new file mode 100644 index 00000000000..2f6e14bc4c2 --- /dev/null +++ b/pkg/cli/alpha/config-gen/README.md @@ -0,0 +1,41 @@ +# Config-gen + +`kubebuilder alpha config-gen` is a subcommand that generates configuration for kubebuilder projects as a configuration function. + +Supports: + +- Generating CRDs and RBAC from code +- Generating webhook certificates for development +- Selectively enabling / disabling components such as prometheus and webhooks + - See [types.go](apis/v1alpha1/types.go) for a list of components + +## Usage + +`config-gen` may be run as a standalone command or from kustomize as a transformer plugin. + +### Standalone command + +config-gen may be run as a standalone program on the commandline. + +See [examples/standalone](examples/standalone/README.md) + +### From kustomize + +config-gen may be run as a Kustomize plugin using kustomize. + +See [examples/kustomize](examples/kustomize/README.md) + +### Extending `config-gen` + +config-gen may be extended by composing additional functions on top of it. + +See examples of layering additional functions on: + +- [examples/basicextension](examples/basicextension/README.md) +- [examples/advancedextension](examples/advancedextension/README.md) + +## `KubebuilderConfigGen` + +See [types.go](apis/v1alpha1/types.go) for KubebuilderConfigGen schema. + +See [testdata](apis/v1alpha1/testdata) for examples of configuration options. diff --git a/pkg/cli/alpha/config-gen/cert-generation-filter.go b/pkg/cli/alpha/config-gen/cert-generation-filter.go new file mode 100644 index 00000000000..1f167a90a99 --- /dev/null +++ b/pkg/cli/alpha/config-gen/cert-generation-filter.go @@ -0,0 +1,164 @@ +/* +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 configgen + +import ( + "encoding/base64" + "fmt" + + "github.com/cloudflare/cfssl/cli/genkey" + "github.com/cloudflare/cfssl/config" + "github.com/cloudflare/cfssl/csr" + "github.com/cloudflare/cfssl/helpers" + "github.com/cloudflare/cfssl/selfsign" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &CertFilter{} + +// CertFilter generates and injects certificates into webhook +type CertFilter struct { + *KubebuilderConfigGen +} + +// Filter implements kio.Filter +func (c CertFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + + if c.Spec.Webhooks.CertificateSource.Type != "dev" { + return input, nil + } + if err := c.generateCert(); err != nil { + return nil, err + } + + s := &framework.Selector{ + Kinds: []string{ + "ValidatingWebhookConfiguration", + "MutatingWebhookConfiguration", + }, + } + matches, err := s.GetMatches(&framework.ResourceList{Items: input}) + if err != nil { + return nil, err + } + for i := range matches { + wh := matches[i].Field("webhooks") + if wh.IsNilOrEmpty() { + continue + } + err := wh.Value.VisitElements(func(node *yaml.RNode) error { + err := node.PipeE(yaml.LookupCreate(yaml.ScalarNode, "clientConfig", "caBundle"), + yaml.FieldSetter{StringValue: c.Status.CertCA}) + if err != nil { + return err + } + err = node.PipeE(yaml.LookupCreate(yaml.ScalarNode, "clientConfig", "service", "namespace"), + yaml.FieldSetter{StringValue: c.Namespace}) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + } + + s = &framework.Selector{ + Filter: func(n *yaml.RNode) bool { + // Allow-list conversion webhooks + m, _ := n.GetMeta() + if m.Kind != "CustomResourceDefinition" { + return true + } + return c.Spec.Webhooks.Conversions[m.Name] + }, + } + matches, err = s.GetMatches(&framework.ResourceList{Items: input}) + if err != nil { + return nil, err + } + for i := range matches { + err := matches[i].PipeE(yaml.LookupCreate(yaml.ScalarNode, "spec", "conversion", "strategy"), + yaml.FieldSetter{StringValue: "Webhook"}) + if err != nil { + return nil, err + } + err = matches[i].PipeE(yaml.LookupCreate( + yaml.ScalarNode, "spec", "conversion", "webhookClientConfig", "caBundle"), + yaml.FieldSetter{StringValue: c.Status.CertCA}) + if err != nil { + return nil, err + } + err = matches[i].PipeE(yaml.LookupCreate( + yaml.ScalarNode, "spec", "conversion", "webhookClientConfig", "service", "name"), + yaml.FieldSetter{StringValue: "webhook-service"}) + if err != nil { + return nil, err + } + err = matches[i].PipeE(yaml.LookupCreate( + yaml.ScalarNode, "spec", "conversion", "webhookClientConfig", "service", "namespace"), + yaml.FieldSetter{StringValue: c.Namespace}) + if err != nil { + return nil, err + } + + err = matches[i].PipeE(yaml.LookupCreate( + yaml.ScalarNode, "spec", "conversion", "webhookClientConfig", "service", "path"), + yaml.FieldSetter{StringValue: "/convert"}) + if err != nil { + return nil, err + } + } + + return input, nil +} + +func (c CertFilter) generateCert() error { + var err error + var req = csr.New() + req.Hosts = []string{ + fmt.Sprintf("webhook-service.%s.svc", c.Namespace), + fmt.Sprintf("webhook-service.%s.svc.cluster.local", c.Namespace), + } + req.CN = "kb-dev-controller-manager" + + var key, csrPEM []byte + g := &csr.Generator{Validator: genkey.Validator} + csrPEM, key, err = g.ProcessRequest(req) + if err != nil { + return err + } + priv, err := helpers.ParsePrivateKeyPEM(key) + if err != nil { + return err + } + + profile := config.DefaultConfig() + profile.Expiry = c.Spec.Webhooks.CertificateSource.DevCertificate.CertDuration + cert, err := selfsign.Sign(priv, csrPEM, profile) + if err != nil { + return err + } + + c.Status.CertCA = base64.StdEncoding.EncodeToString(cert) + c.Status.CertKey = base64.StdEncoding.EncodeToString(key) + return nil +} diff --git a/pkg/cli/alpha/config-gen/cert-manager-patches.go b/pkg/cli/alpha/config-gen/cert-manager-patches.go new file mode 100644 index 00000000000..0f4c2436709 --- /dev/null +++ b/pkg/cli/alpha/config-gen/cert-manager-patches.go @@ -0,0 +1,39 @@ +/* +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 configgen + +import ( + "github.com/markbates/pkger" + "sigs.k8s.io/kustomize/kyaml/fn/framework" +) + +// CertManagerPatchTemplate returns the PatchTemplate for cert-manager +func CertManagerPatchTemplate(_ *KubebuilderConfigGen) framework.PT { + return framework.PT{ + // keep casting -- required by pkger to find the directory + Dir: pkger.Dir("/pkg/cli/alpha/config-gen/templates/patches/cert-manager"), + Selector: func() *framework.Selector { + return &framework.Selector{ + Kinds: []string{ + "CustomResourceDefinition", + "ValidatingWebhookConfiguration", + "MutatingWebhookConfiguration", + }, + } + }, + } +} diff --git a/pkg/cli/alpha/config-gen/cmd.go b/pkg/cli/alpha/config-gen/cmd.go new file mode 100644 index 00000000000..6c0a52f44bf --- /dev/null +++ b/pkg/cli/alpha/config-gen/cmd.go @@ -0,0 +1,317 @@ +/* +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 configgen + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "github.com/markbates/pkger" + "github.com/spf13/cobra" + + // import pkged files + _ "sigs.k8s.io/kubebuilder/v3" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// NewCommand returns a new cobra command +func NewCommand() *cobra.Command { + kp := &KubebuilderConfigGen{} + + // legacy kustomize function support + legacyPlugin := os.Getenv("KUSTOMIZE_PLUGIN_CONFIG_STRING") + err := yaml.Unmarshal([]byte(legacyPlugin), kp) + if err != nil { + log.Fatal(err) + } + + // Eager check to make sure pkged templates are found. + err = pkger.Walk("/pkg/cli/alpha/config-gen/templates/resources", func(_ string, _ os.FileInfo, err error) error { + return err + }) + if err != nil { + // this shouldn't fail if it was compiled correctly + log.Fatal(err) + } + + c := framework.TemplateCommand{ + API: kp, + + MergeResources: true, // apply additional inputs as patches + + // these are run before the templates + PreProcessFilters: []kio.Filter{ + // run controller-gen libraries to generate configuration from code + ControllerGenFilter{KubebuilderConfigGen: kp}, + // inject generated certificates + CertFilter{KubebuilderConfigGen: kp}, + }, + + // generate resources + // keep casting -- required by pkger to find the directory + TemplatesFn: framework.TemplatesFromDir(pkger.Dir("/pkg/cli/alpha/config-gen/templates/resources")), + + // patch resources + PatchTemplatesFn: framework.PatchTemplatesFromDir( + CRDPatchTemplate(kp), + CertManagerPatchTemplate(kp), + ControllerManagerPatchTemplate(kp), + ), + + // perform final modifications + PostProcessFilters: []kio.Filter{ + // sort the resources + ComponentFilter{KubebuilderConfigGen: kp}, + SortFilter{KubebuilderConfigGen: kp}, + }, + }.GetCommand() + + if os.Getenv("KUSTOMIZE_FUNCTION") == "true" { + // run as part of kustomize -- read from stdin + c.Args = cobra.MinimumNArgs(0) + } else { + c.Args = cobra.MinimumNArgs(1) + } + c.RemoveCommand(c.Commands()...) + c.Use = "config-gen PROJECT_FILE [RESOURCE_PATCHES...]" + c.Version = `v0.1.0` + c.Short = `Generate configuration for controller-runtime based projects` + c.Long = strings.TrimSpace(` +config-gen programatically generates configuration for a controller-runtime based +project using the project source code (golang) and a KubebuilderConfigGen resource file. + +This is an alternative to expressing configuration as a static set of kustomize patches +in the "config" directory. + +config-gen may be used as a standalone command run against a file, as a kustomize +transformer plugin, or as a configuration function (e.g. kpt). + +config-gen uses the controller-tools generators to generate CRDs from the go source +and then generates additional resources such as the namespace, controller-manager, +webhooks, etc. + +Following is an example KubebuilderConfigGen resource used by config-gen: + + # kubebuilderconfiggen.yaml + # this resource describes how to generate configuration for a controller-runtime + # based project + apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 + kind: KubebuilderConfigGen + metadata: + name: my-project-name + spec: + controllerManager: + image: my-org-name/my-project-name:v0.1.0 + +If this file was at the project source root, config-gen could be used to emit +configuration using: + + kubebuilder alpha config-gen ./kubebuilderconfiggen.yaml + +The KubebuilderConfigGen resource has the following fields: + + apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 + kind: KubebuilderConfigGen + + metadata: + # name of the project. used in various resource names. + # required + name: project-name + + # namespace for the project + # optional -- defaults to "${metadata.name}-system" + namespace: project-namespace + + spec: + # configure how CRDs are generated + crds: + # path to go module source directory provided to controller-gen libraries + # optional -- defaults to '.' + sourceDirectory: ./relative/path + + # configure how the controller-manager is generated + controllerManager: + # image to run + image: my-org/my-project:v0.1.0 + + # if set, use component config for the controller-manager + # optional + componentConfig: + # use component config + enable: true + + # path to component config to put into a ConfigMap + configFilepath: ./path/to/componentconfig.yaml + + # configure how metrics are exposed + metrics: + # disable the auth proxy required for scraping metrics + # disable: false + + # generate prometheus ServiceMonitor resource + enableServiceMonitor: true + + # configure how webhooks are generated + # optional -- defaults to not generating webhook configuration + webhooks: + # enable will cause webhook config to be generated + enable: true + + # configures crds which use conversion webhooks + enableConversion: + # key is the name of the CRD + "bars.example.my.domain": true + + # configures where to get the certificate used for webhooks + # discriminated union + certificateSource: + # type of certificate source + # one of ["certManager", "dev", "manual"] -- defaults to "manual" + # certManager: certmanager is used to manage certificates -- requires CertManager to be installed + # dev: certificate is generated and wired into resources + # manual: no certificate is generated or wired into resources + type: "dev" + + # options for a dev certificate -- requires "dev" as the type + devCertificate: + duration: 1h +`) + c.Example = strings.TrimSpace(` +# +# As command +# +# create the kubebuilderconfiggen.yaml at project root +cat > kubebuilderconfiggen.yaml < kustomization.yaml < v4.0.0 +kustomize build --enable-alpha-plugins . + +# generate configuration from kustomize <= v4.0.0 +kustomize build --enable_alpha_plugins . +`) + + // command for installing the plugin + install := &cobra.Command{ + Use: "install-as-plugin", + Short: "Install config-gen as a kustomize plugin", + Long: fmt.Sprintf(`Write a script to %s for kustomize to locate as a plugin. +This path will be written to $XDG_CONFIG_HOME if set, otherwise $HOME. +`, pluginScriptPath), + Example: ` +kubebuilder alpha config-gen install-as-plugin +`, + RunE: func(cmd *cobra.Command, args []string) error { + hd, err := getPluginHomeDir() + if err != nil { + log.Fatal(err) + } + fullScriptPath := filepath.Join(hd, pluginScriptPath) + + // Given the script perms, this command will not be able to overwrite the plugin script file. + // That's ok, let the user handle removal to maintain security. + if info, err := os.Stat(fullScriptPath); err == nil && !info.IsDir() { + fmt.Fprintf(cmd.OutOrStdout(), "kustomize plugin configured at %s\n", fullScriptPath) + return nil + } + + fmt.Fprintf(cmd.OutOrStdout(), "writing kustomize plugin file at %s\n", fullScriptPath) + + dir, _ := filepath.Split(fullScriptPath) + if err = os.MkdirAll(dir, 0700); err != nil { + return err + } + + // r-x perms to prevent overwrite vulnerability since the script will be executed out-of-tree. + return ioutil.WriteFile(fullScriptPath, []byte(pluginScript), 0500) + }, + } + c.AddCommand(install) + + return c +} + +// Kustomize plugin execution script. +const pluginScript = `#!/bin/bash +KUSTOMIZE_FUNCTION=true kubebuilder alpha config-gen +` + +// Qualified directory containing the config-gen plugin script. Child of plugin home dir. +var pluginScriptPath = filepath.Join("kustomize", "plugin", + "kubebuilder.sigs.k8s.io", "v1alpha1", "kubebuilderconfiggen", "KubebuilderConfigGen") + +// getPluginHomeDir returns $XDG_CONFIG_HOME if set, otherwise $HOME. +func getPluginHomeDir() (string, error) { + xdg := os.Getenv("XDG_CONFIG_HOME") + if xdg == "" { + dir, err := os.UserHomeDir() + if err != nil { + return "", err + } + xdg = filepath.Join(dir, ".config") + } + return xdg, nil +} diff --git a/pkg/cli/alpha/config-gen/component-filter.go b/pkg/cli/alpha/config-gen/component-filter.go new file mode 100644 index 00000000000..0f0a8d6ae6b --- /dev/null +++ b/pkg/cli/alpha/config-gen/component-filter.go @@ -0,0 +1,59 @@ +/* +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 configgen + +import ( + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &ControllerGenFilter{} + +// ComponentFilter inserts the component config read from disk into the ConfigMap +type ComponentFilter struct { + *KubebuilderConfigGen +} + +// Filter sets the component config in the configmap +func (cf ComponentFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + if !cf.Spec.ControllerManager.ComponentConfig.Enable { + return input, nil + } + s := &framework.Selector{ + APIVersions: []string{"v1"}, + Kinds: []string{"ConfigMap"}, + Names: []string{"manager-config"}, + Namespaces: []string{cf.Namespace}, + } + matches, err := s.GetMatches(&framework.ResourceList{Items: input}) + if err != nil { + return nil, err + } + for i := range matches { + m := matches[i] + value := yaml.NewStringRNode(cf.Status.ComponentConfigString) + value.YNode().Style = yaml.LiteralStyle + err := m.PipeE( + yaml.Lookup("data", "controller_manager_config.yaml"), + yaml.FieldSetter{OverrideStyle: true, Value: value}) + if err != nil { + return nil, err + } + } + return input, nil +} diff --git a/plugins/addon/channel.go b/pkg/cli/alpha/config-gen/configgen_test.go similarity index 51% rename from plugins/addon/channel.go rename to pkg/cli/alpha/config-gen/configgen_test.go index f4804e5d528..db33b8244c4 100644 --- a/plugins/addon/channel.go +++ b/pkg/cli/alpha/config-gen/configgen_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,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package addon +package configgen_test import ( - "path/filepath" + "testing" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" + configgen "sigs.k8s.io/kubebuilder/v3/pkg/cli/alpha/config-gen" + "sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil" ) -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, +func TestNewCommand(t *testing.T) { + test := frameworktestutil.ResultsChecker{ + Command: configgen.NewCommand, + // Uncomment this line to update the testdata directory + // UpdateExpectedFromActual: true, } - - _, err := AddFile(u, m) - return err + test.Assert(t) } diff --git a/pkg/cli/alpha/config-gen/controller-gen-filter.go b/pkg/cli/alpha/config-gen/controller-gen-filter.go new file mode 100644 index 00000000000..5eeff83ddc3 --- /dev/null +++ b/pkg/cli/alpha/config-gen/controller-gen-filter.go @@ -0,0 +1,97 @@ +/* +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 configgen + +import ( + "bytes" + "fmt" + "io" + "os" + + "sigs.k8s.io/controller-tools/pkg/crd" + "sigs.k8s.io/controller-tools/pkg/genall" + "sigs.k8s.io/controller-tools/pkg/loader" + "sigs.k8s.io/controller-tools/pkg/rbac" + "sigs.k8s.io/controller-tools/pkg/webhook" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &ControllerGenFilter{} + +// ControllerGenFilter generates resources from go code using the controller-gen libraries +type ControllerGenFilter struct { + *KubebuilderConfigGen +} + +// Filter implements kio.Filter +func (cgr ControllerGenFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + gens := genall.Generators{} + + // generate CRD definitions + desclen := 40 + crdGen := genall.Generator(crd.Generator{ + TrivialVersions: true, + MaxDescLen: &desclen, + }) + gens = append(gens, &crdGen) + + // generate RBAC definitions + rbacGen := genall.Generator(rbac.Generator{ + RoleName: cgr.Namespace + "-manager-role", + }) + gens = append(gens, &rbacGen) + + // generate Webhook definitions + webhookGen := genall.Generator(webhook.Generator{}) + gens = append(gens, &webhookGen) + + // set the directory + b := bufferedGenerator{} + rt, _ := gens.ForRoots(cgr.Spec.CRDs.SourceDirectory) // ignore the spurious error + rt.OutputRules = genall.OutputRules{Default: &b} + + // run the generators + if failed := rt.Run(); failed { + fmt.Fprintln(os.Stderr, "error running controller-gen") + } + + // Parse the emitted resources + n, err := (&kio.ByteReader{Reader: &b.Buffer}).Read() + if err != nil { + return nil, errors.WrapPrefixf(err, "failed to parse controller-gen output") + } + + // add inputs after generated resources + return append(n, input...), nil +} + +// bufferedGenerator implements a genall.Generator store the output in a bytes.Buffer +type bufferedGenerator struct { + bytes.Buffer +} + +// Open implements genall.Generator +func (o *bufferedGenerator) Open(_ *loader.Package, _ string) (io.WriteCloser, error) { + return o, nil +} + +// Close implements genall.Generator +func (bufferedGenerator) Close() error { + return nil +} diff --git a/pkg/cli/alpha/config-gen/controller-manager-patches.go b/pkg/cli/alpha/config-gen/controller-manager-patches.go new file mode 100644 index 00000000000..7548ba9a820 --- /dev/null +++ b/pkg/cli/alpha/config-gen/controller-manager-patches.go @@ -0,0 +1,39 @@ +/* +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 configgen + +import ( + "github.com/markbates/pkger" + "sigs.k8s.io/kustomize/kyaml/fn/framework" +) + +// ControllerManagerPatchTemplate returns the PatchTemplate for controller-manager +func ControllerManagerPatchTemplate(kp *KubebuilderConfigGen) framework.PT { + return framework.PT{ + // keep casting -- required by pkger to find the directory + Dir: pkger.Dir("/pkg/cli/alpha/config-gen/templates/patches/controller-manager"), + Selector: func() *framework.Selector { + return &framework.Selector{ + Kinds: []string{"Deployment"}, + Namespaces: []string{kp.Namespace}, + Names: []string{"controller-manager"}, + Labels: map[string]string{"control-plane": "controller-manager"}, + TemplatizeValues: true, + } + }, + } +} diff --git a/pkg/cli/alpha/config-gen/crd-patches.go b/pkg/cli/alpha/config-gen/crd-patches.go new file mode 100644 index 00000000000..02d9aba0116 --- /dev/null +++ b/pkg/cli/alpha/config-gen/crd-patches.go @@ -0,0 +1,40 @@ +/* +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 configgen + +import ( + "github.com/markbates/pkger" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// CRDPatchTemplate returns the PatchTemplate for crd +func CRDPatchTemplate(kp *KubebuilderConfigGen) framework.PT { + return framework.PT{ + // keep casting -- required by pkger to find the directory + Dir: pkger.Dir("/pkg/cli/alpha/config-gen/templates/patches/crd"), + Selector: func() *framework.Selector { + return &framework.Selector{ + Kinds: []string{"CustomResourceDefinition"}, + Filter: func(r *yaml.RNode) bool { + m, _ := r.GetMeta() + return kp.Spec.Webhooks.Conversions[m.Name] + }, + } + }, + } +} diff --git a/pkg/cli/alpha/config-gen/examples/advancedextension/README.md b/pkg/cli/alpha/config-gen/examples/advancedextension/README.md new file mode 100644 index 00000000000..c4b2aa02a09 --- /dev/null +++ b/pkg/cli/alpha/config-gen/examples/advancedextension/README.md @@ -0,0 +1,69 @@ +# Advanced Extension Example + +This directory contains an example of extending the kubebuilder config-gen by creating +a second `kustomize` transformer plugin which is composed with `config-gen`. + + +## Prerequisites + +- [kustomize example](../kustomize/README.md) +- KubebuilderConfigGen schema [types.go](../../types.go) + +## Install the extension as a plugin + +The extension is a separate plugin composed with the `config-gen` plugin. + +```sh +# build the extension +go build -o ~/go/bin/advancedextension . + +# setup the extension plugin +export XDG_CONFIG_HOME=$HOME/.config +export KUBEBUILDER_PLUGIN=$XDG_CONFIG_HOME/kustomize/plugin/kubebuilder.sigs.k8s.io/kubebuilderconfiggenadvancedextension +mkdir -p $KUBEBUILDER_PLUGIN +cat > $KUBEBUILDER_PLUGIN/KubebuilderConfigGenAdvancedExtension < $KUBEBUILDER_PLUGIN/KubebuilderConfigGenBasicExtension < _output/config.yaml +``` + +```sh +# apply the config to a cluster +kubebuilder alpha config-gen kubebuilderconfiggen.yaml | kubectl apply -f - +``` + +## Run with patch overrides + +`config-gen` will automatically apply any additional resource files provided as patches to the output. + +```sh +kubebuilder alpha config-gen kubebuilderconfiggen.yaml patch.yaml +``` + +## Also see + +See [types.go](../../types.go) for the KubebuilderConfigGen schema. diff --git a/pkg/cli/alpha/config-gen/examples/standalone/kubebuilderproject.yaml b/pkg/cli/alpha/config-gen/examples/standalone/kubebuilderproject.yaml new file mode 100644 index 00000000000..135386690b6 --- /dev/null +++ b/pkg/cli/alpha/config-gen/examples/standalone/kubebuilderproject.yaml @@ -0,0 +1,9 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: example +spec: + crds: + sourceDirectory: ../../testdata/project/... + controllerManager: + image: example/simple:latest diff --git a/pkg/cli/alpha/config-gen/examples/standalone/patch.yaml b/pkg/cli/alpha/config-gen/examples/standalone/patch.yaml new file mode 100644 index 00000000000..e28fde427d4 --- /dev/null +++ b/pkg/cli/alpha/config-gen/examples/standalone/patch.yaml @@ -0,0 +1,7 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: example-system +spec: + replicas: 5 diff --git a/pkg/cli/alpha/config-gen/sort-filter.go b/pkg/cli/alpha/config-gen/sort-filter.go new file mode 100644 index 00000000000..11d4a3a0094 --- /dev/null +++ b/pkg/cli/alpha/config-gen/sort-filter.go @@ -0,0 +1,65 @@ +/* +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 configgen + +import ( + "math" + "sort" + "strings" + + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +var _ kio.Filter = &SortFilter{} + +// SortFilter sorts resources so they are installed in the right order +type SortFilter struct { + *KubebuilderConfigGen +} + +var order = func() map[string]int { + m := map[string]int{} + for i, k := range []string{ + "Namespace", "CustomResourceDefinition", "Role", "ClusterRole", + "RoleBinding", "ClusterRoleBinding", "Service", "Secret", "Deployment", + } { + m[k] = i + 1 + } + return m +}() + +// Filter implements kio.Filter +func (cgr SortFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + sort.Slice(input, func(i, j int) bool { + mi, _ := input[i].GetMeta() + mj, _ := input[j].GetMeta() + oi := order[mi.Kind] + if oi == 0 { + oi = math.MaxInt32 + } + oj := order[mj.Kind] + if oj == 0 { + oj = math.MaxInt32 + } + if oi != oj { + return oi < oj + } + return strings.Compare(mi.Name, mj.Name) < 0 + }) + return input, nil +} diff --git a/pkg/cli/alpha/config-gen/templates/patches/cert-manager/annotation.template.yaml b/pkg/cli/alpha/config-gen/templates/patches/cert-manager/annotation.template.yaml new file mode 100644 index 00000000000..7cea58889a0 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/patches/cert-manager/annotation.template.yaml @@ -0,0 +1,5 @@ +{{ if eq .Spec.Webhooks.CertificateSource.Type "certManager" }} +metadata: + annotations: + cert-manager.io/inject-ca-from: {{ .Namespace }}/{{ .Name }}-serving-cert +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/patches/controller-manager/01-auth-proxy.template.yaml b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/01-auth-proxy.template.yaml new file mode 100644 index 00000000000..2deffd20de1 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/01-auth-proxy.template.yaml @@ -0,0 +1,20 @@ +{{ if not .Spec.ControllerManager.Metrics.DisableAuthProxy}} +spec: + template: + spec: + containers: + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + - name: manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/patches/controller-manager/02-webhooks.template.yaml b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/02-webhooks.template.yaml new file mode 100644 index 00000000000..c9d7321d4a1 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/02-webhooks.template.yaml @@ -0,0 +1,20 @@ +{{ if .Spec.Webhooks.Enable }} +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/patches/controller-manager/99-component.template.yaml b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/99-component.template.yaml new file mode 100644 index 00000000000..2df24423ff5 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/patches/controller-manager/99-component.template.yaml @@ -0,0 +1,17 @@ +{{ if .Spec.ControllerManager.ComponentConfig.Enable }} +spec: + template: + spec: + containers: + - name: manager + args: + - "--config=controller_manager_config.yaml" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/patches/crd/conversion.template.yaml b/pkg/cli/alpha/config-gen/templates/patches/crd/conversion.template.yaml new file mode 100644 index 00000000000..1fe323f2fce --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/patches/crd/conversion.template.yaml @@ -0,0 +1,17 @@ +{{ if .Spec.Webhooks.Enable }} +spec: + conversion: + strategy: Webhook + webhookClientConfig: +{{- if eq .Spec.Webhooks.CertificateSource.Type "dev" }} + caBundle: {{ .Status.CertCA }} +{{- else }} + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== +{{- end }} + service: + namespace: {{ .Namespace }} + name: webhook-service + path: /convert +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/auth-proxy-rbac.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/auth-proxy-rbac.template.yaml new file mode 100644 index 00000000000..52b185ff71f --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/auth-proxy-rbac.template.yaml @@ -0,0 +1,29 @@ +{{ if not .Spec.ControllerManager.Metrics.DisableAuthProxy}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ .Name }}-proxy-role +rules: +- apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] +- apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Name }}-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Name }}-proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: {{ .Namespace }} +--- +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/cert-manager.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/cert-manager.template.yaml new file mode 100644 index 00000000000..914507e920c --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/cert-manager.template.yaml @@ -0,0 +1,28 @@ +{{ if eq .Spec.Webhooks.CertificateSource.Type "certManager" }} +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for +# breaking changes +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: {{ .Name }}-selfsigned-issuer + namespace: {{ .Namespace }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: {{ .Name }}-serving-cert + namespace: {{ .Namespace }} +spec: + dnsNames: + - webhook-service.{{ .Namespace }}.svc + - webhook-service.{{ .Namespace }}.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize +--- +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/component.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/component.template.yaml new file mode 100644 index 00000000000..179bb6237c3 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/component.template.yaml @@ -0,0 +1,12 @@ +{{- if .Spec.ControllerManager.ComponentConfig.Enable }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: manager-config + namespace: {{ .Namespace }} + labels: + control-plane: controller-manager +data: + controller_manager_config.yaml: "" +--- +{{ end }} \ No newline at end of file diff --git a/pkg/cli/alpha/config-gen/templates/resources/controller-manager.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/controller-manager.template.yaml new file mode 100644 index 00000000000..5f0dc4a137b --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/controller-manager.template.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: {{ .Namespace }} + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - --enable-leader-election + image: {{ .Spec.ControllerManager.Image }} + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + terminationGracePeriodSeconds: 10 +--- +{{- if .Spec.Webhooks.Enable }} +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Namespace }} + name: webhook-service + labels: + control-plane: webhook +spec: + ports: + - port: 443 + targetPort: webhook-server + selector: + control-plane: controller-manager +--- +{{- end}} +{{- if not .Spec.ControllerManager.Metrics.DisableAuthProxy}} +apiVersion: v1 +kind: Service +metadata: + namespace: {{ .Namespace }} + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/development-webhook-secret.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/development-webhook-secret.template.yaml new file mode 100644 index 00000000000..1720dc6b246 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/development-webhook-secret.template.yaml @@ -0,0 +1,11 @@ +{{- if eq .Spec.Webhooks.CertificateSource.Type "dev" }} +apiVersion: v1 +kind: Secret +metadata: + name: webhook-server-cert + namespace: {{ .Namespace }} +data: + tls.key: {{ .Status.CertKey }} + tls.crt: {{ .Status.CertCA }} +--- +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/namespace.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/namespace.template.yaml new file mode 100644 index 00000000000..86066e557d8 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/namespace.template.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: {{ .Namespace }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/prometheus.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/prometheus.template.yaml new file mode 100644 index 00000000000..a50f580e840 --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/prometheus.template.yaml @@ -0,0 +1,17 @@ +{{- if .Spec.ControllerManager.Metrics.EnableServiceMonitor }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + namespace: {{ .Namespace }} + name: controller-manager-metrics-monitor + labels: + control-plane: controller-manager +spec: + endpoints: + - path: /metrics + port: https + selector: + matchLabels: + control-plane: controller-manager +--- +{{ end }} diff --git a/pkg/cli/alpha/config-gen/templates/resources/rbac.template.yaml b/pkg/cli/alpha/config-gen/templates/resources/rbac.template.yaml new file mode 100644 index 00000000000..63e49b80a6d --- /dev/null +++ b/pkg/cli/alpha/config-gen/templates/resources/rbac.template.yaml @@ -0,0 +1,60 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ .Namespace }}-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Namespace }}-manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: {{ .Namespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Namespace }}-leader-election-role + namespace: {{ .Namespace }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Namespace }}-leader-election-rolebinding + namespace: {{ .Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Namespace }}-leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: {{ .Namespace }} +--- diff --git a/pkg/cli/alpha/config-gen/testdata/componentconfig/config.yaml b/pkg/cli/alpha/config-gen/testdata/componentconfig/config.yaml new file mode 100644 index 00000000000..68a8d7dd0ab --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/componentconfig/config.yaml @@ -0,0 +1,13 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + componentConfig: + enable: true + configFilepath: ./controller_manager_config.yaml diff --git a/pkg/cli/alpha/config-gen/testdata/componentconfig/controller_manager_config.yaml b/pkg/cli/alpha/config-gen/testdata/componentconfig/controller_manager_config.yaml new file mode 100644 index 00000000000..d1a5c33004d --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/componentconfig/controller_manager_config.yaml @@ -0,0 +1,9 @@ +apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 +kind: ControllerManagerConfig +metrics: + bindAddress: 127.0.0.1:8080 +webhook: + port: 9443 +leaderElection: + leaderElect: true + resourceName: 6858fb70.testproject.org \ No newline at end of file diff --git a/pkg/cli/alpha/config-gen/testdata/componentconfig/expected.yaml b/pkg/cli/alpha/config-gen/testdata/componentconfig/expected.yaml new file mode 100644 index 00000000000..0bcd3aa48bc --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/componentconfig/expected.yaml @@ -0,0 +1,325 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--config=controller_manager_config.yaml" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 + volumes: + - name: manager-config + configMap: + name: manager-config +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: manager-config + namespace: simple-system + labels: + control-plane: controller-manager +data: + controller_manager_config.yaml: |- + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: 6858fb70.testproject.org diff --git a/pkg/cli/alpha/config-gen/testdata/default/config.yaml b/pkg/cli/alpha/config-gen/testdata/default/config.yaml new file mode 100644 index 00000000000..74d43cfe6e4 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/default/config.yaml @@ -0,0 +1,10 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest diff --git a/pkg/cli/alpha/config-gen/testdata/default/expected.yaml b/pkg/cli/alpha/config-gen/testdata/default/expected.yaml new file mode 100644 index 00000000000..9b9ff310353 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/default/expected.yaml @@ -0,0 +1,299 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 diff --git a/pkg/cli/alpha/config-gen/testdata/disableauthproxy/config.yaml b/pkg/cli/alpha/config-gen/testdata/disableauthproxy/config.yaml new file mode 100644 index 00000000000..7dba4473715 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/disableauthproxy/config.yaml @@ -0,0 +1,12 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + metrics: + disableAuthProxy: true diff --git a/pkg/cli/alpha/config-gen/testdata/disableauthproxy/expected.yaml b/pkg/cli/alpha/config-gen/testdata/disableauthproxy/expected.yaml new file mode 100644 index 00000000000..8cf8984867b --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/disableauthproxy/expected.yaml @@ -0,0 +1,246 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - --enable-leader-election + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + terminationGracePeriodSeconds: 10 diff --git a/pkg/cli/alpha/config-gen/testdata/doc.go b/pkg/cli/alpha/config-gen/testdata/doc.go new file mode 100644 index 00000000000..91a14487c02 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/doc.go @@ -0,0 +1,27 @@ +/* +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 testdata contains input and expected output for running config-gen. +// +// To add a new test create a new directory with the test name, a config.yaml with +// the input, and an expected.yaml with the expected output. +// +// The project directory contains a sample project used as input. New sample projects +// may be added as new directories and referenced from the config.yaml. +// +// To update the testdata automatically modify ../configgen_test.go by uncommenting +// the corresponding line. +package testdata diff --git a/pkg/cli/alpha/config-gen/testdata/enablecertmanager/config.yaml b/pkg/cli/alpha/config-gen/testdata/enablecertmanager/config.yaml new file mode 100644 index 00000000000..1de154760b9 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enablecertmanager/config.yaml @@ -0,0 +1,15 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + + webhooks: + enable: true + certificateSource: + type: certManager diff --git a/pkg/cli/alpha/config-gen/testdata/enablecertmanager/expected.yaml b/pkg/cli/alpha/config-gen/testdata/enablecertmanager/expected.yaml new file mode 100644 index 00000000000..558971dbaf2 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enablecertmanager/expected.yaml @@ -0,0 +1,352 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + cert-manager.io/inject-ca-from: simple-system/simple-serving-cert + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + cert-manager.io/inject-ca-from: simple-system/simple-serving-cert + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: webhook-service + labels: + control-plane: webhook +spec: + ports: + - port: 443 + targetPort: webhook-server + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert +--- +# The following manifests contain a self-signed issuer CR and a certificate CR. +# More document can be found at https://docs.cert-manager.io +# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for +# breaking changes +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: simple-selfsigned-issuer + namespace: simple-system +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: simple-serving-cert + namespace: simple-system +spec: + dnsNames: + - webhook-service.simple-system.svc + - webhook-service.simple-system.svc.cluster.local + issuerRef: + kind: Issuer + name: selfsigned-issuer + secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize diff --git a/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/config.yaml b/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/config.yaml new file mode 100644 index 00000000000..6d7ce318579 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/config.yaml @@ -0,0 +1,15 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + + webhooks: + enable: true + conversions: + "bars.example.my.domain": true \ No newline at end of file diff --git a/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/expected.yaml b/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/expected.yaml new file mode 100644 index 00000000000..5dc9238932a --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enableconversionwebhooks/expected.yaml @@ -0,0 +1,335 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: simple-system + name: webhook-service + path: /convert +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: webhook-service + labels: + control-plane: webhook +spec: + ports: + - port: 443 + targetPort: webhook-server + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/pkg/cli/alpha/config-gen/testdata/enableprometheus/config.yaml b/pkg/cli/alpha/config-gen/testdata/enableprometheus/config.yaml new file mode 100644 index 00000000000..a2957b9cb19 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enableprometheus/config.yaml @@ -0,0 +1,13 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + metrics: + enableServiceMonitor: true + \ No newline at end of file diff --git a/pkg/cli/alpha/config-gen/testdata/enableprometheus/expected.yaml b/pkg/cli/alpha/config-gen/testdata/enableprometheus/expected.yaml new file mode 100644 index 00000000000..7047ebb504b --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enableprometheus/expected.yaml @@ -0,0 +1,314 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + namespace: simple-system + name: controller-manager-metrics-monitor + labels: + control-plane: controller-manager +spec: + endpoints: + - path: /metrics + port: https + selector: + matchLabels: + control-plane: controller-manager diff --git a/pkg/cli/alpha/config-gen/testdata/enablewebhooks/config.yaml b/pkg/cli/alpha/config-gen/testdata/enablewebhooks/config.yaml new file mode 100644 index 00000000000..a987a255174 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enablewebhooks/config.yaml @@ -0,0 +1,13 @@ +apiVersion: kubebuilder.sigs.k8s.io/v1alpha1 +kind: KubebuilderConfigGen +metadata: + name: simple +spec: + crds: + sourceDirectory: ../project/... + + controllerManager: + image: example/simple:latest + + webhooks: + enable: true diff --git a/pkg/cli/alpha/config-gen/testdata/enablewebhooks/expected.yaml b/pkg/cli/alpha/config-gen/testdata/enablewebhooks/expected.yaml new file mode 100644 index 00000000000..ad97a25e588 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/enablewebhooks/expected.yaml @@ -0,0 +1,326 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: simple-system +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: bars.example.my.domain +spec: + group: example.my.domain + names: + kind: Bar + listKind: BarList + plural: bars + singular: bar + scope: Namespaced + validation: + openAPIV3Schema: + description: Bar is the Schema for the bars API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: BarSpec defines the desired state of Bar + properties: + foo: + description: Foo is an example field of Bar. + type: string + type: object + status: + description: 'BarStatus defines the observed state of ' + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: (unknown) + creationTimestamp: null + name: foos.example.my.domain +spec: + group: example.my.domain + names: + kind: Foo + listKind: FooList + plural: foos + singular: foo + scope: Namespaced + validation: + openAPIV3Schema: + description: Foo is the Schema for the foos API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema ' + type: string + kind: + description: 'Kind is a string value representing the ' + type: string + metadata: + type: object + spec: + description: FooSpec defines the desired state of Foo + properties: + foo: + description: Foo is an example field of Foo. + type: string + type: object + status: + description: 'FooStatus defines the observed state of ' + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: simple-system-leader-election-role + namespace: simple-system +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: simple-proxy-role +rules: + - apiGroups: ["authentication.k8s.io"] + resources: + - tokenreviews + verbs: ["create"] + - apiGroups: ["authorization.k8s.io"] + resources: + - subjectaccessreviews + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: simple-system-manager-role +rules: + - apiGroups: + - example.my.domain + resources: + - bars + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - bars/status + verbs: + - get + - patch + - update + - apiGroups: + - example.my.domain + resources: + - foos + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - example.my.domain + resources: + - foos/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: simple-system-leader-election-rolebinding + namespace: simple-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: simple-system-leader-election-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-proxy-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: simple-system-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: simple-system-manager-role +subjects: + - kind: ServiceAccount + name: default + namespace: simple-system +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: metrics-service + labels: + control-plane: controller-manager +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: v1 +kind: Service +metadata: + namespace: simple-system + name: webhook-service + labels: + control-plane: webhook +spec: + ports: + - port: 443 + targetPort: webhook-server + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: simple-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - command: + - /manager + args: + - "--metrics-addr=127.0.0.1:8080" + - "--enable-leader-election" + image: example/simple:latest + name: manager + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + - name: kube-rbac-proxy + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + args: + - "--secure-listen-address=0.0.0.0:8443" + - "--upstream=http://127.0.0.1:8080/" + - "--logtostderr=true" + - "--v=10" + ports: + - containerPort: 8443 + name: https + terminationGracePeriodSeconds: 10 + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/foo_types.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/foo_types.go new file mode 100644 index 00000000000..0bf0e4803bd --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/foo_types.go @@ -0,0 +1,63 @@ +/* + + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// FooSpec defines the desired state of Foo +type FooSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Foo. Edit Foo_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// FooStatus defines the observed state of Foo +type FooStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true + +// Foo is the Schema for the foos API +type Foo struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FooSpec `json:"spec,omitempty"` + Status FooStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// FooList contains a list of Foo +type FooList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Foo `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Foo{}, &FooList{}) +} diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/groupversion_info.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/groupversion_info.go new file mode 100644 index 00000000000..f6b1f5cab03 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/groupversion_info.go @@ -0,0 +1,36 @@ +/* + + +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 v1alpha1 contains API Schema definitions for the example v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=example.my.domain +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "example.my.domain", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/zz_generated.deepcopy.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..40f10b6917d --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +// +build !ignore_autogenerated + +/* + + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Foo) DeepCopyInto(out *Foo) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Foo. +func (in *Foo) DeepCopy() *Foo { + if in == nil { + return nil + } + out := new(Foo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Foo) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FooList) DeepCopyInto(out *FooList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Foo, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooList. +func (in *FooList) DeepCopy() *FooList { + if in == nil { + return nil + } + out := new(FooList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FooList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FooSpec) DeepCopyInto(out *FooSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooSpec. +func (in *FooSpec) DeepCopy() *FooSpec { + if in == nil { + return nil + } + out := new(FooSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FooStatus) DeepCopyInto(out *FooStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FooStatus. +func (in *FooStatus) DeepCopy() *FooStatus { + if in == nil { + return nil + } + out := new(FooStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/bar_types.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/bar_types.go new file mode 100644 index 00000000000..d931e88ab55 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/bar_types.go @@ -0,0 +1,63 @@ +/* + + +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 v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// BarSpec defines the desired state of Bar +type BarSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Foo is an example field of Bar. Edit Bar_types.go to remove/update + Foo string `json:"foo,omitempty"` +} + +// BarStatus defines the observed state of Bar +type BarStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file +} + +// +kubebuilder:object:root=true + +// Bar is the Schema for the bars API +type Bar struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BarSpec `json:"spec,omitempty"` + Status BarStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BarList contains a list of Bar +type BarList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Bar `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Bar{}, &BarList{}) +} diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/groupversion_info.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/groupversion_info.go new file mode 100644 index 00000000000..088b91cfc2e --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/groupversion_info.go @@ -0,0 +1,36 @@ +/* + + +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 v1beta1 contains API Schema definitions for the example v1beta1 API group +// +kubebuilder:object:generate=true +// +groupName=example.my.domain +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "example.my.domain", Version: "v1beta1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/zz_generated.deepcopy.go b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..b05c785c1ef --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/api/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,114 @@ +// +build !ignore_autogenerated + +/* + + +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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Bar) DeepCopyInto(out *Bar) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bar. +func (in *Bar) DeepCopy() *Bar { + if in == nil { + return nil + } + out := new(Bar) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Bar) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BarList) DeepCopyInto(out *BarList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Bar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarList. +func (in *BarList) DeepCopy() *BarList { + if in == nil { + return nil + } + out := new(BarList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BarList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BarSpec) DeepCopyInto(out *BarSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarSpec. +func (in *BarSpec) DeepCopy() *BarSpec { + if in == nil { + return nil + } + out := new(BarSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BarStatus) DeepCopyInto(out *BarStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarStatus. +func (in *BarStatus) DeepCopy() *BarStatus { + if in == nil { + return nil + } + out := new(BarStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/cli/alpha/config-gen/testdata/project/controllers/bar_controller.go b/pkg/cli/alpha/config-gen/testdata/project/controllers/bar_controller.go new file mode 100644 index 00000000000..03e8503ba21 --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/controllers/bar_controller.go @@ -0,0 +1,31 @@ +/* + + +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 controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +// BarReconciler reconciles a Bar object +type BarReconciler struct{} + +// +kubebuilder:rbac:groups=example.my.domain,resources=bars,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=example.my.domain,resources=bars/status,verbs=get;update;patch + +func (r *BarReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return ctrl.Result{}, nil +} diff --git a/pkg/cli/alpha/config-gen/testdata/project/controllers/foo_controller.go b/pkg/cli/alpha/config-gen/testdata/project/controllers/foo_controller.go new file mode 100644 index 00000000000..b3ae530b38b --- /dev/null +++ b/pkg/cli/alpha/config-gen/testdata/project/controllers/foo_controller.go @@ -0,0 +1,31 @@ +/* + + +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 controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" +) + +// FooReconciler reconciles a Foo object +type FooReconciler struct{} + +// +kubebuilder:rbac:groups=example.my.domain,resources=foos,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=example.my.domain,resources=foos/status,verbs=get;update;patch + +func (r *FooReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return ctrl.Result{}, nil +} diff --git a/pkg/cli/alpha/config-gen/types.go b/pkg/cli/alpha/config-gen/types.go new file mode 100644 index 00000000000..7cdff6c5f0d --- /dev/null +++ b/pkg/cli/alpha/config-gen/types.go @@ -0,0 +1,230 @@ +/* +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 configgen + +import ( + "io/ioutil" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/kustomize/kyaml/errors" +) + +// KubebuilderConfigGen implements the API for generating configuration +type KubebuilderConfigGen struct { + metav1.TypeMeta `json:",inline" yaml:",omitempty"` + + // ObjectMeta has metadata about the object + ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + + // Spec is the configuration spec defining what configuration should be produced. + Spec KubebuilderConfigGenSpec `json:"spec,omitempty" yaml:"spec,omitempty"` + + // Status is the configuration status defined at runtime. + Status KubebuilderConfigGenStatus `json:"status,omitempty" yaml:"status,omitempty"` +} + +// ObjectMeta contains metadata about the resource +type ObjectMeta struct { + // Name is used to generate the names of resources. + Name string `json:"name,omitempty" yaml:"name,omitempty"` + + // Namespace defines the namespace for the controller resources. + // Must be a DNS_LABEL. + // More info: http://kubernetes.io/docs/user-guide/namespaces + // Defaults to "${name}-system" -- e.g. if name is "foo", then namespace defaults + // to "foo-system" + // +optional + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + + // Map of string keys and values that can be used to organize and categorize + // (scope and select) objects. May match selectors of replication controllers + // and services. + // More info: http://kubernetes.io/docs/user-guide/labels + // +optional + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + + // Annotations is an unstructured key value map stored with a resource that may be + // set by external tools to store and retrieve arbitrary metadata. They are not + // queryable and should be preserved when modifying objects. + // More info: http://kubernetes.io/docs/user-guide/annotations + // +optional + Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` +} + +// KubebuilderConfigGenSpec defines the desired configuration to be generated +type KubebuilderConfigGenSpec struct { + // CRDs configures how CRDs + related RBAC and Webhook resources are generated. + CRDs CRDs `json:"crds,omitempty" yaml:"crds,omitempty"` + + // ControllerManager configures how the controller-manager Deployment is generated. + ControllerManager ControllerManager `json:"controllerManager,omitempty" yaml:"controllerManager,omitempty"` + + // Webhooks configures how webhooks and certificates are generated. + Webhooks Webhooks `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` +} + +// CRDs configures how controller-gen is run against the project go source code in order to generate CRDs and RBAC. +type CRDs struct { + // SourceDirectory is the go project directory containing source code marked up with controller-gen tags. + // Defaults to the directory containing the KubebuilderConfigGen configuration file. + // +optional + SourceDirectory string `json:"sourceDirectory,omitempty" yaml:"sourceDirectory,omitempty"` +} + +// ControllerManager configures how the controller-manager resources are generated. +type ControllerManager struct { + // Image is the container image to run as the controller-manager. + Image string `json:"image,omitempty" yaml:"image,omitempty"` + + // Metrics configures how prometheus metrics are exposed. + Metrics Metrics `json:"metrics,omitempty" yaml:"metrics,omitempty"` + + // ComponentConfig configures how the controller-manager is configured. + // +optional + ComponentConfig ComponentConfig `json:"componentConfig,omitempty" yaml:"componentConfig,omitempty"` +} + +// Metrics configures how prometheus metrics are exposed from the controller. +type Metrics struct { + // DisableAuthProxy if set to true will disable the auth proxy + // +optional + DisableAuthProxy bool `json:"disableAuthProxy,omitempty" yaml:"disableAuthProxy,omitempty"` + + // EnableServiceMonitor if set to true with generate the prometheus ServiceMonitor resource + // +optional + EnableServiceMonitor bool `json:"enableServiceMonitor,omitempty" yaml:"enableServiceMonitor,omitempty"` +} + +// ComponentConfig configures how to setup the controller-manager to use component config rather +// than flag driven options. +type ComponentConfig struct { + // Enable if set to true will use component config rather than flags. + Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` + + // ConfigFilepath is the relative path to a file containing component config. + ConfigFilepath string `json:"configFilepath,omitempty" yaml:"configFilepath,omitempty"` +} + +// Webhooks configures how webhooks are generated. +type Webhooks struct { + // Enable if set to true will generate webhook configurations. + Enable bool `json:"enable,omitempty" yaml:"enable,omitempty"` + + // Conversions configures which resource types to enable conversion webhooks for. + // Conversion will be set in the CRDs for these resource types. + // The key is the CRD name. + // Note: This is a map rather than a list so it can be overridden when patched or merged. + Conversions map[string]bool `json:"conversions,omitempty" yaml:"conversions,omitempty"` + + // CertificateSource defines where to get the webhook certificates from. + CertificateSource CertificateSource `json:"certificateSource,omitempty" yaml:"certificateSource,omitempty"` +} + +// CertificateSource configures where to get webhook certificates from. +// It is a discriminated union. +type CertificateSource struct { + // Type is a discriminator for this union. + // One of: ["certManager", "dev", "manual"]. + // Defaults to "manual". + Type string `json:"type,omitempty" yaml:"type,omitempty"` + + // ManualCertificate requires the user to provide a certificate. + // Requires "manual" as the type. + ManualCertificate *ManualCertificate `json:"manualCertificate,omitempty" yaml:"manualCertificate,omitempty"` + + // CertManagerCertificate relies on the certificate manager operator installed separately. + // Requires "certManager" as the type. + //nolint:lll + CertManagerCertificate *CertManagerCertificate `json:"certManagerCertificate,omitempty" yaml:"certManagerCertificate,omitempty"` + + // GenerateCert will generate self signed certificate and inject it into the caBundles. + // For development only, not a production grade solution. + // Requires "dev" as the type. + DevCertificate *DevCertificate `json:"devCertificate,omitempty" yaml:"devCertificate,omitempty"` +} + +// ManualCertificate will not generate any certificate, and requires the user to manually +// specify and wire one in. +type ManualCertificate struct { + // Placeholder for future options + // TODO: Consider allowing users to specify the path to a file containing a certificate +} + +// CertManagerCertificate will generate cert-manager.io/v1 Issuer and Certificate resources. +type CertManagerCertificate struct { + // Placeholder for future options +} + +// DevCertificate generates a certificate for development purposes and wires it into the appropriate locations. +type DevCertificate struct { + // CertDuration sets the duration for the generated cert. Defaults to 1 hour. + CertDuration time.Duration `json:"certDuration,omitempty" yaml:"certDuration,omitempty"` +} + +// KubebuilderConfigGenStatus is runtime status for the api configuration. +// It is used to pass values generated at runtime (not directly specified by users) +// to templates. +type KubebuilderConfigGenStatus struct { + // CertCA is the CertCA generated at runtime. + CertCA string + + // CertKey is the CertKey generated at runtime. + CertKey string + + // ComponentConfigString is the contents of the component config file read from disk. + ComponentConfigString string +} + +// Default defaults the values +func (kp *KubebuilderConfigGen) Default() error { + // Validate the input + if kp.Name == "" { + return errors.Errorf("must specify metadata.name field") + } + if kp.Spec.ControllerManager.Image == "" { + return errors.Errorf("must specify spec.controllerManager.image field") + } + + // Perform defaulting + if kp.Namespace == "" { + kp.Namespace = kp.Name + "-system" + } + + if kp.Spec.CRDs.SourceDirectory == "" { + kp.Spec.CRDs.SourceDirectory = "./..." + } + + if kp.Spec.ControllerManager.ComponentConfig.ConfigFilepath != "" { + b, err := ioutil.ReadFile(kp.Spec.ControllerManager.ComponentConfig.ConfigFilepath) + if err != nil { + return err + } + kp.Status.ComponentConfigString = string(b) + } + + if kp.Spec.Webhooks.CertificateSource.Type == "dev" { + if kp.Spec.Webhooks.CertificateSource.DevCertificate == nil { + kp.Spec.Webhooks.CertificateSource.DevCertificate = &DevCertificate{} + } + if kp.Spec.Webhooks.CertificateSource.DevCertificate.CertDuration == 0 { + kp.Spec.Webhooks.CertificateSource.DevCertificate.CertDuration = time.Hour + } + } + + return nil +} diff --git a/pkg/cli/api.go b/pkg/cli/api.go index c858784962f..6d4f8a17969 100644 --- a/pkg/cli/api.go +++ b/pkg/cli/api.go @@ -14,80 +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" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -func (c cli) newCreateAPICmd() *cobra.Command { - ctx := c.newAPIContext() +const apiErrorMsg = "failed to create API" + +func (c CLI) newCreateAPICmd() *cobra.Command { 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 } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + c.applySubcommandHooks(cmd, subcommands, apiErrorMsg, false) - subcommand := createAPIPlugin.GetCreateAPISubcommand() - subcommand.InjectConfig(cfg.Config) - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to create API with %q", plugin.KeyFor(createAPIPlugin))) + return cmd } diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index 8c12384676b..a6cc7908b9f 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -17,16 +17,18 @@ limitations under the License. package cli import ( + "errors" "fmt" "os" "strings" + "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" - internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + yamlstore "sigs.k8s.io/kubebuilder/v3/pkg/config/store/yaml" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) @@ -34,76 +36,59 @@ 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 interacts with a command line interface. -type CLI interface { - // Run runs the CLI, usually returning an error if command line configuration - // is incorrect. - Run() error -} - -// cli defines the command line structure and interfaces that are used to -// scaffold kubebuilder project files. -type cli struct { //nolint:maligned +// CLI is the command line utility that is used to scaffold kubebuilder project files. +type CLI struct { //nolint:maligned /* Fields set by Option */ // Root command name. It is injected downstream to provide correct help, usage, examples and errors. 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 + // 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 - // Plugins registered in the cli. - plugins map[string]plugin.Plugin + // 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 - // Whether to add a completion command to the cli. + // Alpha commands injected by options. + extraAlphaCommands []*cobra.Command + // Whether to add a completion command to the CLI. completionCommand bool /* 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 // Root command. cmd *cobra.Command + + // Underlying fs + fs machinery.Filesystem } -// New creates a new cli instance. -// Developer errors (e.g. not registering any plugins, extra commands with conflicting names) return an error -// while user errors (e.g. errors while parsing flags, unresolvable plugins) create a command which return the error. -func New(opts ...Option) (CLI, error) { +// New creates a new CLI instance. +// +// It follows the functional options pattern in order to customize the resulting CLI. +// +// It returns an error if any of the provided options fails. As some processing needs +// to be done, execution errors may be found here. Instead of returning an error, this +// function will return a valid CLI that errors in Run so that help is provided to the +// user. +func New(options ...Option) (*CLI, error) { // Create the CLI. - c, err := newCLI(opts...) + c, err := newCLI(options...) if err != nil { return nil, err } @@ -119,26 +104,31 @@ func New(opts ...Option) (CLI, error) { return nil, err } + // Add extra alpha commands injected by options. + if err := c.addExtraAlphaCommands(); err != nil { + return nil, err + } + // Write deprecation notices after all commands have been constructed. c.printDeprecationWarnings() return c, nil } -// newCLI creates a default cli instance and applies the provided options. +// newCLI creates a default CLI instance and applies the provided options. // It is as a separate function for test purposes. -func newCLI(opts ...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), +func newCLI(options ...Option) (*CLI, error) { + // Default CLI options. + c := &CLI{ + commandName: "kubebuilder", + plugins: make(map[string]plugin.Plugin), + defaultPlugins: make(map[config.Version][]string), + fs: machinery.Filesystem{FS: afero.NewOsFs()}, } // Apply provided options. - for _, opt := range opts { - if err := opt(c); err != nil { + for _, option := range options { + if err := option(c); err != nil { return nil, err } } @@ -146,263 +136,248 @@ func newCLI(opts ...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 getInfoFromConfigFile() (config.Version, []string, error) { +func (c *CLI) getInfoFromConfigFile() error { // Read the project configuration file - projectConfig, err := internalconfig.Read() - switch { - case err == nil: - case os.IsNotExist(err): - return config.Version{}, nil, nil - default: - return config.Version{}, nil, err + cfg := yamlstore.New(c.fs) + if err := cfg.Load(); err != nil { + return err } - return getInfoFromConfig(projectConfig) + 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) { - // Split the comma-separated plugins - var pluginSet []string - if projectConfig.GetLayout() != "" { - for _, p := range strings.Split(projectConfig.GetLayout(), ",") { - pluginSet = append(pluginSet, strings.TrimSpace(p)) +func (c *CLI) getInfoFromConfig(projectConfig config.Config) error { + c.pluginKeys = projectConfig.GetPluginChain() + c.projectVersion = projectConfig.GetVersion() + + 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) } } - return projectConfig.GetVersion(), pluginSet, nil + return nil } -// 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) - } +// 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 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) + // 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 } - // 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 + // 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, _ := 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 } // addSubcommands returns a root command with a subcommand tree reflecting the // current project's state. -func (c *cli) addSubcommands() { +func (c *CLI) addSubcommands() { + // add the alpha command if it has any subcommands enabled + c.addAlphaCmd() + // kubebuilder completion // Only add completion if requested if c.completionCommand { @@ -431,28 +406,8 @@ 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 { +func (c *CLI) addExtraCommands() error { for _, cmd := range c.extraCommands { for _, subCmd := range c.cmd.Commands() { if cmd.Name() == subCmd.Name() { @@ -465,7 +420,7 @@ func (c *cli) addExtraCommands() error { } // printDeprecationWarnings prints the deprecation warnings of the resolved plugins. -func (c cli) printDeprecationWarnings() { +func (c CLI) printDeprecationWarnings() { for _, p := range c.resolvedPlugins { if d, isDeprecated := p.(plugin.Deprecated); isDeprecated { fmt.Printf(noticeColor, fmt.Sprintf(deprecationFmt, d.DeprecationWarning())) @@ -473,7 +428,16 @@ func (c cli) printDeprecationWarnings() { } } -// Run implements CLI.Run. -func (c cli) Run() error { +// 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. +func (c CLI) Run() error { return c.cmd.Execute() } diff --git a/pkg/cli/cli_test.go b/pkg/cli/cli_test.go index d9b7000e439..a08c2a1dd94 100644 --- a/pkg/cli/cli_test.go +++ b/pkg/cli/cli_test.go @@ -20,15 +20,20 @@ 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" "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/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 { @@ -65,8 +70,8 @@ func setPluginsFlag(value string) { setFlag(pluginsFlag, value) } -func hasSubCommand(c CLI, name string) bool { - for _, subcommand := range c.(*cli).cmd.Commands() { +func hasSubCommand(cmd *cobra.Command, name string) bool { + for _, subcommand := range cmd.Commands() { if subcommand.Name() == name { return true } @@ -75,530 +80,231 @@ func hasSubCommand(c CLI, 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 - ) + 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(len(plugins)).To(Equal(0)) - }) + 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.SetLayout("go.kubebuilder.io/v2")).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(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, - }, - } - 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", @@ -611,52 +317,87 @@ 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() { - var c CLI + var c *CLI var err error When("no option is provided", func() { @@ -672,17 +413,25 @@ 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, "version")).To(BeTrue()) + Expect(hasSubCommand(c.cmd, "version")).To(BeTrue()) }) }) 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, "completion")).To(BeTrue()) + Expect(hasSubCommand(c.cmd, "completion")).To(BeTrue()) }) }) @@ -702,25 +451,73 @@ 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()) }) }) When("providing extra commands", func() { - var extraCommand *cobra.Command + It("should create a valid CLI for non-conflicting ones", func() { + extraCommand := &cobra.Command{Use: "extra"} + 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"} + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraCommands(extraCommand), + ) + Expect(err).To(HaveOccurred()) + }) + }) + + When("providing extra alpha commands", func() { It("should create a valid CLI for non-conflicting ones", func() { - extraCommand = &cobra.Command{Use: "extra"} - c, err = New(WithExtraCommands(extraCommand)) + extraAlphaCommand := &cobra.Command{Use: "extra"} + c, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraAlphaCommands(extraAlphaCommand), + ) Expect(err).NotTo(HaveOccurred()) - Expect(hasSubCommand(c, extraCommand.Use)).To(BeTrue()) + var alpha *cobra.Command + for _, subcmd := range c.cmd.Commands() { + if subcmd.Name() == alphaCommand { + alpha = subcmd + break + } + } + Expect(alpha).NotTo(BeNil()) + Expect(hasSubCommand(alpha, extraAlphaCommand.Use)).To(BeTrue()) }) It("should return an error for conflicting ones", func() { - extraCommand = &cobra.Command{Use: "init"} - _, err = New(WithExtraCommands(extraCommand)) + extraAlphaCommand := &cobra.Command{Use: "extra"} + _, err = New( + WithPlugins(&goPluginV3.Plugin{}), + WithDefaultPlugins(projectVersion, &goPluginV3.Plugin{}), + WithExtraAlphaCommands(extraAlphaCommand, extraAlphaCommand), + ) Expect(err).To(HaveOccurred()) }) }) @@ -731,7 +528,6 @@ var _ = Describe("CLI", func() { deprecationWarning = "DEPRECATED" ) var ( - projectVersion = config.Version{Number: 2} deprecatedPlugin = newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) ) @@ -744,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 a7b51d6ee6f..5acc8ec70a2 100644 --- a/pkg/cli/cmd_helpers.go +++ b/pkg/cli/cmd_helpers.go @@ -17,14 +17,38 @@ limitations under the License. package cli import ( + "errors" "fmt" + "os" "github.com/spf13/cobra" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" + "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) { @@ -32,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 { @@ -45,17 +63,273 @@ func errCmdFunc(err error) func(*cobra.Command, []string) error { } } -// runECmdFunc returns a cobra RunE function that runs subcommand and saves the -// config, which may have been modified by subcommand. -func runECmdFunc( - c *config.Config, - subcommand plugin.Subcommand, // nolint:interfacer - msg string, +// 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 +} + +// 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 +} + +func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error { + for i, tuple := range factory.subcommands { + if tuple.skip { + continue + } + + err := cb(tuple.subcommand) + + var exitError plugin.ExitError + switch { + case err == nil: + // No error do nothing + case errors.As(err, &exitError): + // Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped + factory.subcommands[i].skip = true + fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason) + default: + // Any other error, wrap it + return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err) + } + } + + return nil +} + +// 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 { - if err := subcommand.Run(); err != nil { - return fmt.Errorf("%s: %v", 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 fmt.Errorf("%s: already initialized", factory.errorMessage) + } + + // Initialize the project configuration. + if err := factory.store.New(factory.projectVersion); err != nil { + return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err) + } + } else { + // Load the project configuration. + if err := factory.store.Load(); os.IsNotExist(err) { + return fmt.Errorf("%s: unable to find configuration file, project must be initialized", + factory.errorMessage) + } else if err != nil { + return fmt.Errorf("%s: unable to load configuration file: %w", factory.errorMessage, 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 fmt.Errorf("%s: unable to create resource: %w", factory.errorMessage, err) + } + res = options.newResource() + } + + // Inject config hook. + if err := factory.forEach(func(subcommand plugin.Subcommand) error { + if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { + return subcommand.InjectConfig(cfg) + } + return nil + }, "unable to inject the configuration to"); err != nil { + return err + } + + if res != nil { + // Inject resource hook. + if err := factory.forEach(func(subcommand plugin.Subcommand) error { + if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource { + return subcommand.InjectResource(res) + } + return nil + }, "unable to inject the resource to"); err != nil { + return err + } + + if err := res.Validate(); err != nil { + return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err) + } + } + + // Pre-scaffold hook. + if err := factory.forEach(func(subcommand plugin.Subcommand) error { + if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { + return subcommand.PreScaffold(factory.fs) + } + return nil + }, "unable to run pre-scaffold tasks of"); err != nil { + return err + } + + return nil + } +} + +// 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 { + // Scaffold hook. + if err := factory.forEach(func(subcommand plugin.Subcommand) error { + return subcommand.Scaffold(factory.fs) + }, "unable to scaffold with"); err != nil { + return err } - return c.Save() + + return nil + } +} + +// 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 { + if err := factory.store.Save(); err != nil { + return fmt.Errorf("%s: unable to save configuration file: %w", factory.errorMessage, err) + } + + // Post-scaffold hook. + if err := factory.forEach(func(subcommand plugin.Subcommand) error { + if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold { + return subcommand.PostScaffold() + } + return nil + }, "unable to run post-scaffold tasks of"); err != nil { + return err + } + + return nil } } diff --git a/pkg/cli/completion.go b/pkg/cli/completion.go index 1190d89f665..162eb7c072e 100644 --- a/pkg/cli/completion.go +++ b/pkg/cli/completion.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" ) -func (c cli) newBashCmd() *cobra.Command { +func (c CLI) newBashCmd() *cobra.Command { return &cobra.Command{ Use: "bash", Short: "Load bash completions", @@ -42,7 +42,7 @@ MacOS: } } -func (c cli) newZshCmd() *cobra.Command { +func (c CLI) newZshCmd() *cobra.Command { return &cobra.Command{ Use: "zsh", Short: "Load zsh completions", @@ -61,11 +61,7 @@ $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" } } -/* TODO: support fish code completion - At the time this comment is written, the imported spf13.cobra version does not support fish completion. - However, fish completion has been added to new spf13.cobra versions. When a new spf13.cobra version that - supports it is used, uncomment this command and add it to the base completion command. -func (c cli) newFishCmd() *cobra.Command { +func (c CLI) newFishCmd() *cobra.Command { return &cobra.Command{ Use: "fish", Short: "Load fish completions", @@ -73,15 +69,15 @@ func (c cli) newFishCmd() *cobra.Command { $ %[1]s completion fish | source # To load completions for each session, execute once: -$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish`, c.commandName), +$ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish +`, c.commandName), RunE: func(cmd *cobra.Command, cmdArgs []string) error { - return cmd.Root().GenFishCompletion(os.Stdout) + return cmd.Root().GenFishCompletion(os.Stdout, true) }, } } -*/ -func (cli) newPowerShellCmd() *cobra.Command { +func (CLI) newPowerShellCmd() *cobra.Command { return &cobra.Command{ Use: "powershell", Short: "Load powershell completions", @@ -91,7 +87,7 @@ func (cli) newPowerShellCmd() *cobra.Command { } } -func (c cli) newCompletionCmd() *cobra.Command { +func (c CLI) newCompletionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "completion", Short: "Load completions for the specified shell", @@ -102,7 +98,7 @@ Detailed instructions on how to do this for each shell are provided in their own } cmd.AddCommand(c.newBashCmd()) cmd.AddCommand(c.newZshCmd()) - // cmd.AddCommand(c.newFishCmd()) // TODO: uncomment when adding fish completion + cmd.AddCommand(c.newFishCmd()) cmd.AddCommand(c.newPowerShellCmd()) return cmd } diff --git a/pkg/cli/create.go b/pkg/cli/create.go index fa3a377d364..f698b96ceea 100644 --- a/pkg/cli/create.go +++ b/pkg/cli/create.go @@ -20,7 +20,7 @@ import ( "github.com/spf13/cobra" ) -func (cli) newCreateCmd() *cobra.Command { +func (CLI) newCreateCmd() *cobra.Command { return &cobra.Command{ Use: "create", SuggestFor: []string{"new"}, diff --git a/pkg/cli/doc.go b/pkg/cli/doc.go new file mode 100644 index 00000000000..6e2f728405a --- /dev/null +++ b/pkg/cli/doc.go @@ -0,0 +1,22 @@ +/* +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 cli provides the required tools to build a CLI utility that creates +// scaffolds for operator projects. +// +// It is the entrypoint for any CLI that wants to use kubebuilder's scaffolding +// capabilities. +package cli diff --git a/pkg/cli/edit.go b/pkg/cli/edit.go index a5908a08364..8f867790410 100644 --- a/pkg/cli/edit.go +++ b/pkg/cli/edit.go @@ -14,80 +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" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -func (c cli) newEditCmd() *cobra.Command { - ctx := c.newEditContext() +const editErrorMsg = "failed to edit project" + +func (c CLI) newEditCmd() *cobra.Command { 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 } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + c.applySubcommandHooks(cmd, subcommands, editErrorMsg, false) - subcommand := editPlugin.GetEditSubcommand() - subcommand.InjectConfig(cfg.Config) - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to edit project with %q", plugin.KeyFor(editPlugin))) + return cmd } diff --git a/pkg/cli/init.go b/pkg/cli/init.go index 92997491bc2..f52e3e5f429 100644 --- a/pkg/cli/init.go +++ b/pkg/cli/init.go @@ -18,58 +18,64 @@ package cli import ( "fmt" - "log" - "os" "sort" "strconv" "strings" "github.com/spf13/cobra" - internalconfig "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/config" - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -func (c cli) newInitCmd() *cobra.Command { - ctx := c.newInitContext() +const initErrorMsg = "failed to initialize project" + +func (c CLI) newInitCmd() *cobra.Command { 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 { +func (c CLI) getInitHelpExamples() string { var sb strings.Builder for _, version := range c.getAvailableProjectVersions() { rendered := fmt.Sprintf(` # Help for initializing a project with version %[2]s @@ -82,7 +88,7 @@ func (c cli) getInitHelpExamples() string { return strings.TrimSuffix(sb.String(), "\n\n") } -func (c cli) getAvailableProjectVersions() (projectVersions []string) { +func (c CLI) getAvailableProjectVersions() (projectVersions []string) { versionSet := make(map[config.Version]struct{}) for _, p := range c.plugins { // Only return versions of non-deprecated plugins. @@ -98,66 +104,3 @@ func (c cli) getAvailableProjectVersions() (projectVersions []string) { sort.Strings(projectVersions) return projectVersions } - -func (c cli) getAvailablePlugins() (pluginKeys []string) { - for key, p := range c.plugins { - // Only return non-deprecated plugins. - if _, isDeprecated := p.(plugin.Deprecated); !isDeprecated { - pluginKeys = append(pluginKeys, strconv.Quote(key)) - } - } - 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 - } - - cfg, err := internalconfig.New(c.projectVersion, internalconfig.DefaultPath) - if err != nil { - cmdErr(cmd, fmt.Errorf("unable to initialize the project configuration: %w", err)) - return - } - - subcommand := initPlugin.GetInitSubcommand() - subcommand.InjectConfig(cfg.Config) - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - cmd.RunE = func(*cobra.Command, []string) error { - // Check if a config is initialized in the command runner so the check - // doesn't erroneously fail other commands used in initialized projects. - _, err := internalconfig.Read() - if err == nil || os.IsExist(err) { - log.Fatal("config already initialized") - } - if err := subcommand.Run(); err != nil { - return fmt.Errorf("failed to initialize project with %q: %v", plugin.KeyFor(initPlugin), err) - } - return cfg.Save() - } -} diff --git a/pkg/cli/internal/config/config.go b/pkg/cli/internal/config/config.go deleted file mode 100644 index 0bcbd4ba014..00000000000 --- a/pkg/cli/internal/config/config.go +++ /dev/null @@ -1,193 +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 config - -import ( - "errors" - "fmt" - "os" - - "github.com/spf13/afero" - "sigs.k8s.io/yaml" - - "sigs.k8s.io/kubebuilder/v3/pkg/config" -) - -const ( - // DefaultPath is the default path for the configuration file - DefaultPath = "PROJECT" -) - -func exists(fs afero.Fs, path string) (bool, error) { - // Look up the file - _, err := fs.Stat(path) - - // If we could find it the file exists - if err == nil || os.IsExist(err) { - return true, nil - } - - // Not existing and different errors are differentiated - if os.IsNotExist(err) { - err = nil - } - return false, err -} - -type versionedConfig struct { - Version config.Version -} - -func readFrom(fs afero.Fs, path string) (config.Config, error) { - // Read the file - in, err := afero.ReadFile(fs, path) //nolint:gosec - if err != nil { - return nil, err - } - - // Check the file version - var versioned versionedConfig - if err := yaml.Unmarshal(in, &versioned); err != nil { - return nil, err - } - - // Create the config object - var c config.Config - c, err = config.New(versioned.Version) - if err != nil { - return nil, err - } - - // Unmarshal the file content - if err := c.Unmarshal(in); err != nil { - return nil, err - } - - return c, nil -} - -// Read obtains the configuration from the default path but doesn't allow to persist changes -func Read() (config.Config, error) { - return ReadFrom(DefaultPath) -} - -// ReadFrom obtains the configuration from the provided path but doesn't allow to persist changes -func ReadFrom(path string) (config.Config, error) { - return readFrom(afero.NewOsFs(), path) -} - -// Config extends model/config.Config allowing to persist changes -// NOTE: the existence of Config structs in both model and internal packages is to guarantee that kubebuilder -// is the only project that can modify the file, while plugins can still receive the configuration -type Config struct { - config.Config - - // path stores where the config should be saved to - path string - // mustNotExist requires the file not to exist when saving it - mustNotExist bool - // fs is for testing. - fs afero.Fs -} - -// New creates a new configuration that will be stored at the provided path -func New(version config.Version, path string) (*Config, error) { - cfg, err := config.New(version) - if err != nil { - return nil, err - } - - return &Config{ - Config: cfg, - path: path, - mustNotExist: true, - fs: afero.NewOsFs(), - }, nil -} - -// Load obtains the configuration from the default path allowing to persist changes (Save method) -func Load() (*Config, error) { - return LoadFrom(DefaultPath) -} - -// LoadInitialized calls Load() but returns helpful error messages if the config -// does not exist. -func LoadInitialized() (*Config, error) { - c, err := Load() - if os.IsNotExist(err) { - return nil, errors.New("unable to find configuration file, project must be initialized") - } - return c, err -} - -// LoadFrom obtains the configuration from the provided path allowing to persist changes (Save method) -func LoadFrom(path string) (*Config, error) { - fs := afero.NewOsFs() - c, err := readFrom(fs, path) - return &Config{Config: c, path: path, fs: fs}, err -} - -// Save saves the configuration information -func (c Config) Save() error { - if c.fs == nil { - c.fs = afero.NewOsFs() - } - // If path is unset, it was created directly with `Config{}` - if c.path == "" { - return saveError{errors.New("no information where it should be stored, " + - "use one of the constructors (`New`, `Load` or `LoadFrom`) to create Config instances")} - } - - // If it is a new configuration, the path should not exist yet - if c.mustNotExist { - // Lets check that the file doesn't exist - alreadyExists, err := exists(c.fs, c.path) - if err != nil { - return saveError{err} - } - if alreadyExists { - return saveError{errors.New("configuration already exists in the provided path")} - } - } - - // Marshall into YAML - content, err := c.Marshal() - if err != nil { - return saveError{err} - } - - // Write the marshalled configuration - err = afero.WriteFile(c.fs, c.path, content, 0600) - if err != nil { - return saveError{fmt.Errorf("failed to save configuration to %s: %v", c.path, err)} - } - - return nil -} - -// Path returns the path for configuration file -func (c Config) Path() string { - return c.path -} - -type saveError struct { - err error -} - -func (e saveError) Error() string { - return fmt.Sprintf("unable to save the configuration: %v", e.err) -} diff --git a/pkg/cli/internal/config/config_test.go b/pkg/cli/internal/config/config_test.go deleted file mode 100644 index fe7d7ee4932..00000000000 --- a/pkg/cli/internal/config/config_test.go +++ /dev/null @@ -1,71 +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 config - -import ( - "os" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/spf13/afero" - - cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" -) - -var _ = Describe("Config", func() { - Context("Save", func() { - It("should success for valid configs", func() { - cfg := Config{ - Config: cfgv2.New(), - fs: afero.NewMemMapFs(), - path: DefaultPath, - } - Expect(cfg.Save()).To(Succeed()) - - cfgBytes, err := afero.ReadFile(cfg.fs, DefaultPath) - Expect(err).NotTo(HaveOccurred()) - Expect(string(cfgBytes)).To(Equal(`version: "2" -`)) - }) - - It("should fail if path is not provided", func() { - cfg := Config{ - Config: cfgv2.New(), - fs: afero.NewMemMapFs(), - } - Expect(cfg.Save()).NotTo(Succeed()) - }) - }) - - Context("readFrom", func() { - It("should success for valid configs", func() { - configStr := `domain: example.com -repo: github.com/example/project -version: "2"` - expectedConfig := cfgv2.New() - _ = expectedConfig.SetDomain("example.com") - _ = expectedConfig.SetRepository("github.com/example/project") - - fs := afero.NewMemMapFs() - Expect(afero.WriteFile(fs, DefaultPath, []byte(configStr), os.ModePerm)).To(Succeed()) - - cfg, err := readFrom(fs, DefaultPath) - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).To(Equal(expectedConfig)) - }) - }) -}) diff --git a/pkg/cli/options.go b/pkg/cli/options.go index df89dd5c649..4ed0dce5c75 100644 --- a/pkg/cli/options.go +++ b/pkg/cli/options.go @@ -25,42 +25,51 @@ import ( "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -// Option is a function that can configure the cli -type Option func(*cli) error +// Option is a function used as arguments to New in order to configure the resulting CLI. +type Option func(*CLI) error -// WithCommandName is an Option that sets the cli's root command name. +// WithCommandName is an Option that sets the CLI's root command name. func WithCommandName(name string) Option { - return func(c *cli) error { + return func(c *CLI) error { c.commandName = name return nil } } -// WithVersion is an Option that defines the version string of the cli. +// WithVersion is an Option that defines the version string of the CLI. func WithVersion(version string) Option { - return func(c *cli) error { + return func(c *CLI) error { c.version = version return nil } } -// WithDefaultProjectVersion is an Option that sets the cli's default project version. -// Setting an unknown version will result in an error. -func WithDefaultProjectVersion(version config.Version) 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) +// WithPlugins is an Option that sets the CLI's plugins. +// +// Specifying any invalid plugin results in an error. +func WithPlugins(plugins ...plugin.Plugin) 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 } - c.defaultProjectVersion = version return nil } } -// WithDefaultPlugins is an Option that sets the cli's default plugins. +// WithDefaultPlugins is an Option that sets the CLI's default plugins. +// +// Specifying any invalid plugin results in an error. func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { - return func(c *cli) error { + 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) @@ -78,34 +87,47 @@ func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) } } -// WithPlugins is an Option that sets the cli's plugins. -func WithPlugins(plugins ...plugin.Plugin) 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 +// WithDefaultProjectVersion is an Option that sets the CLI's default project version. +// +// Setting an invalid version results in an error. +func WithDefaultProjectVersion(version config.Version) 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) } + c.defaultProjectVersion = version return nil } } -// WithExtraCommands is an Option that adds extra subcommands to the cli. +// WithExtraCommands is an Option that adds extra subcommands to the CLI. +// // Adding extra commands that duplicate existing commands results in an error. func WithExtraCommands(cmds ...*cobra.Command) Option { - return func(c *cli) error { + return func(c *CLI) error { + // We don't know the commands defined by the CLI yet so we are not checking if the extra commands + // conflict with a pre-existing one yet. We do this after creating the base commands. c.extraCommands = append(c.extraCommands, cmds...) return nil } } +// WithExtraAlphaCommands is an Option that adds extra alpha subcommands to the CLI. +// +// Adding extra alpha commands that duplicate existing commands results in an error. +func WithExtraAlphaCommands(cmds ...*cobra.Command) Option { + return func(c *CLI) error { + // We don't know the commands defined by the CLI yet so we are not checking if the extra alpha commands + // conflict with a pre-existing one yet. We do this after creating the base commands. + c.extraAlphaCommands = append(c.extraAlphaCommands, cmds...) + return nil + } +} + // WithCompletion is an Option that adds the completion subcommand. -func WithCompletion(c *cli) error { - c.completionCommand = true - return nil +func WithCompletion() Option { + return func(c *CLI) error { + c.completionCommand = true + return nil + } } diff --git a/pkg/cli/options_test.go b/pkg/cli/options_test.go index 88ebe1b4b2c..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" @@ -36,7 +35,7 @@ var _ = Describe("CLI options", func() { ) var ( - c *cli + c *CLI err error projectVersion = config.Version{Number: 1} @@ -68,144 +67,138 @@ 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() { It("should return a valid CLI with extra commands", func() { commandTest := &cobra.Command{ @@ -221,6 +214,21 @@ var _ = Describe("CLI options", func() { }) }) + Context("WithExtraAlphaCommands", func() { + It("should return a valid CLI with extra alpha commands", func() { + commandTest := &cobra.Command{ + Use: "example", + } + c, err = newCLI(WithExtraAlphaCommands(commandTest)) + Expect(err).NotTo(HaveOccurred()) + Expect(c).NotTo(BeNil()) + Expect(c.extraAlphaCommands).NotTo(BeNil()) + Expect(len(c.extraAlphaCommands)).To(Equal(1)) + Expect(c.extraAlphaCommands[0]).NotTo(BeNil()) + Expect(c.extraAlphaCommands[0].Use).To(Equal(commandTest.Use)) + }) + }) + Context("WithCompletion", func() { It("should not add the completion command by default", func() { c, err = newCLI() @@ -230,7 +238,7 @@ var _ = Describe("CLI options", func() { }) It("should add the completion command if requested", func() { - c, err = newCLI(WithCompletion) + c, err = newCLI(WithCompletion()) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.completionCommand).To(BeTrue()) 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 e0baa398180..93d0c0e863d 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -29,7 +29,7 @@ const ( projectVersionsHeader = "Supported project versions" ) -func (c cli) newRootCmd() *cobra.Command { +func (c CLI) newRootCmd() *cobra.Command { cmd := &cobra.Command{ Use: c.commandName, Long: `CLI tool for building Kubernetes extensions and tools. @@ -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 -func (c cli) rootExamples() string { +// 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,27 +65,25 @@ 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 } // getPluginTable returns an ASCII table of the available plugins and their supported project versions. -func (c cli) getPluginTable() string { +func (c CLI) getPluginTable() string { var ( maxPluginKeyLength = len(pluginKeysHeader) pluginKeys = make([]string, 0, len(c.plugins)) 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/version.go b/pkg/cli/version.go index 9bcbfa48b29..ad16d615cd8 100644 --- a/pkg/cli/version.go +++ b/pkg/cli/version.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" ) -func (c cli) newVersionCmd() *cobra.Command { +func (c CLI) newVersionCmd() *cobra.Command { return &cobra.Command{ Use: "version", Short: fmt.Sprintf("Print the %s version", c.commandName), diff --git a/pkg/cli/webhook.go b/pkg/cli/webhook.go index fb2e4511a66..bea5b0b17a7 100644 --- a/pkg/cli/webhook.go +++ b/pkg/cli/webhook.go @@ -14,80 +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" - "sigs.k8s.io/kubebuilder/v3/pkg/cli/internal/config" "sigs.k8s.io/kubebuilder/v3/pkg/plugin" ) -func (c cli) newCreateWebhookCmd() *cobra.Command { - ctx := c.newWebhookContext() +const webhookErrorMsg = "failed to create webhook" + +func (c CLI) newCreateWebhookCmd() *cobra.Command { 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 } - cfg, err := config.LoadInitialized() - if err != nil { - cmdErr(cmd, err) - return - } + c.applySubcommandHooks(cmd, subcommands, webhookErrorMsg, false) - subcommand := createWebhookPlugin.GetCreateWebhookSubcommand() - subcommand.InjectConfig(cfg.Config) - subcommand.BindFlags(cmd.Flags()) - subcommand.UpdateContext(&ctx) - cmd.Long = ctx.Description - cmd.Example = ctx.Examples - cmd.RunE = runECmdFunc(cfg, subcommand, - fmt.Sprintf("failed to create webhook with %q", plugin.KeyFor(createWebhookPlugin))) + return cmd } diff --git a/pkg/config/interface.go b/pkg/config/interface.go index 8621d5f8afd..3d0e0e2c110 100644 --- a/pkg/config/interface.go +++ b/pkg/config/interface.go @@ -20,79 +20,79 @@ 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 - // IsCRDVersionCompatible returns true if crdVersion can be added to the existing set of CRD versions. - IsCRDVersionCompatible(crdVersion string) bool - // IsWebhookVersionCompatible returns true if webhookVersion can be added to the existing set of Webhook versions. - IsWebhookVersionCompatible(webhookVersion string) bool + // 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() []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/registry.go b/pkg/config/registry.go index b0c03f754fe..2100ca557f3 100644 --- a/pkg/config/registry.go +++ b/pkg/config/registry.go @@ -16,14 +16,12 @@ limitations under the License. package config -type constructorFunc func() Config - var ( - registry = make(map[Version]constructorFunc) + registry = make(map[Version]func() Config) ) // Register allows implementations of Config to register themselves so that they can be created with New -func Register(version Version, constructor constructorFunc) { +func Register(version Version, constructor func() Config) { registry[version] = constructor } diff --git a/pkg/config/resgistry_test.go b/pkg/config/resgistry_test.go index c10fd839526..814c5df9e4f 100644 --- a/pkg/config/resgistry_test.go +++ b/pkg/config/resgistry_test.go @@ -28,7 +28,7 @@ var _ = Describe("registry", func() { ) AfterEach(func() { - registry = make(map[Version]constructorFunc) + registry = make(map[Version]func() Config) }) Context("Register", func() { diff --git a/pkg/config/store/errors.go b/pkg/config/store/errors.go new file mode 100644 index 00000000000..fd57aee0a9a --- /dev/null +++ b/pkg/config/store/errors.go @@ -0,0 +1,51 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package store + +import ( + "fmt" +) + +// LoadError wraps errors yielded by Store.Load and Store.LoadFrom methods +type LoadError struct { + Err error +} + +// Error implements error interface +func (e LoadError) Error() string { + return fmt.Sprintf("unable to load the configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e LoadError) Unwrap() error { + return e.Err +} + +// SaveError wraps errors yielded by Store.Save and Store.SaveTo methods +type SaveError struct { + Err error +} + +// Error implements error interface +func (e SaveError) Error() string { + return fmt.Sprintf("unable to save the configuration: %v", e.Err) +} + +// Unwrap implements Wrapper interface +func (e SaveError) Unwrap() error { + return e.Err +} diff --git a/pkg/config/store/errors_test.go b/pkg/config/store/errors_test.go new file mode 100644 index 00000000000..acc9b445b1c --- /dev/null +++ b/pkg/config/store/errors_test.go @@ -0,0 +1,68 @@ +/* +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 store + +import ( + "fmt" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfigStore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Store Suite") +} + +var _ = Describe("LoadError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = LoadError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("unable to load the configuration: %v", wrapped))) + }) + }) + + Context("Unwrap", func() { + It("should unwrap to the wrapped error", func() { + Expect(err.Unwrap()).To(Equal(wrapped)) + }) + }) +}) + +var _ = Describe("SaveError", func() { + var ( + wrapped = fmt.Errorf("error message") + err = SaveError{Err: wrapped} + ) + + Context("Error", func() { + It("should return the correct error message", func() { + Expect(err.Error()).To(Equal(fmt.Sprintf("unable to save the configuration: %v", wrapped))) + }) + }) + + Context("Unwrap", func() { + It("should unwrap to the wrapped error", func() { + Expect(err.Unwrap()).To(Equal(wrapped)) + }) + }) +}) diff --git a/pkg/config/store/interface.go b/pkg/config/store/interface.go new file mode 100644 index 00000000000..2bb1a21cb01 --- /dev/null +++ b/pkg/config/store/interface.go @@ -0,0 +1,38 @@ +/* +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 store + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +// Store represents a persistence backend for config.Config +type Store interface { + // New creates a new config.Config to store + New(config.Version) error + // Load retrieves the config.Config from the persistence backend + Load() error + // LoadFrom retrieves the config.Config from the persistence backend at the specified key + LoadFrom(string) error + // Save stores the config.Config into the persistence backend + Save() error + // SaveTo stores the config.Config into the persistence backend at the specified key + SaveTo(string) error + + // Config returns the stored config.Config + Config() config.Config +} diff --git a/pkg/config/store/yaml/store.go b/pkg/config/store/yaml/store.go new file mode 100644 index 00000000000..2065a4557e6 --- /dev/null +++ b/pkg/config/store/yaml/store.go @@ -0,0 +1,148 @@ +/* +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 yaml + +import ( + "fmt" + "os" + + "github.com/spf13/afero" + "sigs.k8s.io/yaml" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config/store" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" +) + +const ( + // DefaultPath is the default path for the configuration file + DefaultPath = "PROJECT" +) + +// yamlStore implements store.Store using a YAML file as the storage backend +// The key is translated into the YAML file path +type yamlStore struct { + // fs is the filesystem that will be used to store the config.Config + fs afero.Fs + // mustNotExist requires the file not to exist when saving it + mustNotExist bool + + cfg config.Config +} + +// New creates a new configuration that will be stored at the provided path +func New(fs machinery.Filesystem) store.Store { + return &yamlStore{fs: fs.FS} +} + +// New implements store.Store interface +func (s *yamlStore) New(version config.Version) error { + cfg, err := config.New(version) + if err != nil { + return err + } + + s.cfg = cfg + s.mustNotExist = true + return nil +} + +// Load implements store.Store interface +func (s *yamlStore) Load() error { + return s.LoadFrom(DefaultPath) +} + +type versionedConfig struct { + Version config.Version `json:"version"` +} + +// LoadFrom implements store.Store interface +func (s *yamlStore) LoadFrom(path string) error { + s.mustNotExist = false + + // Read the file + in, err := afero.ReadFile(s.fs, path) + if err != nil { + return store.LoadError{Err: fmt.Errorf("unable to read %q file: %w", path, err)} + } + + // Check the file version + var versioned versionedConfig + if err := yaml.Unmarshal(in, &versioned); err != nil { + return store.LoadError{Err: fmt.Errorf("unable to determine config version: %w", err)} + } + + // Create the config object + var cfg config.Config + cfg, err = config.New(versioned.Version) + if err != nil { + return store.LoadError{Err: fmt.Errorf("unable to create config for version %q: %w", versioned.Version, err)} + } + + // Unmarshal the file content + if err := cfg.UnmarshalYAML(in); err != nil { + return store.LoadError{Err: fmt.Errorf("unable to unmarshal config at %q: %w", path, err)} + } + + s.cfg = cfg + return nil +} + +// Save implements store.Store interface +func (s yamlStore) Save() error { + return s.SaveTo(DefaultPath) +} + +// SaveTo implements store.Store interface +func (s yamlStore) SaveTo(path string) error { + // If yamlStore is unset, none of New, Load, or LoadFrom were called successfully + if s.cfg == nil { + return store.SaveError{Err: fmt.Errorf("undefined config, use one of the initializers: New, Load, LoadFrom")} + } + + // If it is a new configuration, the path should not exist yet + if s.mustNotExist { + // Lets check that the file doesn't exist + _, err := s.fs.Stat(path) + if os.IsNotExist(err) { + // This is exactly what we want + } else if err == nil || os.IsExist(err) { + return store.SaveError{Err: fmt.Errorf("configuration already exists in %q", path)} + } else { + return store.SaveError{Err: fmt.Errorf("unable to check for file prior existence: %w", err)} + } + } + + // Marshall into YAML + content, err := s.cfg.MarshalYAML() + if err != nil { + return store.SaveError{Err: fmt.Errorf("unable to marshal to YAML: %w", err)} + } + + // Write the marshalled configuration + err = afero.WriteFile(s.fs, path, content, 0600) + if err != nil { + return store.SaveError{Err: fmt.Errorf("failed to save configuration to %q: %w", path, err)} + } + + return nil +} + +// Config implements store.Store interface +func (s yamlStore) Config() config.Config { + return s.cfg +} diff --git a/pkg/config/store/yaml/store_test.go b/pkg/config/store/yaml/store_test.go new file mode 100644 index 00000000000..61049163dde --- /dev/null +++ b/pkg/config/store/yaml/store_test.go @@ -0,0 +1,246 @@ +/* +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 yaml + +import ( + "errors" + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/config/store" + cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2" + "sigs.k8s.io/kubebuilder/v3/pkg/machinery" +) + +func TestConfigStoreYaml(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Store YAML Suite") +} + +var _ = Describe("New", func() { + It("should return a new empty store", func() { + s := New(machinery.Filesystem{FS: afero.NewMemMapFs()}) + Expect(s.Config()).To(BeNil()) + + ys, ok := s.(*yamlStore) + Expect(ok).To(BeTrue()) + Expect(ys.fs).NotTo(BeNil()) + }) +}) + +var _ = Describe("yamlStore", func() { + const ( + v2File = `version: "2" +` + unversionedFile = `version: +` + nonexistentVersionFile = `version: 1-alpha +` // v1-alpha never existed + wrongFile = `version: "2" +layout: "" +` // layout field does not exist in v2 + ) + + var ( + s *yamlStore + + path = DefaultPath + "2" + ) + + BeforeEach(func() { + s = New(machinery.Filesystem{FS: afero.NewMemMapFs()}).(*yamlStore) + }) + + Context("New", func() { + It("should initialize a new Config backend for the provided version", func() { + Expect(s.New(cfgv2.Version)).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeTrue()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail for an unregistered config version", func() { + Expect(s.New(config.Version{})).NotTo(Succeed()) + }) + }) + + Context("Load", func() { + It("should load the Config from an existing file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(v2File), os.ModePerm)).To(Succeed()) + + Expect(s.Load()).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeFalse()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail if no file exists at the default path", func() { + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to identify the version of the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(unversionedFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to create a Config for the version of the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(nonexistentVersionFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to unmarshal the file at the default path", func() { + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(wrongFile), os.ModePerm)).To(Succeed()) + + err := s.Load() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + }) + + Context("LoadFrom", func() { + It("should load the Config from an existing file from the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(v2File), os.ModePerm)).To(Succeed()) + + Expect(s.LoadFrom(path)).To(Succeed()) + Expect(s.fs).NotTo(BeNil()) + Expect(s.mustNotExist).To(BeFalse()) + Expect(s.Config()).NotTo(BeNil()) + Expect(s.Config().GetVersion().Compare(cfgv2.Version)).To(Equal(0)) + }) + + It("should fail if no file exists at the specified path", func() { + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to identify the version of the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(unversionedFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to create a Config for the version of the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(nonexistentVersionFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + + It("should fail if unable to unmarshal the file at the specified path", func() { + Expect(afero.WriteFile(s.fs, path, []byte(wrongFile), os.ModePerm)).To(Succeed()) + + err := s.LoadFrom(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.LoadError{})).To(BeTrue()) + }) + }) + + Context("Save", func() { + + It("should succeed for a valid config", func() { + s.cfg = cfgv2.New() + Expect(s.Save()).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should succeed for a valid config that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(s.Save()).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should fail for an empty config", func() { + err := s.Save() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + + It("should fail for a pre-existent file that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(afero.WriteFile(s.fs, DefaultPath, []byte(v2File), os.ModePerm)).To(Succeed()) + + err := s.Save() + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + }) + + Context("SaveTo", func() { + It("should success for valid configs", func() { + s.cfg = cfgv2.New() + Expect(s.SaveTo(path)).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(`version: "2" +`)) + }) + + It("should succeed for a valid config that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(s.SaveTo(path)).To(Succeed()) + + cfgBytes, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(cfgBytes)).To(Equal(v2File)) + }) + + It("should fail for an empty config", func() { + err := s.SaveTo(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + + It("should fail for a pre-existent file that must not exist", func() { + s.cfg = cfgv2.New() + s.mustNotExist = true + Expect(afero.WriteFile(s.fs, path, []byte(v2File), os.ModePerm)).To(Succeed()) + + err := s.SaveTo(path) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &store.SaveError{})).To(BeTrue()) + }) + }) +}) diff --git a/pkg/config/v2/config.go b/pkg/config/v2/config.go index b9ee4b57f5d..a01f6c06053 100644 --- a/pkg/config/v2/config.go +++ b/pkg/config/v2/config.go @@ -28,8 +28,6 @@ import ( // Version is the config.Version for project configuration 2 var Version = config.Version{Number: 2} -const apiVersion = "v1beta1" - type cfg struct { // Version Version config.Version `json:"version"` @@ -94,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", } } @@ -222,14 +220,14 @@ func (c cfg) HasGroup(group string) bool { return false } -// IsCRDVersionCompatible implements config.Config -func (c cfg) IsCRDVersionCompatible(crdVersion string) bool { - return crdVersion == apiVersion +// ListCRDVersions implements config.Config +func (c cfg) ListCRDVersions() []string { + return make([]string, 0) } -// IsWebhookVersionCompatible implements config.Config -func (c cfg) IsWebhookVersionCompatible(webhookVersion string) bool { - return webhookVersion == apiVersion +// ListWebhookVersions implements config.Config +func (c cfg) ListWebhookVersions() []string { + return make([]string, 0) } // DecodePluginConfig implements config.Config @@ -249,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} @@ -259,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 8c5359550dc..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()) }) }) @@ -225,22 +225,12 @@ var _ = Describe("cfg", func() { Expect(c.HasGroup("other-group")).To(BeFalse()) }) - It("IsCRDVersionCompatible should return true for `v1beta1`", func() { - Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) + It("ListCRDVersions should return an empty list", func() { + Expect(c.ListCRDVersions()).To(BeEmpty()) }) - It("IsCRDVersionCompatible should return false for any other than `v1beta1`", func() { - Expect(c.IsCRDVersionCompatible("v1")).To(BeFalse()) - Expect(c.IsCRDVersionCompatible("v2")).To(BeFalse()) - }) - - It("IsWebhookVersionCompatible should return true for `v1beta1`", func() { - Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) - }) - - It("IsWebhookVersionCompatible should return false for any other than `v1beta1`", func() { - Expect(c.IsWebhookVersionCompatible("v1")).To(BeFalse()) - Expect(c.IsWebhookVersionCompatible("v2")).To(BeFalse()) + It("ListWebhookVersions should return an empty list", func() { + Expect(c.ListWebhookVersions()).To(BeEmpty()) }) }) @@ -298,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)) }, @@ -308,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)) @@ -330,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 144e9580d96..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"` @@ -47,12 +70,11 @@ type cfg struct { Resources []resource.Resource `json:"resources,omitempty"` // Plugins - Plugins PluginConfigs `json:"plugins,omitempty"` + Plugins pluginConfigs `json:"plugins,omitempty"` } -// PluginConfigs holds a set of arbitrary plugin configuration objects mapped by plugin key. -// TODO: do not export this once internalconfig has merged with config -type PluginConfigs map[string]pluginConfig +// pluginConfigs holds a set of arbitrary plugin configuration objects mapped by plugin key. +type pluginConfigs map[string]pluginConfig // pluginConfig is an arbitrary plugin configuration object. type pluginConfig interface{} @@ -105,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 } @@ -249,35 +271,40 @@ func (c cfg) HasGroup(group string) bool { return false } -// IsCRDVersionCompatible implements config.Config -func (c cfg) IsCRDVersionCompatible(crdVersion string) bool { - return c.resourceAPIVersionCompatible("crd", crdVersion) -} +// ListCRDVersions implements config.Config +func (c cfg) ListCRDVersions() []string { + // Make a map to remove duplicates + versionSet := make(map[string]struct{}) + for _, r := range c.Resources { + if r.API != nil && r.API.CRDVersion != "" { + versionSet[r.API.CRDVersion] = struct{}{} + } + } -// IsWebhookVersionCompatible implements config.Config -func (c cfg) IsWebhookVersionCompatible(webhookVersion string) bool { - return c.resourceAPIVersionCompatible("webhook", webhookVersion) + // Convert the map into a slice + versions := make([]string, 0, len(versionSet)) + for version := range versionSet { + versions = append(versions, version) + } + return versions } -func (c cfg) resourceAPIVersionCompatible(verType, version string) bool { - for _, res := range c.Resources { - var currVersion string - switch verType { - case "crd": - if res.API != nil { - currVersion = res.API.CRDVersion - } - case "webhook": - if res.Webhooks != nil { - currVersion = res.Webhooks.WebhookVersion - } - } - if currVersion != "" && version != currVersion { - return false +// ListWebhookVersions implements config.Config +func (c cfg) ListWebhookVersions() []string { + // Make a map to remove duplicates + versionSet := make(map[string]struct{}) + for _, r := range c.Resources { + if r.Webhooks != nil && r.Webhooks.WebhookVersion != "" { + versionSet[r.Webhooks.WebhookVersion] = struct{}{} } } - return true + // Convert the map into a slice + versions := make([]string, 0, len(versionSet)) + for version := range versionSet { + versions = append(versions, version) + } + return versions } // DecodePluginConfig implements config.Config @@ -320,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() { @@ -341,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 37ed9d864af..8450f230887 100644 --- a/pkg/config/v3/config_test.go +++ b/pkg/config/v3/config_test.go @@ -18,6 +18,7 @@ package v3 import ( "errors" + "sort" "testing" . "github.com/onsi/ginkgo" @@ -38,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, } }) @@ -86,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)) }) @@ -97,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)) }) }) @@ -302,42 +307,60 @@ var _ = Describe("cfg", func() { Expect(c.HasGroup("other-group")).To(BeFalse()) }) - It("IsCRDVersionCompatible should return true with no tracked resources", func() { - Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) - Expect(c.IsCRDVersionCompatible("v1")).To(BeTrue()) + It("ListCRDVersions should return an empty list with no tracked resources", func() { + Expect(c.ListCRDVersions()).To(BeEmpty()) }) - It("IsCRDVersionCompatible should return true only for matching CRD versions of tracked resources", func() { - c.Resources = append(c.Resources, resource.Resource{ - GVK: resource.GVK{ - Group: res.Group, - Version: res.Version, - Kind: res.Kind, + It("ListCRDVersions should return a list of tracked resources CRD versions", func() { + c.Resources = append(c.Resources, + resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: res.Kind, + }, + API: &resource.API{CRDVersion: "v1beta1"}, }, - API: &resource.API{CRDVersion: "v1beta1"}, - }) - Expect(c.IsCRDVersionCompatible("v1beta1")).To(BeTrue()) - Expect(c.IsCRDVersionCompatible("v1")).To(BeFalse()) - Expect(c.IsCRDVersionCompatible("v2")).To(BeFalse()) + resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: "OtherKind", + }, + API: &resource.API{CRDVersion: "v1"}, + }, + ) + versions := c.ListCRDVersions() + sort.Strings(versions) // ListCRDVersions has no order guarantee so sorting for reproducibility + Expect(versions).To(Equal([]string{"v1", "v1beta1"})) }) - It("IsWebhookVersionCompatible should return true with no tracked resources", func() { - Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) - Expect(c.IsWebhookVersionCompatible("v1")).To(BeTrue()) + It("ListWebhookVersions should return an empty list with no tracked resources", func() { + Expect(c.ListWebhookVersions()).To(BeEmpty()) }) - It("IsWebhookVersionCompatible should return true only for matching webhook versions of tracked resources", func() { - c.Resources = append(c.Resources, resource.Resource{ - GVK: resource.GVK{ - Group: res.Group, - Version: res.Version, - Kind: res.Kind, + It("ListWebhookVersions should return a list of tracked resources webhook versions", func() { + c.Resources = append(c.Resources, + resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: res.Kind, + }, + Webhooks: &resource.Webhooks{WebhookVersion: "v1beta1"}, + }, + resource.Resource{ + GVK: resource.GVK{ + Group: res.Group, + Version: res.Version, + Kind: "OtherKind", + }, + Webhooks: &resource.Webhooks{WebhookVersion: "v1"}, }, - Webhooks: &resource.Webhooks{WebhookVersion: "v1beta1"}, - }) - Expect(c.IsWebhookVersionCompatible("v1beta1")).To(BeTrue()) - Expect(c.IsWebhookVersionCompatible("v1")).To(BeFalse()) - Expect(c.IsWebhookVersionCompatible("v2")).To(BeFalse()) + ) + versions := c.ListWebhookVersions() + sort.Strings(versions) // ListWebhookVersions has no order guarantee so sorting for reproducibility + Expect(versions).To(Equal([]string{"v1", "v1beta1"})) }) }) @@ -355,32 +378,32 @@ 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, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, + Plugins: pluginConfigs{ + key: map[string]interface{}{ "data-1": "", }, }, } c2 = cfg{ - Version: Version, - Domain: domain, - Repository: repo, - Name: name, - Layout: layout, - Plugins: PluginConfigs{ - "plugin-x": map[string]interface{}{ + Version: Version, + Domain: domain, + Repository: repo, + Name: name, + PluginChain: pluginChain, + Plugins: pluginConfigs{ + key: map[string]interface{}{ "data-1": "plugin value 1", "data-2": "plugin value 2", }, @@ -399,6 +422,13 @@ var _ = Describe("cfg", func() { Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue()) }) + It("DecodePluginConfig should fail to retrieve data from a non-existent plugin", func() { + var pluginConfig PluginConfig + err := c1.DecodePluginConfig("plugin-y", &pluginConfig) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &config.PluginKeyNotFoundError{})).To(BeTrue()) + }) + DescribeTable("DecodePluginConfig should retrieve the plugin data correctly", func(inputConfig cfg, expectedPluginConfig PluginConfig) { var pluginConfig PluginConfig @@ -427,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{ @@ -488,7 +518,7 @@ var _ = Describe("cfg", func() { }, }, }, - Plugins: PluginConfigs{ + Plugins: pluginConfigs{ "plugin-x": map[string]interface{}{ "data-1": "single plugin datum", }, @@ -499,9 +529,15 @@ var _ = Describe("cfg", func() { }, }, } - // TODO: include cases with Plural, Path, API.namespaced, Controller, Webhooks.Defaulting, - // Webhooks.Validation and Webhooks.Conversion when added + // 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 @@ -509,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: @@ -554,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)) }, @@ -564,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)) @@ -589,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/internal/validation/dns.go b/pkg/internal/validation/dns.go index 33a150f9e70..cf34178c766 100644 --- a/pkg/internal/validation/dns.go +++ b/pkg/internal/validation/dns.go @@ -27,7 +27,7 @@ import ( const ( dns1123LabelFmt string = "[a-z0-9](?:[-a-z0-9]*[a-z0-9])?" - dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*" + dns1123SubdomainFmt string = dns1123LabelFmt + "(?:\\." + dns1123LabelFmt + ")*" dns1035LabelFmt string = "[a-z](?:[-a-z0-9]*[a-z0-9])?" ) @@ -40,10 +40,11 @@ type dnsValidationConfig struct { } var dns1123LabelConfig = dnsValidationConfig{ - format: dns1123LabelFmt, - maxLen: 56, // = 63 - len("-system") - re: regexp.MustCompile("^" + dns1123LabelFmt + "$"), - errMsg: "a DNS-1123 label must consist of lower case alphanumeric characters or '-'", + format: dns1123LabelFmt, + maxLen: 56, // = 63 - len("-system") + re: regexp.MustCompile("^" + dns1123LabelFmt + "$"), + errMsg: "a DNS-1123 label must consist of lower case alphanumeric characters or '-', " + + "and must start and end with an alphanumeric character", examples: []string{"example.com"}, } @@ -75,17 +76,17 @@ func (c dnsValidationConfig) check(value string) (errs []string) { return errs } +// IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). +func IsDNS1123Label(value string) []string { + return dns1123LabelConfig.check(value) +} + // IsDNS1123Subdomain tests for a string that conforms to the definition of a // subdomain in DNS (RFC 1123). func IsDNS1123Subdomain(value string) []string { return dns1123SubdomainConfig.check(value) } -// IsDNS1123Label tests for a string that conforms to the definition of a label in DNS (RFC 1123). -func IsDNS1123Label(value string) []string { - return dns1123LabelConfig.check(value) -} - // IsDNS1035Label tests for a string that conforms to the definition of a label in DNS (RFC 1035). func IsDNS1035Label(value string) []string { return dns1035LabelConfig.check(value) diff --git a/pkg/internal/validation/dns_test.go b/pkg/internal/validation/dns_test.go new file mode 100644 index 00000000000..fa7c1d8770f --- /dev/null +++ b/pkg/internal/validation/dns_test.go @@ -0,0 +1,131 @@ +/* +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 validation + +import ( + "fmt" + "strings" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestValidation(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Validation Suite") +} + +var _ = Describe("IsDNS1123Label", func() { + It("should return no error", func() { + for _, value := range []string{ + "a", "ab", "abc", "a1", "a-1", "a--1--2--b", + "0", "01", "012", "1a", "1-a", "1--a--b--2", + strings.Repeat("a", 56), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1123Label(value))).To(Equal(0)) + } + }) + + It("should return at least one error", func() { + for _, value := range []string{ + "", "A", "ABC", "aBc", "A1", "A-1", "1-A", + "-", "a-", "-a", "1-", "-1", + "_", "a_", "_a", "a_b", "1_", "_1", "1_2", + ".", "a.", ".a", "a.b", "1.", ".1", "1.2", + " ", "a ", " a", "a b", "1 ", " 1", "1 2", + strings.Repeat("a", 57), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1123Label(value))).NotTo(Equal(0)) + } + }) +}) + +var _ = Describe("IsDNS1123Subdomain", func() { + It("should return no error", func() { + for _, value := range []string{ + "a", "ab", "abc", "a1", "a-1", "a--1--2--b", + "0", "01", "012", "1a", "1-a", "1--a--b--2", + "a.a", "ab.a", "abc.a", "a1.a", "a-1.a", "a--1--2--b.a", + "a.1", "ab.1", "abc.1", "a1.1", "a-1.1", "a--1--2--b.1", + "0.a", "01.a", "012.a", "1a.a", "1-a.a", "1--a--b--2", + "0.1", "01.1", "012.1", "1a.1", "1-a.1", "1--a--b--2.1", + "a.b.c.d.e", "aa.bb.cc.dd.ee", "1.2.3.4.5", "11.22.33.44.55", + strings.Repeat("a", 253), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1123Subdomain(value))).To(Equal(0)) + } + }) + + It("should return at least one error", func() { + for _, value := range []string{ + "", "A", "ABC", "aBc", "A1", "A-1", "1-A", + "-", "a-", "-a", "1-", "-1", + "_", "a_", "_a", "a_b", "1_", "_1", "1_2", + ".", "a.", ".a", "a..b", "1.", ".1", "1..2", + " ", "a ", " a", "a b", "1 ", " 1", "1 2", + "A.a", "aB.a", "ab.A", "A1.a", "a1.A", + "A.1", "aB.1", "A1.1", "1A.1", + "0.A", "01.A", "012.A", "1A.a", "1a.A", + "A.B.C.D.E", "AA.BB.CC.DD.EE", "a.B.c.d.e", "aa.bB.cc.dd.ee", + "a@b", "a,b", "a_b", "a;b", + "a:b", "a%b", "a?b", "a$b", + strings.Repeat("a", 254), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1123Subdomain(value))).NotTo(Equal(0)) + } + }) +}) + +var _ = Describe("IsDNS1035Label", func() { + It("should return no error", func() { + for _, value := range []string{ + "a", "ab", "abc", "a1", "a-1", "a--1--2--b", + strings.Repeat("a", 63), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1035Label(value))).To(Equal(0)) + } + }) + + It("should return at least one error", func() { + for _, value := range []string{ + "0", "01", "012", "1a", "1-a", "1--a--b--2", + "", "A", "ABC", "aBc", "A1", "A-1", "1-A", + "-", "a-", "-a", "1-", "-1", + "_", "a_", "_a", "a_b", "1_", "_1", "1_2", + ".", "a.", ".a", "a.b", "1.", ".1", "1.2", + " ", "a ", " a", "a b", "1 ", " 1", "1 2", + strings.Repeat("a", 64), + } { + By(fmt.Sprintf("for %s", value)) + Expect(len(IsDNS1035Label(value))).NotTo(Equal(0)) + } + }) +}) + +// This test provides coverage to the conditional that handles no examples +// as they are not being used in any check +var _ = Describe("regexError", func() { + It("should work without examples", func() { + Expect(regexError("$", "_")).To(Equal("$ (regex used for validation is '_')")) + }) +}) diff --git a/pkg/machinery/errors.go b/pkg/machinery/errors.go new file mode 100644 index 00000000000..af015ac30e9 --- /dev/null +++ b/pkg/machinery/errors.go @@ -0,0 +1,145 @@ +/* +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 machinery + +import ( + "fmt" +) + +// This file contains the errors returned by the scaffolding machinery +// They are exported to be able to check which kind of error was returned + +// ValidateError is a wrapper error that will be used for errors returned by RequiresValidation.Validate +type ValidateError struct { + error +} + +// Unwrap implements Wrapper interface +func (e ValidateError) Unwrap() error { + return e.error +} + +// SetTemplateDefaultsError is a wrapper error that will be used for errors returned by Template.SetTemplateDefaults +type SetTemplateDefaultsError struct { + error +} + +// Unwrap implements Wrapper interface +func (e SetTemplateDefaultsError) 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 +} + +// Unwrap implements Wrapper interface +func (e ExistsFileError) Unwrap() error { + return e.error +} + +// OpenFileError is a wrapper error that will be used for errors when opening a file +type OpenFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e OpenFileError) Unwrap() error { + return e.error +} + +// CreateDirectoryError is a wrapper error that will be used for errors when creating a directory +type CreateDirectoryError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CreateDirectoryError) Unwrap() error { + return e.error +} + +// CreateFileError is a wrapper error that will be used for errors when creating a file +type CreateFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CreateFileError) Unwrap() error { + return e.error +} + +// ReadFileError is a wrapper error that will be used for errors when reading a file +type ReadFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e ReadFileError) Unwrap() error { + return e.error +} + +// WriteFileError is a wrapper error that will be used for errors when writing a file +type WriteFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e WriteFileError) Unwrap() error { + return e.error +} + +// CloseFileError is a wrapper error that will be used for errors when closing a file +type CloseFileError struct { + error +} + +// Unwrap implements Wrapper interface +func (e CloseFileError) Unwrap() error { + return e.error +} + +// ModelAlreadyExistsError is returned if the file is expected not to exist but a previous model does +type ModelAlreadyExistsError struct { + path string +} + +// Error implements error interface +func (e ModelAlreadyExistsError) Error() string { + return fmt.Sprintf("failed to create %s: model already exists", e.path) +} + +// UnknownIfExistsActionError is returned if the if-exists-action is unknown +type UnknownIfExistsActionError struct { + path string + ifExistsAction IfExistsAction +} + +// Error implements error interface +func (e UnknownIfExistsActionError) Error() string { + return fmt.Sprintf("unknown behavior if file exists (%d) for %s", e.ifExistsAction, e.path) +} + +// FileAlreadyExistsError is returned if the file is expected not to exist but it does +type FileAlreadyExistsError struct { + path string +} + +// Error implements error interface +func (e FileAlreadyExistsError) Error() string { + return fmt.Sprintf("failed to create %s: file already exists", e.path) +} diff --git a/pkg/machinery/errors_test.go b/pkg/machinery/errors_test.go new file mode 100644 index 00000000000..2c4d682e957 --- /dev/null +++ b/pkg/machinery/errors_test.go @@ -0,0 +1,55 @@ +/* +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 machinery + +import ( + "errors" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Errors", func() { + var ( + path = filepath.Join("path", "to", "file") + testErr = errors.New("test error") + ) + + DescribeTable("should contain the wrapped error", + func(err error) { + Expect(errors.Is(err, testErr)).To(BeTrue()) + }, + Entry("for validate errors", ValidateError{testErr}), + Entry("for set template defaults errors", SetTemplateDefaultsError{testErr}), + Entry("for file existence errors", ExistsFileError{testErr}), + Entry("for file opening errors", OpenFileError{testErr}), + Entry("for directory creation errors", CreateDirectoryError{testErr}), + Entry("for file creation errors", CreateFileError{testErr}), + Entry("for file reading errors", ReadFileError{testErr}), + Entry("for file writing errors", WriteFileError{testErr}), + Entry("for file closing errors", CloseFileError{testErr}), + ) + + // NOTE: the following test increases coverage + It("should print a descriptive error message", func() { + Expect(ModelAlreadyExistsError{path}.Error()).To(ContainSubstring("model already exists")) + Expect(UnknownIfExistsActionError{path, -1}.Error()).To(ContainSubstring("unknown behavior if file exists")) + Expect(FileAlreadyExistsError{path}.Error()).To(ContainSubstring("file already exists")) + }) +}) 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/machinery/filesystem.go b/pkg/machinery/filesystem.go new file mode 100644 index 00000000000..f28cfb8b9e7 --- /dev/null +++ b/pkg/machinery/filesystem.go @@ -0,0 +1,26 @@ +/* +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/spf13/afero" +) + +// Filesystem abstracts the underlying disk for scaffolding +type Filesystem struct { + FS afero.Fs +} 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/plugins/golang/v2/suite_test.go b/pkg/machinery/machinery_suite_test.go similarity index 88% rename from pkg/plugins/golang/v2/suite_test.go rename to pkg/machinery/machinery_suite_test.go index de06fb4cf53..becbe2ed2d4 100644 --- a/pkg/plugins/golang/v2/suite_test.go +++ b/pkg/machinery/machinery_suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v2 +package machinery import ( "testing" @@ -23,7 +23,7 @@ import ( . "github.com/onsi/gomega" ) -func TestGoPluginV2(t *testing.T) { +func TestMachinery(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Go Plugin v2 Suite") + RunSpecs(t, "Machinery suite") } 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/plugins/internal/machinery/scaffold.go b/pkg/machinery/scaffold.go similarity index 51% rename from pkg/plugins/internal/machinery/scaffold.go rename to pkg/machinery/scaffold.go index 5391c45bb2e..3b79104ad9b 100644 --- a/pkg/plugins/internal/machinery/scaffold.go +++ b/pkg/machinery/scaffold.go @@ -20,16 +20,23 @@ import ( "bufio" "bytes" "fmt" - "io/ioutil" + "os" "path/filepath" "strings" "text/template" + "github.com/spf13/afero" "golang.org/x/tools/imports" - "sigs.k8s.io/kubebuilder/v3/pkg/model" - "sigs.k8s.io/kubebuilder/v3/pkg/model/file" - "sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/filesystem" + "sigs.k8s.io/kubebuilder/v3/pkg/config" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +const ( + createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC + + defaultDirectoryPermission os.FileMode = 0700 + defaultFilePermission os.FileMode = 0600 ) var options = imports.Options{ @@ -40,73 +47,108 @@ var options = imports.Options{ } // Scaffold uses templates to scaffold new files -type Scaffold interface { - // Execute writes to disk the provided files - Execute(*model.Universe, ...file.Builder) error -} +type Scaffold struct { + // fs allows to mock the file system for tests + fs afero.Fs -// scaffold implements Scaffold interface -type scaffold struct { - // plugins is the list of plugins we should allow to transform our generated scaffolding - plugins []model.Plugin + // permissions for new directories and files + dirPerm os.FileMode + filePerm os.FileMode - // fs allows to mock the file system for tests - fs filesystem.FileSystem + // injector is used to provide several fields to the templates + injector injector } +// ScaffoldOption allows to provide optional arguments to the Scaffold +type ScaffoldOption func(*Scaffold) + // NewScaffold returns a new Scaffold with the provided plugins -func NewScaffold(plugins ...model.Plugin) Scaffold { - return &scaffold{ - plugins: plugins, - fs: filesystem.New(), +func NewScaffold(fs Filesystem, options ...ScaffoldOption) *Scaffold { + s := &Scaffold{ + fs: fs.FS, + dirPerm: defaultDirectoryPermission, + filePerm: defaultFilePermission, } + + for _, option := range options { + option(s) + } + + return s } -// Execute implements Scaffold.Execute -func (s *scaffold) Execute(universe *model.Universe, files ...file.Builder) error { - // Initialize the universe files - universe.Files = make(map[string]*file.File, len(files)) +// WithDirectoryPermissions sets the permissions for new directories +func WithDirectoryPermissions(dirPerm os.FileMode) ScaffoldOption { + return func(s *Scaffold) { + s.dirPerm = dirPerm + } +} - // Set the repo as the local prefix so that it knows how to group imports - if universe.Config != nil { - imports.LocalPrefix = universe.Config.GetRepository() +// WithFilePermissions sets the permissions for new files +func WithFilePermissions(filePerm os.FileMode) ScaffoldOption { + return func(s *Scaffold) { + s.filePerm = filePerm } +} - for _, f := range files { +// WithConfig provides the project configuration to the Scaffold +func WithConfig(cfg config.Config) ScaffoldOption { + return func(s *Scaffold) { + s.injector.config = cfg + + if cfg != nil && cfg.GetRepository() != "" { + imports.LocalPrefix = cfg.GetRepository() + } + } +} + +// WithBoilerplate provides the boilerplate to the Scaffold +func WithBoilerplate(boilerplate string) ScaffoldOption { + return func(s *Scaffold) { + s.injector.boilerplate = boilerplate + } +} + +// WithResource provides the resource to the Scaffold +func WithResource(resource *resource.Resource) ScaffoldOption { + return func(s *Scaffold) { + s.injector.resource = resource + } +} + +// Execute writes to disk the provided files +func (s *Scaffold) Execute(builders ...Builder) error { + // Initialize the files + files := make(map[string]*File, len(builders)) + + 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 { - return file.NewValidateError(err) + 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 model.NewPluginError(err) - } - } - // Persist the files to disk - for _, f := range universe.Files { + for _, f := range files { if err := s.writeFile(f); err != nil { return err } @@ -116,51 +158,60 @@ func (s *scaffold) Execute(universe *model.Universe, files ...file.Builder) erro } // 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 - err := t.SetTemplateDefaults() - if err != nil { - return file.NewSetTemplateDefaultsError(err) + if err := t.SetTemplateDefaults(); err != nil { + return SetTemplateDefaultsError{err} } + path := t.GetPath() + // Handle already existing models - if _, found := models[t.GetPath()]; found { + if _, found := models[path]; found { switch t.GetIfExistsAction() { - case file.Skip: + case SkipFile: return nil - case file.Error: - return modelAlreadyExistsError{t.GetPath()} - case file.Overwrite: + case Error: + return ModelAlreadyExistsError{path} + case OverwriteFile: default: - return unknownIfExistsActionError{t.GetPath(), t.GetIfExistsAction()} + return UnknownIfExistsActionError{path, t.GetIfExistsAction()} } } - m := &file.File{ - Path: t.GetPath(), - IfExistsAction: t.GetIfExistsAction(), - } - b, err := doTemplate(t) if err != nil { return err } - m.Contents = string(b) - models[m.Path] = m + models[path] = &File{ + Path: path, + Contents: string(b), + IfExistsAction: t.GetIfExistsAction(), + } return nil } // doTemplate executes the template for a file using the input -func doTemplate(t file.Template) ([]byte, error) { - temp, err := newTemplate(t).Parse(t.GetBody()) - if err != nil { +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 := DefaultFuncMap() + if templateWithFuncMap, hasCustomFuncMap := t.(UseCustomFuncMap); hasCustomFuncMap { + fm = templateWithFuncMap.GetFuncMap() + } + temp.Funcs(fm) + + // Set the template body + if _, err := temp.Parse(t.GetBody()); err != nil { return nil, err } + // Execute the template out := &bytes.Buffer{} - err = temp.Execute(out, t) - if err != nil { + if err := temp.Execute(out, t); err != nil { return nil, err } b := out.Bytes() @@ -168,8 +219,8 @@ func doTemplate(t file.Template) ([]byte, error) { // TODO(adirio): move go-formatting to write step // gofmt the imports if filepath.Ext(t.GetPath()) == ".go" { - b, err = imports.Process(t.GetPath(), b, &options) - if err != nil { + var err error + if b, err = imports.Process(t.GetPath(), b, &options); err != nil { return nil, err } } @@ -177,18 +228,8 @@ func doTemplate(t file.Template) ([]byte, error) { return b, nil } -// newTemplate a new template with common functions -func newTemplate(t file.Template) *template.Template { - fm := file.DefaultFuncMap() - useFM, ok := t.(file.UseCustomFuncMap) - if ok { - fm = useFM.GetFuncMap() - } - return template.New(fmt.Sprintf("%T", t)).Funcs(fm) -} - // 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 @@ -223,19 +264,21 @@ 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 - if m, found := models[i.GetPath()]; found { + if m, found := models[path]; found { // Check if there is already an scaffolded file - exists, err := s.fs.Exists(i.GetPath()) + exists, err := afero.Exists(s.fs, path) if err != nil { - return nil, err + return nil, ExistsFileError{err} } // If there is a model but no scaffolded file we return the model @@ -245,52 +288,50 @@ 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(i.GetPath()) + 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{i.GetPath()} - case file.Overwrite: + return nil, FileAlreadyExistsError{path} + case OverwriteFile: // Model has preference return m, nil default: - return nil, unknownIfExistsActionError{i.GetPath(), m.IfExistsAction} + return nil, UnknownIfExistsActionError{path, m.IfExistsAction} } } // There was no model - return s.loadModelFromFile(i.GetPath()) + return s.loadModelFromFile(path) } // 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 + return nil, OpenFileError{err} } defer func() { - closeErr := reader.Close() - if err == nil { - err = closeErr + if closeErr := reader.Close(); err == nil && closeErr != nil { + err = CloseFileError{closeErr} } }() - content, err := ioutil.ReadAll(reader) + content, err := afero.ReadAll(reader) if err != nil { - return + return nil, ReadFileError{err} } - f = &file.File{Path: path, Contents: string(content)} - return + 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() @@ -314,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() @@ -335,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)) @@ -359,31 +400,44 @@ func insertStrings(content string, codeFragmentsMap file.CodeFragmentsMap) ([]by return out.Bytes(), nil } -func (s scaffold) writeFile(f *file.File) error { +func (s Scaffold) writeFile(f *File) (err error) { // Check if the file to write already exists - exists, err := s.fs.Exists(f.Path) + exists, err := afero.Exists(s.fs, f.Path) if err != nil { - return err + return ExistsFileError{err} } 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} + return FileAlreadyExistsError{f.Path} } } - writer, err := s.fs.Create(f.Path) + // Create the directory if needed + if err := s.fs.MkdirAll(filepath.Dir(f.Path), s.dirPerm); err != nil { + return CreateDirectoryError{err} + } + + // Create or truncate the file + writer, err := s.fs.OpenFile(f.Path, createOrUpdate, s.filePerm) if err != nil { - return err + return CreateFileError{err} } + defer func() { + if closeErr := writer.Close(); err == nil && closeErr != nil { + err = CloseFileError{err} + } + }() - _, err = writer.Write([]byte(f.Contents)) + if _, err := writer.Write([]byte(f.Contents)); err != nil { + return WriteFileError{err} + } - return err + return nil } diff --git a/pkg/machinery/scaffold_test.go b/pkg/machinery/scaffold_test.go new file mode 100644 index 00000000000..96142e259f1 --- /dev/null +++ b/pkg/machinery/scaffold_test.go @@ -0,0 +1,505 @@ +/* +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 ( + "errors" + "os" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" + "github.com/spf13/afero" + + cfgv3 "sigs.k8s.io/kubebuilder/v3/pkg/config/v3" + "sigs.k8s.io/kubebuilder/v3/pkg/model/resource" +) + +var _ = Describe("Scaffold", func() { + Describe("NewScaffold", func() { + It("should succeed for no option", func() { + s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + 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.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(dirPermissions)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + 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.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(filePermissions)) + 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.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + 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.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + 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() { + var res = &resource.Resource{GVK: resource.GVK{ + Group: "group", + Domain: "my.domain", + Version: "v1", + Kind: "Kind", + }} + + s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithResource(res)) + Expect(s.fs).NotTo(BeNil()) + Expect(s.dirPerm).To(Equal(defaultDirectoryPermission)) + Expect(s.filePerm).To(Equal(defaultFilePermission)) + 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()) + }) + }) + + Describe("Scaffold.Execute", func() { + const ( + path = "filename" + pathGo = path + ".go" + pathYaml = path + ".yaml" + content = "Hello world!" + ) + + var ( + testErr = errors.New("error text") + + s *Scaffold + ) + + BeforeEach(func() { + s = &Scaffold{fs: afero.NewMemMapFs()} + }) + + DescribeTable("successes", + func(path, expected string, files ...Builder) { + Expect(s.Execute(files...)).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(expected)) + }, + Entry("should write the file", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, + ), + Entry("should skip optional models if already have one", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + ), + Entry("should overwrite required models if already have one", + path, content, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content}, + ), + Entry("should format a go file", + pathGo, "package file\n", + fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: "package file"}, + ), + ) + + DescribeTable("file builders related errors", + func(errType interface{}, files ...Builder) { + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, errType)).To(BeTrue()) + }, + Entry("should fail if unable to validate a file builder", + &ValidateError{}, + fakeRequiresValidation{validateErr: testErr}, + ), + Entry("should fail if unable to set default values for a template", + &SetTemplateDefaultsError{}, + fakeTemplate{err: testErr}, + ), + Entry("should fail if an unexpected previous model is found", + &ModelAlreadyExistsError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}}, + ), + Entry("should fail if behavior if-exists-action is not defined", + &UnknownIfExistsActionError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, + fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: -1}}, + ), + ) + + // Following errors are unwrapped, so we need to check for substrings + DescribeTable("template related errors", + func(errMsg string, files ...Builder) { + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(errMsg)) + }, + Entry("should fail if a template is broken", + "template: ", + fakeTemplate{body: "{{ .Field }"}, + ), + Entry("should fail if a template params aren't provided", + "template: ", + fakeTemplate{body: "{{ .Field }}"}, + ), + Entry("should fail if unable to format a go file", + "expected 'package', found ", + fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: content}, + ), + ) + + DescribeTable("insert strings", + func(path, input, expected string, files ...Builder) { + Expect(afero.WriteFile(s.fs, path, []byte(input), 0666)).To(Succeed()) + + Expect(s.Execute(files...)).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(expected)) + }, + Entry("should insert lines for go files", + pathGo, + `package test + +//+kubebuilder:scaffold:- +`, + `package test + +var a int +var b int + +//+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathGo}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, + }, + }, + ), + Entry("should insert lines for yaml files", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use models if there is no file", + pathYaml, + "", + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` +#+kubebuilder:scaffold:- +`}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use required models over files", + pathYaml, + content, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` +#+kubebuilder:scaffold:- +`}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should use files over optional models", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +1 +2 +#+kubebuilder:scaffold:- +`, + fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml}, body: content}, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + }, + }, + ), + Entry("should filter invalid markers", + pathYaml, + ` +#+kubebuilder:scaffold:- +#+kubebuilder:scaffold:* +`, + ` +1 +2 +#+kubebuilder:scaffold:- +#+kubebuilder:scaffold:* +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + markers: []Marker{NewMarkerFor(pathYaml, "-")}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + }, + }, + ), + Entry("should filter already existing one-line code fragments", + pathYaml, + ` +1 +#+kubebuilder:scaffold:- +3 +4 +#+kubebuilder:scaffold:* +`, + ` +1 +2 +#+kubebuilder:scaffold:- +3 +4 +#+kubebuilder:scaffold:* +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, + NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, + }, + }, + ), + Entry("should not insert anything if no code fragment", + pathYaml, + ` +#+kubebuilder:scaffold:- +`, + ` +#+kubebuilder:scaffold:- +`, + fakeInserter{ + fakeBuilder: fakeBuilder{path: pathYaml}, + codeFragments: CodeFragmentsMap{ + NewMarkerFor(pathYaml, "-"): {}, + }, + }, + ), + ) + + DescribeTable("insert strings related errors", + func(errType interface{}, files ...Builder) { + Expect(afero.WriteFile(s.fs, path, []byte{}, 0666)).To(Succeed()) + + err := s.Execute(files...) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, errType)).To(BeTrue()) + }, + 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: 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", + &UnknownIfExistsActionError{}, + fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, + fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, + ), + ) + + Context("write when the file already exists", func() { + BeforeEach(func() { + _ = afero.WriteFile(s.fs, path, []byte{}, 0666) + }) + + It("should skip the file by default", func() { + Expect(s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path}, + body: content, + })).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(BeEmpty()) + }) + + It("should write the file if configured to do so", func() { + Expect(s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, + body: content, + })).To(Succeed()) + + b, err := afero.ReadFile(s.fs, path) + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(Equal(content)) + }) + + It("should error if configured to do so", func() { + err := s.Execute(fakeTemplate{ + fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}, + body: content, + }) + Expect(err).To(HaveOccurred()) + Expect(errors.As(err, &FileAlreadyExistsError{})).To(BeTrue()) + }) + }) + }) +}) + +var _ Builder = fakeBuilder{} + +// fakeBuilder is used to mock a Builder +type fakeBuilder struct { + path string + ifExistsAction IfExistsAction +} + +// GetPath implements Builder +func (f fakeBuilder) GetPath() string { + return f.path +} + +// GetIfExistsAction implements Builder +func (f fakeBuilder) GetIfExistsAction() IfExistsAction { + return f.ifExistsAction +} + +var _ RequiresValidation = fakeRequiresValidation{} + +// fakeRequiresValidation is used to mock a RequiresValidation in order to test Scaffold +type fakeRequiresValidation struct { + fakeBuilder + + validateErr error +} + +// Validate implements RequiresValidation +func (f fakeRequiresValidation) Validate() error { + return f.validateErr +} + +var _ Template = fakeTemplate{} + +// fakeTemplate is used to mock a File in order to test Scaffold +type fakeTemplate struct { + fakeBuilder + + body string + err error +} + +// GetBody implements Template +func (f fakeTemplate) GetBody() string { + return f.body +} + +// SetTemplateDefaults implements Template +func (f fakeTemplate) SetTemplateDefaults() error { + if f.err != nil { + return f.err + } + + return nil +} + +type fakeInserter struct { + fakeBuilder + + markers []Marker + codeFragments CodeFragmentsMap +} + +// GetMarkers implements Inserter +func (f fakeInserter) GetMarkers() []Marker { + if f.markers != nil { + return f.markers + } + + markers := make([]Marker, 0, len(f.codeFragments)) + for marker := range f.codeFragments { + markers = append(markers, marker) + } + return markers +} + +// GetCodeFragments implements Inserter +func (f fakeInserter) GetCodeFragments() CodeFragmentsMap { + return f.codeFragments +} diff --git a/pkg/model/file/errors.go b/pkg/model/file/errors.go deleted file mode 100644 index 20349f55844..00000000000 --- a/pkg/model/file/errors.go +++ /dev/null @@ -1,61 +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 file - -import ( - "errors" -) - -// validateError is a wrapper error that will be used for errors returned by RequiresValidation.Validate -type validateError struct { - error -} - -// NewValidateError wraps an error to specify it was returned by RequiresValidation.Validate -func NewValidateError(err error) error { - return validateError{err} -} - -// Unwrap implements Wrapper interface -func (e validateError) Unwrap() error { - return e.error -} - -// IsValidateError checks if the error was returned by RequiresValidation.Validate -func IsValidateError(err error) bool { - return errors.As(err, &validateError{}) -} - -// setTemplateDefaultsError is a wrapper error that will be used for errors returned by Template.SetTemplateDefaults -type setTemplateDefaultsError struct { - error -} - -// NewSetTemplateDefaultsError wraps an error to specify it was returned by Template.SetTemplateDefaults -func NewSetTemplateDefaultsError(err error) error { - return setTemplateDefaultsError{err} -} - -// Unwrap implements Wrapper interface -func (e setTemplateDefaultsError) Unwrap() error { - return e.error -} - -// IsSetTemplateDefaultsError checks if the error was returned by Template.SetTemplateDefaults -func IsSetTemplateDefaultsError(err error) bool { - return errors.As(err, &setTemplateDefaultsError{}) -} diff --git a/pkg/model/plugin.go b/pkg/model/plugin.go deleted file mode 100644 index 57d326fa378..00000000000 --- a/pkg/model/plugin.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2018 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package model - -import ( - "errors" -) - -// 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 -} - -// pluginError is a wrapper error that will be used for errors returned by Plugin.Pipe -type pluginError struct { - error -} - -// NewPluginError wraps an error to specify it was returned by Plugin.Pipe -func NewPluginError(err error) error { - return pluginError{err} -} - -// Unwrap implements Wrapper interface -func (e pluginError) Unwrap() error { - return e.error -} - -// IsPluginError checks if the error was returned by Plugin.Pipe -func IsPluginError(err error) bool { - return errors.As(err, &pluginError{}) -} 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 15d597c88f7..00000000000 --- a/pkg/model/universe.go +++ /dev/null @@ -1,111 +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"` -} - -// NewUniverse creates a new Universe -func NewUniverse(options ...UniverseOption) *Universe { - universe := &Universe{} - - // Apply options - for _, option := range options { - option(universe) - } - - return universe -} - -// UniverseOption configure Universe -type UniverseOption func(*Universe) - -// WithConfig stores the already loaded project configuration -func WithConfig(projectConfig config.Config) UniverseOption { - return func(universe *Universe) { - universe.Config = projectConfig - } -} - -// WithBoilerplate stores the already loaded project configuration -func WithBoilerplate(boilerplate string) UniverseOption { - return func(universe *Universe) { - universe.Boilerplate = boilerplate - } -} - -// WithoutBoilerplate is used for files that do not require a boilerplate -func WithoutBoilerplate(universe *Universe) { - universe.Boilerplate = "" -} - -// WithResource stores the provided resource -func WithResource(resource *resource.Resource) UniverseOption { - return func(universe *Universe) { - universe.Resource = resource - } -} - -// 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/plugin/errors.go b/pkg/plugin/errors.go new file mode 100644 index 00000000000..4297ddca6ab --- /dev/null +++ b/pkg/plugin/errors.go @@ -0,0 +1,32 @@ +/* +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" +) + +// 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/pkg/plugin/errors_test.go b/pkg/plugin/errors_test.go new file mode 100644 index 00000000000..5d295b8212c --- /dev/null +++ b/pkg/plugin/errors_test.go @@ -0,0 +1,35 @@ +/* +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/gomega" +) + +var _ = Describe("PluginKeyNotFoundError", func() { + var err = ExitError{ + Plugin: "go.kubebuilder.io/v1", + Reason: "skipping plugin", + } + + 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 60% rename from pkg/plugin/interfaces.go rename to pkg/plugin/plugin.go index 1c1cf2da01a..131cd555aa4 100644 --- a/pkg/plugin/interfaces.go +++ b/pkg/plugin/plugin.go @@ -17,8 +17,6 @@ limitations under the License. package plugin import ( - "github.com/spf13/pflag" - "sigs.k8s.io/kubebuilder/v3/pkg/config" ) @@ -43,32 +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. - BindFlags(*pflag.FlagSet) - // Run runs the subcommand. - Run() error - // 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) -} - -// 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 @@ -76,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 @@ -88,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 @@ -100,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 @@ -112,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 @@ -124,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/plugins/internal/util/exec.go b/pkg/plugin/util/exec.go similarity index 100% rename from pkg/plugins/internal/util/exec.go rename to pkg/plugin/util/exec.go diff --git a/pkg/plugin/util/helpers.go b/pkg/plugin/util/helpers.go new file mode 100644 index 00000000000..2bd5a2de52e --- /dev/null +++ b/pkg/plugin/util/helpers.go @@ -0,0 +1,35 @@ +/* +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 util + +import ( + "sigs.k8s.io/kubebuilder/v3/pkg/config" +) + +// HasDifferentCRDVersion returns true if any other CRD version is tracked in the project configuration. +func HasDifferentCRDVersion(config config.Config, crdVersion string) bool { + return hasDifferentAPIVersion(config.ListCRDVersions(), crdVersion) +} + +// HasDifferentWebhookVersion returns true if any other webhook version is tracked in the project configuration. +func HasDifferentWebhookVersion(config config.Config, webhookVersion string) bool { + return hasDifferentAPIVersion(config.ListWebhookVersions(), webhookVersion) +} + +func hasDifferentAPIVersion(versions []string, version string) bool { + return !(len(versions) == 0 || (len(versions) == 1 && versions[0] == version)) +} diff --git a/pkg/plugin/util/helpers_test.go b/pkg/plugin/util/helpers_test.go new file mode 100644 index 00000000000..342d0f07f9d --- /dev/null +++ b/pkg/plugin/util/helpers_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 util + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugin Util Suite") +} + +var _ = Describe("hasDifferentAPIVersion", func() { + DescribeTable("should return false", + func(versions []string) { Expect(hasDifferentAPIVersion(versions, "v1")).To(BeFalse()) }, + Entry("for an empty list of versions", []string{}), + Entry("for a list of only that version", []string{"v1"}), + ) + + DescribeTable("should return true", + func(versions []string) { Expect(hasDifferentAPIVersion(versions, "v1")).To(BeTrue()) }, + Entry("for a list of only a different version", []string{"v2"}), + Entry("for a list of several different versions", []string{"v2", "v3"}), + Entry("for a list of several versions containing that version", []string{"v1", "v2"}), + ) +}) diff --git a/pkg/plugins/internal/util/stdin.go b/pkg/plugin/util/stdin.go similarity index 100% rename from pkg/plugins/internal/util/stdin.go rename to pkg/plugin/util/stdin.go 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/internal/util/go_version.go b/pkg/plugins/golang/go_version.go similarity index 56% rename from pkg/plugins/internal/util/go_version.go rename to pkg/plugins/golang/go_version.go index fb71f7589cb..78fe13dcccd 100644 --- a/pkg/plugins/internal/util/go_version.go +++ b/pkg/plugins/golang/go_version.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package util +package golang import ( "fmt" @@ -24,6 +24,96 @@ import ( "strings" ) +const ( + goVerPattern = `^go(?P[0-9]+)\.(?P[0-9]+)(?:\.(?P[0-9]+)|(?P
(?:alpha|beta|rc)[0-9]+))?$`
+)
+
+var (
+	go113 = goVersion{
+		major: 1,
+		minor: 13,
+	}
+	go116alpha1 = goVersion{
+		major:      1,
+		minor:      16,
+		prerelease: "alpha1",
+	}
+
+	goVerRegexp = regexp.MustCompile(goVerPattern)
+)
+
+type goVersion struct {
+	major, minor, patch int
+	prerelease          string
+}
+
+func (v *goVersion) parse(verStr string) error {
+	m := goVerRegexp.FindStringSubmatch(verStr)
+	if m == nil {
+		return fmt.Errorf("invalid version string")
+	}
+
+	var err error
+
+	v.major, err = strconv.Atoi(m[1])
+	if err != nil {
+		return fmt.Errorf("error parsing major version '%s': %s", m[1], err)
+	}
+
+	v.minor, err = strconv.Atoi(m[2])
+	if err != nil {
+		return fmt.Errorf("error parsing minor version '%s': %s", m[2], err)
+	}
+
+	if m[3] != "" {
+		v.patch, err = strconv.Atoi(m[3])
+		if err != nil {
+			return fmt.Errorf("error parsing patch version '%s': %s", m[2], err)
+		}
+	}
+
+	v.prerelease = m[4]
+
+	return nil
+}
+
+func (v goVersion) compare(other goVersion) int {
+	if v.major > other.major {
+		return 1
+	}
+	if v.major < other.major {
+		return -1
+	}
+
+	if v.minor > other.minor {
+		return 1
+	}
+	if v.minor < other.minor {
+		return -1
+	}
+
+	if v.patch > other.patch {
+		return 1
+	}
+	if v.patch < other.patch {
+		return -1
+	}
+
+	if v.prerelease == other.prerelease {
+		return 0
+	}
+	if v.prerelease == "" {
+		return 1
+	}
+	if other.prerelease == "" {
+		return -1
+	}
+	if v.prerelease > other.prerelease {
+		return 1
+	}
+	return -1
+}
+
 // ValidateGoVersion verifies that Go is installed and the current go version is supported by kubebuilder
 func ValidateGoVersion() error {
 	err := fetchAndCheckGoVersion()
@@ -54,24 +144,13 @@ func fetchAndCheckGoVersion() error {
 // checkGoVersion should only ever check if the Go version >= 1.13, since the kubebuilder binary only cares
 // that the go binary supports go modules which were stabilized in that version (i.e. in go 1.13) by default
 func checkGoVersion(verStr string) error {
-	goVerRegex := `^go?([0-9]+)\.([0-9]+)[\.0-9A-Za-z\-]*$`
-	m := regexp.MustCompile(goVerRegex).FindStringSubmatch(verStr)
-	if m == nil {
-		return fmt.Errorf("invalid version string")
-	}
-
-	major, err := strconv.Atoi(m[1])
-	if err != nil {
-		return fmt.Errorf("error parsing major version '%s': %s", m[1], err)
-	}
-
-	minor, err := strconv.Atoi(m[2])
-	if err != nil {
-		return fmt.Errorf("error parsing minor version '%s': %s", m[2], err)
+	var version goVersion
+	if err := version.parse(verStr); err != nil {
+		return err
 	}
 
-	if major < 1 || minor < 13 {
-		return fmt.Errorf("requires version >= 1.13")
+	if version.compare(go113) < 0 || version.compare(go116alpha1) >= 0 {
+		return fmt.Errorf("requires 1.13 <= version < 1.16")
 	}
 
 	return nil
diff --git a/pkg/plugins/golang/go_version_test.go b/pkg/plugins/golang/go_version_test.go
new file mode 100644
index 00000000000..ccba6bd9b34
--- /dev/null
+++ b/pkg/plugins/golang/go_version_test.go
@@ -0,0 +1,189 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package golang
+
+import (
+	"sort"
+
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/ginkgo/extensions/table"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("goVersion", func() {
+	Context("parse", func() {
+		var v goVersion
+
+		BeforeEach(func() {
+			v = goVersion{}
+		})
+
+		DescribeTable("should succeed for valid versions",
+			func(version string, expected goVersion) {
+				Expect(v.parse(version)).NotTo(HaveOccurred())
+				Expect(v.major).To(Equal(expected.major))
+				Expect(v.minor).To(Equal(expected.minor))
+				Expect(v.patch).To(Equal(expected.patch))
+				Expect(v.prerelease).To(Equal(expected.prerelease))
+			},
+			Entry("for minor release", "go1.15", goVersion{
+				major: 1,
+				minor: 15,
+			}),
+			Entry("for patch release", "go1.15.1", goVersion{
+				major: 1,
+				minor: 15,
+				patch: 1,
+			}),
+			Entry("for alpha release", "go1.15alpha1", goVersion{
+				major:      1,
+				minor:      15,
+				prerelease: "alpha1",
+			}),
+			Entry("for beta release", "go1.15beta1", goVersion{
+				major:      1,
+				minor:      15,
+				prerelease: "beta1",
+			}),
+			Entry("for release candidate", "go1.15rc1", goVersion{
+				major:      1,
+				minor:      15,
+				prerelease: "rc1",
+			}),
+		)
+
+		DescribeTable("should fail for invalid versions",
+			func(version string) { Expect(v.parse(version)).To(HaveOccurred()) },
+			Entry("for invalid prefix", "g1.15"),
+			Entry("for missing major version", "go.15"),
+			Entry("for missing minor version", "go1."),
+			Entry("for patch and prerelease version", "go1.15.1rc1"),
+			Entry("for invalid major version", "goa.15"),
+			Entry("for invalid minor version", "go1.a"),
+			Entry("for invalid patch version", "go1.15.a"),
+		)
+	})
+
+	Context("compare", func() {
+		// Test compare() by sorting a list.
+		var (
+			versions = []goVersion{
+				{major: 1, minor: 15, prerelease: "rc2"},
+				{major: 1, minor: 15, patch: 1},
+				{major: 1, minor: 16},
+				{major: 1, minor: 15, prerelease: "beta1"},
+				{major: 1, minor: 15, prerelease: "alpha2"},
+				{major: 2, minor: 0},
+				{major: 1, minor: 15, prerelease: "alpha1"},
+				{major: 1, minor: 13},
+				{major: 1, minor: 15, prerelease: "rc1"},
+				{major: 1, minor: 15},
+				{major: 1, minor: 15, patch: 2},
+				{major: 1, minor: 14},
+				{major: 1, minor: 15, prerelease: "beta2"},
+				{major: 0, minor: 123},
+			}
+
+			sortedVersions = []goVersion{
+				{major: 0, minor: 123},
+				{major: 1, minor: 13},
+				{major: 1, minor: 14},
+				{major: 1, minor: 15, prerelease: "alpha1"},
+				{major: 1, minor: 15, prerelease: "alpha2"},
+				{major: 1, minor: 15, prerelease: "beta1"},
+				{major: 1, minor: 15, prerelease: "beta2"},
+				{major: 1, minor: 15, prerelease: "rc1"},
+				{major: 1, minor: 15, prerelease: "rc2"},
+				{major: 1, minor: 15},
+				{major: 1, minor: 15, patch: 1},
+				{major: 1, minor: 15, patch: 2},
+				{major: 1, minor: 16},
+				{major: 2, minor: 0},
+			}
+		)
+
+		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
+			})
+			Expect(versions).To(Equal(sortedVersions))
+		})
+	})
+})
+
+var _ = Describe("checkGoVersion", func() {
+	DescribeTable("should return true for supported go versions",
+		func(version string) { Expect(checkGoVersion(version)).NotTo(HaveOccurred()) },
+		Entry("for go 1.13", "go1.13"),
+		Entry("for go 1.13.1", "go1.13.1"),
+		Entry("for go 1.13.2", "go1.13.2"),
+		Entry("for go 1.13.3", "go1.13.3"),
+		Entry("for go 1.13.4", "go1.13.4"),
+		Entry("for go 1.13.5", "go1.13.5"),
+		Entry("for go 1.13.6", "go1.13.6"),
+		Entry("for go 1.13.7", "go1.13.7"),
+		Entry("for go 1.13.8", "go1.13.8"),
+		Entry("for go 1.13.9", "go1.13.9"),
+		Entry("for go 1.13.10", "go1.13.10"),
+		Entry("for go 1.13.11", "go1.13.11"),
+		Entry("for go 1.13.12", "go1.13.12"),
+		Entry("for go 1.13.13", "go1.13.13"),
+		Entry("for go 1.13.14", "go1.13.14"),
+		Entry("for go 1.13.15", "go1.13.15"),
+		Entry("for go 1.14beta1", "go1.14beta1"),
+		Entry("for go 1.14rc1", "go1.14rc1"),
+		Entry("for go 1.14", "go1.14"),
+		Entry("for go 1.14.1", "go1.14.1"),
+		Entry("for go 1.14.2", "go1.14.2"),
+		Entry("for go 1.14.3", "go1.14.3"),
+		Entry("for go 1.14.4", "go1.14.4"),
+		Entry("for go 1.14.5", "go1.14.5"),
+		Entry("for go 1.14.6", "go1.14.6"),
+		Entry("for go 1.14.7", "go1.14.7"),
+		Entry("for go 1.14.8", "go1.14.8"),
+		Entry("for go 1.14.9", "go1.14.9"),
+		Entry("for go 1.14.10", "go1.14.10"),
+		Entry("for go 1.14.11", "go1.14.11"),
+		Entry("for go 1.14.12", "go1.14.12"),
+		Entry("for go 1.14.13", "go1.14.13"),
+		Entry("for go 1.14.14", "go1.14.14"),
+		Entry("for go 1.14.15", "go1.14.15"),
+		Entry("for go 1.15beta1", "go1.15beta1"),
+		Entry("for go 1.15rc1", "go1.15rc1"),
+		Entry("for go 1.15rc2", "go1.15rc2"),
+		Entry("for go 1.15", "go1.15"),
+		Entry("for go 1.15.1", "go1.15.1"),
+		Entry("for go 1.15.2", "go1.15.2"),
+		Entry("for go 1.15.3", "go1.15.3"),
+		Entry("for go 1.15.4", "go1.15.4"),
+		Entry("for go 1.15.5", "go1.15.5"),
+		Entry("for go 1.15.6", "go1.15.6"),
+		Entry("for go 1.15.7", "go1.15.7"),
+		Entry("for go 1.15.8", "go1.15.8"),
+	)
+
+	DescribeTable("should return false for non-supported go versions",
+		func(version string) { Expect(checkGoVersion(version)).To(HaveOccurred()) },
+		Entry("for invalid go versions", "go"),
+		Entry("for go 1.13beta1", "go1.13beta1"),
+		Entry("for go 1.13rc1", "go1.13rc1"),
+		Entry("for go 1.13rc2", "go1.13rc2"),
+		Entry("for go 1.16beta1", "go1.16beta1"),
+		Entry("for go 1.16rc1", "go1.16rc1"),
+		Entry("for go 1.16", "go1.16"),
+	)
+})
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/internal/util/repository.go b/pkg/plugins/golang/repository.go
similarity index 88%
rename from pkg/plugins/internal/util/repository.go
rename to pkg/plugins/golang/repository.go
index dab8b89e425..47debda41c3 100644
--- a/pkg/plugins/internal/util/repository.go
+++ b/pkg/plugins/golang/repository.go
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package util
+package golang
 
 import (
 	"encoding/json"
@@ -34,12 +34,9 @@ type module struct {
 }
 
 // findGoModulePath finds the path of the current module, if present.
-func findGoModulePath(forceModules bool) (string, error) {
+func findGoModulePath() (string, error) {
 	cmd := exec.Command("go", "mod", "edit", "-json")
 	cmd.Env = append(cmd.Env, os.Environ()...)
-	if forceModules {
-		cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */)
-	}
 	out, err := cmd.Output()
 	if err != nil {
 		if exitErr, isExitErr := err.(*exec.ExitError); isExitErr {
@@ -58,7 +55,7 @@ func findGoModulePath(forceModules bool) (string, error) {
 // though a combination of go/packages and `go mod` commands/tricks.
 func FindCurrentRepo() (string, error) {
 	// easiest case: existing go module
-	path, err := findGoModulePath(false)
+	path, err := findGoModulePath()
 	if err == nil {
 		return path, nil
 	}
@@ -79,7 +76,6 @@ func FindCurrentRepo() (string, error) {
 	// otherwise, try to get `go mod init` to guess for us -- it's pretty good
 	cmd := exec.Command("go", "mod", "init")
 	cmd.Env = append(cmd.Env, os.Environ()...)
-	cmd.Env = append(cmd.Env, "GO111MODULE=on" /* turn on modules just for these commands */)
 	if _, err := cmd.Output(); err != nil {
 		if exitErr, isExitErr := err.(*exec.ExitError); isExitErr {
 			err = fmt.Errorf("%s", string(exitErr.Stderr))
@@ -90,5 +86,5 @@ func FindCurrentRepo() (string, error) {
 	}
 	//nolint:errcheck
 	defer os.Remove("go.mod") // clean up after ourselves
-	return findGoModulePath(true)
+	return findGoModulePath()
 }
diff --git a/pkg/plugins/golang/v2/api.go b/pkg/plugins/golang/v2/api.go
index bdca3497512..99704870d37 100644
--- a/pkg/plugins/golang/v2/api.go
+++ b/pkg/plugins/golang/v2/api.go
@@ -20,32 +20,27 @@ import (
 	"bufio"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"os"
-	"path/filepath"
-	"strings"
 
 	"github.com/spf13/pflag"
 
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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"
+	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/pkg/plugins/internal/util"
-	"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
@@ -58,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
@@ -89,32 +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")
-	p.options.Domain = p.config.GetDomain()
-	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,
@@ -122,11 +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
+
+	return nil
 }
 
-func (p *createAPISubcommand) Run() 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 {
@@ -138,16 +125,7 @@ func (p *createAPISubcommand) Run() error {
 		p.options.DoController = util.YesNo(reader)
 	}
 
-	// Create the resource from the options
-	p.resource = p.options.NewResource(p.config)
-
-	return cmdutil.Run(p)
-}
-
-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
@@ -163,52 +141,31 @@ func (p *createAPISubcommand) Validate() error {
 		// Check that the provided group can be added to the project
 		if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
 			return fmt.Errorf("multiple groups are not allowed by default, to enable multi-group visit %s",
-				"kubebuilder.io/migration/multi-group.html")
+				"https://kubebuilder.io/migration/multi-group.html")
 		}
 	}
 
 	return nil
 }
 
-func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	// Load the boilerplate
-	bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec
-	if err != nil {
-		return nil, fmt.Errorf("unable to load boilerplate: %v", err)
-	}
-
-	// 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, string(bp), 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)
+	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
+	if err != nil {
+		return err
+	}
+
+	if p.runMake && p.resource.HasAPI() {
+		err = util.RunCmd("Running make", "make", "generate")
 		if err != nil {
 			return err
 		}
-	default:
-		return fmt.Errorf("unknown pattern %q", p.pattern)
 	}
 
-	if p.runMake { // TODO: check if API was scaffolded
-		return util.RunCmd("Running make", "make", "generate")
-	}
 	return nil
 }
diff --git a/pkg/plugins/golang/v2/edit.go b/pkg/plugins/golang/v2/edit.go
index 7c232a44bec..bdab9054400 100644
--- a/pkg/plugins/golang/v2/edit.go
+++ b/pkg/plugins/golang/v2/edit.go
@@ -22,53 +22,44 @@ 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/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
-        %s edit --multigroup
-
-        # Disable the multigroup layout
-        %s edit --multigroup=false
-	`, ctx.CommandName, 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() error {
-	return cmdutil.Run(p)
-}
-
-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 af767e74c65..6c9c7bbdb3d 100644
--- a/pkg/plugins/golang/v2/init.go
+++ b/pkg/plugins/golang/v2/init.go
@@ -27,12 +27,15 @@ import (
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
 	"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
+	"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"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util"
 )
 
+var _ plugin.InitSubcommand = &initSubcommand{}
+
 type initSubcommand struct {
 	config config.Config
 
@@ -53,30 +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) {
@@ -95,30 +90,26 @@ func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
 	fs.StringVar(&p.domain, "domain", "my.domain", "domain for groups")
 	fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
 		"defaults to the go package of the current working directory.")
-	if p.config.GetVersion().Compare(cfgv2.Version) > 0 {
-		fs.StringVar(&p.name, "project-name", "", "name of this project")
-	}
+	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.SetLayout(plugin.KeyFor(Plugin{}))
-	}
-
+func (p *initSubcommand) InjectConfig(c config.Config) error {
 	p.config = c
-}
 
-func (p *initSubcommand) Run() error {
-	return cmdutil.Run(p)
-}
+	if err := p.config.SetDomain(p.domain); err != nil {
+		return err
+	}
 
-func (p *initSubcommand) Validate() error {
-	// Requires go1.11+
-	if !p.skipGoVersionCheck {
-		if err := util.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 {
@@ -134,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 := util.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
@@ -172,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 go.mod", "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 353aac0e5f0..46d7f20fc42 100644
--- a/pkg/plugins/golang/v2/scaffolds/api.go
+++ b/pkg/plugins/golang/v2/scaffolds/api.go
@@ -19,10 +19,13 @@ package scaffolds
 import (
 	"fmt"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	cfgv2 "sigs.k8s.io/kubebuilder/v3/pkg/config/v2"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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/config/crd"
@@ -30,62 +33,55 @@ 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/samples"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/controllers"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack"
 )
 
-// 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.
 type apiScaffolder struct {
-	config      config.Config
-	boilerplate string
-	resource    resource.Resource
+	config   config.Config
+	resource resource.Resource
 
-	// plugins is the list of plugins we should allow to transform our generated scaffolding
-	plugins []model.Plugin
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 
 	// 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,
-	boilerplate string,
-	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,
-		boilerplate: boilerplate,
-		resource:    res,
-		plugins:     plugins,
-		force:       force,
+		config:   config,
+		resource: res,
+		force:    force,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *apiScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-func (s *apiScaffolder) newUniverse() *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(s.boilerplate),
-		model.WithResource(&s.resource),
+	// Load the boilerplate
+	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+	if err != nil {
+		return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
+	}
+
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+		machinery.WithResource(&s.resource),
 	)
-}
 
-func (s *apiScaffolder) scaffold() error {
 	// Keep track of these values before the update
 	doAPI := s.resource.HasAPI()
 	doController := s.resource.HasController()
@@ -103,9 +99,7 @@ func (s *apiScaffolder) scaffold() error {
 	}
 
 	if doAPI {
-
-		if err := machinery.NewScaffold(s.plugins...).Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&api.Types{Force: s.force},
 			&api.Group{},
 			&samples.CRDSample{Force: s.force},
@@ -117,19 +111,16 @@ func (s *apiScaffolder) scaffold() error {
 			return fmt.Errorf("error scaffolding APIs: %w", err)
 		}
 
-		if err := machinery.NewScaffold().Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&crd.Kustomization{},
 			&crd.KustomizeConfig{},
 		); err != nil {
 			return fmt.Errorf("error scaffolding kustomization: %v", err)
 		}
-
 	}
 
 	if doController {
-		if err := machinery.NewScaffold(s.plugins...).Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&controllers.SuiteTest{Force: s.force},
 			&controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
 		); err != nil {
@@ -137,8 +128,7 @@ func (s *apiScaffolder) scaffold() error {
 		}
 	}
 
-	if err := machinery.NewScaffold(s.plugins...).Execute(
-		s.newUniverse(),
+	if err := scaffold.Execute(
 		&templates.MainUpdater{WireResource: doAPI, WireController: doController},
 	); err != nil {
 		return fmt.Errorf("error updating main.go: %v", err)
diff --git a/pkg/plugins/golang/v2/scaffolds/edit.go b/pkg/plugins/golang/v2/scaffolds/edit.go
index 4c5def9d576..0e4f39751c2 100644
--- a/pkg/plugins/golang/v2/scaffolds/edit.go
+++ b/pkg/plugins/golang/v2/scaffolds/edit.go
@@ -18,32 +18,42 @@ package scaffolds
 
 import (
 	"fmt"
-	"io/ioutil"
 	"strings"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins"
 )
 
-var _ cmdutil.Scaffolder = &editScaffolder{}
+var _ plugins.Scaffolder = &editScaffolder{}
 
 type editScaffolder struct {
 	config     config.Config
 	multigroup bool
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 }
 
 // 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,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *editScaffolder) Scaffold() error {
 	filename := "Dockerfile"
-	bs, err := ioutil.ReadFile(filename)
+	bs, err := afero.ReadFile(s.fs.FS, filename)
 	if err != nil {
 		return err
 	}
@@ -76,9 +86,8 @@ func (s *editScaffolder) Scaffold() error {
 	// Check if the str is not empty, because when the file is already in desired format it will return empty string
 	// because there is nothing to replace.
 	if str != "" {
-		// false positive
-		// nolint:gosec
-		return ioutil.WriteFile(filename, []byte(str), 0644)
+		// TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
+		return afero.WriteFile(s.fs.FS, filename, []byte(str), 0644)
 	}
 
 	return nil
diff --git a/pkg/plugins/golang/v2/scaffolds/init.go b/pkg/plugins/golang/v2/scaffolds/init.go
index d0674f28395..4651a109914 100644
--- a/pkg/plugins/golang/v2/scaffolds/init.go
+++ b/pkg/plugins/golang/v2/scaffolds/init.go
@@ -18,11 +18,12 @@ package scaffolds
 
 import (
 	"fmt"
-	"io/ioutil"
-	"path/filepath"
+
+	"github.com/spf13/afero"
 
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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,8 +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"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
 )
 
 const (
@@ -46,66 +45,78 @@ const (
 	imageName = "controller:latest"
 )
 
-var _ cmdutil.Scaffolder = &initScaffolder{}
+var _ plugins.Scaffolder = &initScaffolder{}
 
 type initScaffolder struct {
 	config          config.Config
 	boilerplatePath string
 	license         string
 	owner           string
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 }
 
 // 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: filepath.Join("hack", "boilerplate.go.txt"),
+		boilerplatePath: hack.DefaultBoilerplatePath,
 		license:         license,
 		owner:           owner,
 	}
 }
 
-func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(boilerplate),
-	)
+// InjectFS implements cmdutil.Scaffolder
+func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
 }
 
-// Scaffold implements Scaffolder
+// Scaffold implements cmdutil.Scaffolder
 func (s *initScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-func (s *initScaffolder) scaffold() error {
-	bpFile := &hack.Boilerplate{}
+	// Initialize the machinery.Scaffold that will write the boilerplate file to disk
+	// The boilerplate file needs to be scaffolded as a separate step as it is going to
+	// be used by the rest of the files, even those scaffolded in this command call.
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+	)
+
+	bpFile := &hack.Boilerplate{
+		License: s.license,
+		Owner:   s.owner,
+	}
 	bpFile.Path = s.boilerplatePath
-	bpFile.License = s.license
-	bpFile.Owner = s.owner
-	if err := machinery.NewScaffold().Execute(
-		s.newUniverse(""),
-		bpFile,
-	); err != nil {
+	if err := scaffold.Execute(bpFile); err != nil {
 		return err
 	}
 
-	boilerplate, err := ioutil.ReadFile(s.boilerplatePath) //nolint:gosec
+	boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
 	if err != nil {
 		return err
 	}
 
-	return machinery.NewScaffold().Execute(
-		s.newUniverse(string(boilerplate)),
-		&templates.GitIgnore{},
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold = machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+	)
+
+	return scaffold.Execute(
+		&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,
@@ -114,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 4ee7d32c1c2..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
@@ -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/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 561d4befbdc..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,15 +21,18 @@ import (
 	"path/filepath"
 	"time"
 
-	"sigs.k8s.io/kubebuilder/v3/pkg/model/file"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
 )
 
-var _ file.Template = &Boilerplate{}
+// DefaultBoilerplatePath is the default path to the boilerplate file
+var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")
+
+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
@@ -62,7 +65,7 @@ func (f Boilerplate) Validate() error {
 // SetTemplateDefaults implements file.Template
 func (f *Boilerplate) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("hack", "boilerplate.go.txt")
+		f.Path = DefaultBoilerplatePath
 	}
 
 	if f.License == "" {
diff --git a/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v2/scaffolds/internal/templates/main.go
index e9f4b64362d..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),
 	}
 }
 
@@ -92,9 +92,6 @@ const (
 `
 	controllerImportCodeFragment = `"%s/controllers"
 `
-	// TODO(v3): `&%scontrollers` should be used instead of `&%scontroller` as there may be multiple
-	//  controller for different Kinds in the same group. However, this is a backwards incompatible
-	//  change, and thus should be done for next project version.
 	multiGroupControllerImportCodeFragment = `%scontroller "%s/controllers/%s"
 `
 	addschemeCodeFragment = `utilruntime.Must(%s.AddToScheme(scheme))
@@ -108,9 +105,6 @@ const (
 		os.Exit(1)
 	}
 `
-	// TODO(v3): loggers for the same Kind controllers from different groups use the same logger.
-	//  `.WithName("controllers").WithName(GROUP).WithName(KIND)` should be used instead. However,
-	//  this is a backwards incompatible change, and thus should be done for next project version.
 	multiGroupReconcilerSetupCodeFragment = `if err = (&%scontroller.%sReconciler{
 		Client: mgr.GetClient(),
 		Log: ctrl.Log.WithName("controllers").WithName("%s"),
@@ -128,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 {
@@ -175,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 d60175f8c13..d3cef711cad 100644
--- a/pkg/plugins/golang/v2/scaffolds/webhook.go
+++ b/pkg/plugins/golang/v2/scaffolds/webhook.go
@@ -19,57 +19,62 @@ package scaffolds
 import (
 	"fmt"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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/internal/cmdutil"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v2/scaffolds/internal/templates/hack"
 )
 
-var _ cmdutil.Scaffolder = &webhookScaffolder{}
+var _ plugins.Scaffolder = &webhookScaffolder{}
 
 type webhookScaffolder struct {
-	config      config.Config
-	boilerplate string
-	resource    resource.Resource
+	config   config.Config
+	resource resource.Resource
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 }
 
 // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
-func NewWebhookScaffolder(
-	config config.Config,
-	boilerplate string,
-	resource resource.Resource,
-) cmdutil.Scaffolder {
+func NewWebhookScaffolder(config config.Config, resource resource.Resource) plugins.Scaffolder {
 	return &webhookScaffolder{
-		config:      config,
-		boilerplate: boilerplate,
-		resource:    resource,
+		config:   config,
+		resource: resource,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *webhookScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-func (s *webhookScaffolder) newUniverse() *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(s.boilerplate),
-		model.WithResource(&s.resource),
+	// Load the boilerplate
+	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+	if err != nil {
+		return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err)
+	}
+
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+		machinery.WithResource(&s.resource),
 	)
-}
 
-func (s *webhookScaffolder) scaffold() error {
 	if err := s.config.UpdateResource(s.resource); err != nil {
 		return fmt.Errorf("error updating resource: %w", err)
 	}
 
-	if err := machinery.NewScaffold().Execute(
-		s.newUniverse(),
+	if err := scaffold.Execute(
 		&api.Webhook{},
 		&templates.MainUpdater{WireWebhook: true},
 	); err != nil {
diff --git a/pkg/plugins/golang/v2/webhook.go b/pkg/plugins/golang/v2/webhook.go
index 735c48c68df..d142298850b 100644
--- a/pkg/plugins/golang/v2/webhook.go
+++ b/pkg/plugins/golang/v2/webhook.go
@@ -18,59 +18,51 @@ package v2
 
 import (
 	"fmt"
-	"io/ioutil"
-	"path/filepath"
 
 	"github.com/spf13/pflag"
 
-	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/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 newconfig.Config
+	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.
-  %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.
-  %s create webhook --group ship --version v1beta1 --kind Frigate --conversion
-`,
-		ctx.CommandName, ctx.CommandName)
-
-	p.commandName = ctx.CommandName
+	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 Group: ship, Version: v1beta1
+  # and Kind: Frigate
+  %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion
+`, cliMeta.CommandName)
 }
 
 func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
-	p.options = &Options{}
-	fs.StringVar(&p.options.Group, "group", "", "resource Group")
-	p.options.Domain = p.config.GetDomain()
-	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,
@@ -79,22 +71,21 @@ func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
 		"if set, scaffold the conversion webhook")
 }
 
-func (p *createWebhookSubcommand) InjectConfig(c newconfig.Config) {
+func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
 	p.config = c
-}
 
-func (p *createWebhookSubcommand) Run() error {
-	// Create the resource from the options
-	p.resource = p.options.NewResource(p.config)
-
-	return cmdutil.Run(p)
+	return nil
 }
 
-func (p *createWebhookSubcommand) Validate() error {
-	if err := p.options.Validate(); err != nil {
-		return err
+func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
+	p.resource = res
+
+	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
 	}
@@ -120,16 +111,8 @@ func (p *createWebhookSubcommand) Validate() error {
 	return nil
 }
 
-func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	// Load the boilerplate
-	bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec
-	if err != nil {
-		return nil, fmt.Errorf("unable to load boilerplate: %v", err)
-	}
-
-	return scaffolds.NewWebhookScaffolder(p.config, string(bp), 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 f71bc7e5e4f..71d4443e93d 100644
--- a/pkg/plugins/golang/v3/api.go
+++ b/pkg/plugins/golang/v3/api.go
@@ -20,30 +20,20 @@ import (
 	"bufio"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"os"
-	"path/filepath"
-	"strings"
 
 	"github.com/spf13/pflag"
 
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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"
 	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/pkg/plugins/internal/util"
-	"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"
 )
@@ -51,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
@@ -72,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
@@ -103,27 +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")
-	p.options.Domain = p.config.GetDomain()
-	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,
@@ -138,13 +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
+
+	return nil
 }
 
-func (p *createAPISubcommand) Run() 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,41 +134,27 @@ func (p *createAPISubcommand) Run() error {
 		p.options.DoController = util.YesNo(reader)
 	}
 
-	// Create the resource from the options
-	p.resource = p.options.NewResource(p.config)
-
-	return cmdutil.Run(p)
-}
-
-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")
 		}
 
 		// Check that the provided group can be added to the project
 		if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
 			return fmt.Errorf("multiple groups are not allowed by default, " +
-				"to enable multi-group visit kubebuilder.io/migration/multi-group.html")
+				"to enable multi-group visit https://kubebuilder.io/migration/multi-group.html")
 		}
 
 		// Check CRDVersion against all other CRDVersions in p.config for compatibility.
-		if !p.config.IsCRDVersionCompatible(p.resource.API.CRDVersion) {
+		if util.HasDifferentCRDVersion(p.config, p.resource.API.CRDVersion) {
 			return fmt.Errorf("only one CRD version can be used for all resources, cannot add %q",
 				p.resource.API.CRDVersion)
 		}
@@ -198,46 +163,32 @@ func (p *createAPISubcommand) Validate() error {
 	return nil
 }
 
-func (p *createAPISubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	// Load the boilerplate
-	bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec
-	if err != nil {
-		return nil, fmt.Errorf("unable to load boilerplate: %v", err)
+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)
 	}
 
-	// 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 nil
+}
 
-	return scaffolds.NewAPIScaffolder(p.config, string(bp), 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
-		// 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)
+	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
+	if err != nil {
+		return err
+	}
+	if p.runMake && p.resource.HasAPI() {
+		err = util.RunCmd("Running make", "make", "generate")
 		if err != nil {
 			return err
 		}
-	default:
-		return fmt.Errorf("unknown pattern %q", p.pattern)
 	}
 
-	if p.runMake { // TODO: check if API was scaffolded
-		return util.RunCmd("Running make", "make", "generate")
-	}
 	return nil
 }
diff --git a/pkg/plugins/golang/v3/edit.go b/pkg/plugins/golang/v3/edit.go
index 503ed2fafe3..b75e7339bd6 100644
--- a/pkg/plugins/golang/v3/edit.go
+++ b/pkg/plugins/golang/v3/edit.go
@@ -22,53 +22,44 @@ 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/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
-        %s edit --multigroup
-
-        # Disable the multigroup layout
-        %s edit --multigroup=false
-	`, ctx.CommandName, 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() error {
-	return cmdutil.Run(p)
-}
-
-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 b2572c7bf4a..3c9883e7254 100644
--- a/pkg/plugins/golang/v3/init.go
+++ b/pkg/plugins/golang/v3/init.go
@@ -26,12 +26,15 @@ import (
 
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
 	"sigs.k8s.io/kubebuilder/v3/pkg/internal/validation"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
+	"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"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/util"
 )
 
+var _ plugin.InitSubcommand = &initSubcommand{}
+
 type initSubcommand struct {
 	config config.Config
 	// For help text.
@@ -52,30 +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) {
@@ -99,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.SetLayout(plugin.KeyFor(Plugin{}))
-
+func (p *initSubcommand) InjectConfig(c config.Config) error {
 	p.config = c
-}
 
-func (p *initSubcommand) Run() error {
-	return cmdutil.Run(p)
-}
+	if err := p.config.SetDomain(p.domain); err != nil {
+		return err
+	}
 
-func (p *initSubcommand) Validate() error {
-	// Requires go1.11+
-	if !p.skipGoVersionCheck {
-		if err := util.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
 	}
 
@@ -134,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 := util.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
@@ -174,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 go.mod", "go", "mod", "tidy")
+	return nil
+}
+
+func (p *initSubcommand) PostScaffold() error {
+	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
 	if err != nil {
 		return err
 	}
@@ -228,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 1d9698cafc3..5b31798200a 100644
--- a/pkg/plugins/golang/v3/scaffolds/api.go
+++ b/pkg/plugins/golang/v3/scaffolds/api.go
@@ -19,9 +19,12 @@ package scaffolds
 import (
 	"fmt"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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/crd"
@@ -29,59 +32,55 @@ import (
 	"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/config/samples"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/controllers"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack"
 )
 
-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.
 type apiScaffolder struct {
-	config      config.Config
-	boilerplate string
-	resource    resource.Resource
+	config   config.Config
+	resource resource.Resource
 
-	// plugins is the list of plugins we should allow to transform our generated scaffolding
-	plugins []model.Plugin
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 
 	// 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,
-	boilerplate string,
-	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,
-		boilerplate: boilerplate,
-		resource:    res,
-		plugins:     plugins,
-		force:       force,
+		config:   config,
+		resource: res,
+		force:    force,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *apiScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-func (s *apiScaffolder) newUniverse() *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(s.boilerplate),
-		model.WithResource(&s.resource),
+	// Load the boilerplate
+	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+	if err != nil {
+		return fmt.Errorf("error scaffolding API/controller: unable to load boilerplate: %w", err)
+	}
+
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+		machinery.WithResource(&s.resource),
 	)
-}
 
-// TODO: re-use universe created by s.newUniverse() if possible.
-func (s *apiScaffolder) scaffold() error {
 	// Keep track of these values before the update
 	doAPI := s.resource.HasAPI()
 	doController := s.resource.HasController()
@@ -92,8 +91,7 @@ func (s *apiScaffolder) scaffold() error {
 
 	if doAPI {
 
-		if err := machinery.NewScaffold(s.plugins...).Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&api.Types{Force: s.force},
 			&api.Group{},
 			&samples.CRDSample{Force: s.force},
@@ -105,8 +103,7 @@ func (s *apiScaffolder) scaffold() error {
 			return fmt.Errorf("error scaffolding APIs: %v", err)
 		}
 
-		if err := machinery.NewScaffold().Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&crd.Kustomization{},
 			&crd.KustomizeConfig{},
 		); err != nil {
@@ -116,8 +113,7 @@ func (s *apiScaffolder) scaffold() error {
 	}
 
 	if doController {
-		if err := machinery.NewScaffold(s.plugins...).Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&controllers.SuiteTest{Force: s.force},
 			&controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
 		); err != nil {
@@ -125,8 +121,7 @@ func (s *apiScaffolder) scaffold() error {
 		}
 	}
 
-	if err := machinery.NewScaffold(s.plugins...).Execute(
-		s.newUniverse(),
+	if err := scaffold.Execute(
 		&templates.MainUpdater{WireResource: doAPI, WireController: doController},
 	); err != nil {
 		return fmt.Errorf("error updating main.go: %v", err)
diff --git a/pkg/plugins/golang/v3/scaffolds/edit.go b/pkg/plugins/golang/v3/scaffolds/edit.go
index f4f92785359..e099a8a7b16 100644
--- a/pkg/plugins/golang/v3/scaffolds/edit.go
+++ b/pkg/plugins/golang/v3/scaffolds/edit.go
@@ -18,32 +18,42 @@ package scaffolds
 
 import (
 	"fmt"
-	"io/ioutil"
 	"strings"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/cmdutil"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins"
 )
 
-var _ cmdutil.Scaffolder = &editScaffolder{}
+var _ plugins.Scaffolder = &editScaffolder{}
 
 type editScaffolder struct {
 	config     config.Config
 	multigroup bool
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 }
 
 // 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,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *editScaffolder) Scaffold() error {
 	filename := "Dockerfile"
-	bs, err := ioutil.ReadFile(filename)
+	bs, err := afero.ReadFile(s.fs.FS, filename)
 	if err != nil {
 		return err
 	}
@@ -77,9 +87,8 @@ func (s *editScaffolder) Scaffold() error {
 	// Check if the str is not empty, because when the file is already in desired format it will return empty string
 	// because there is nothing to replace.
 	if str != "" {
-		// false positive
-		// nolint:gosec
-		return ioutil.WriteFile(filename, []byte(str), 0644)
+		// TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
+		return afero.WriteFile(s.fs.FS, filename, []byte(str), 0644)
 	}
 
 	return nil
diff --git a/pkg/plugins/golang/v3/scaffolds/init.go b/pkg/plugins/golang/v3/scaffolds/init.go
index e46478d42ed..1b7b6ece584 100644
--- a/pkg/plugins/golang/v3/scaffolds/init.go
+++ b/pkg/plugins/golang/v3/scaffolds/init.go
@@ -18,11 +18,12 @@ package scaffolds
 
 import (
 	"fmt"
-	"io/ioutil"
-	"path/filepath"
+
+	"github.com/spf13/afero"
 
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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,13 +31,11 @@ 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"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
 )
 
 const (
 	// ControllerRuntimeVersion is the kubernetes-sigs/controller-runtime version to be used in the project
-	ControllerRuntimeVersion = "v0.7.0"
+	ControllerRuntimeVersion = "v0.7.2"
 	// ControllerToolsVersion is the kubernetes-sigs/controller-tools version to be used in the project
 	ControllerToolsVersion = "v0.4.1"
 	// KustomizeVersion is the kubernetes-sigs/kustomize version to be used in the project
@@ -45,58 +44,65 @@ const (
 	imageName = "controller:latest"
 )
 
-var _ cmdutil.Scaffolder = &initScaffolder{}
+var _ plugins.Scaffolder = &initScaffolder{}
 
 type initScaffolder struct {
 	config          config.Config
 	boilerplatePath string
 	license         string
 	owner           string
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 }
 
 // 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: filepath.Join("hack", "boilerplate.go.txt"),
+		boilerplatePath: hack.DefaultBoilerplatePath,
 		license:         license,
 		owner:           owner,
 	}
 }
 
-func (s *initScaffolder) newUniverse(boilerplate string) *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(boilerplate),
-	)
+// InjectFS implements cmdutil.Scaffolder
+func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
 }
 
-// Scaffold implements Scaffolder
+// Scaffold implements cmdutil.Scaffolder
 func (s *initScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-// TODO: re-use universe created by s.newUniverse() if possible.
-func (s *initScaffolder) scaffold() error {
-	bpFile := &hack.Boilerplate{}
+	// Initialize the machinery.Scaffold that will write the boilerplate file to disk
+	// The boilerplate file needs to be scaffolded as a separate step as it is going to
+	// be used by the rest of the files, even those scaffolded in this command call.
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+	)
+
+	bpFile := &hack.Boilerplate{
+		License: s.license,
+		Owner:   s.owner,
+	}
 	bpFile.Path = s.boilerplatePath
-	bpFile.License = s.license
-	bpFile.Owner = s.owner
-	if err := machinery.NewScaffold().Execute(
-		s.newUniverse(""),
-		bpFile,
-	); err != nil {
+	if err := scaffold.Execute(bpFile); err != nil {
 		return err
 	}
 
-	boilerplate, err := ioutil.ReadFile(s.boilerplatePath) //nolint:gosec
+	boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
 	if err != nil {
 		return err
 	}
 
-	return machinery.NewScaffold().Execute(
-		s.newUniverse(string(boilerplate)),
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold = machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+	)
+
+	return scaffold.Execute(
 		&rbac.Kustomization{},
 		&rbac.AuthProxyRole{},
 		&rbac.AuthProxyRoleBinding{},
@@ -105,6 +111,7 @@ func (s *initScaffolder) scaffold() error {
 		&rbac.RoleBinding{},
 		&rbac.LeaderElectionRole{},
 		&rbac.LeaderElectionRoleBinding{},
+		&rbac.ServiceAccount{},
 		&manager.Kustomization{},
 		&manager.Config{Image: imageName},
 		&manager.ControllerManagerConfig{},
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 96663a7a6c9..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
@@ -100,5 +100,6 @@ spec:
           requests:
             cpu: 100m
             memory: 20Mi
+      serviceAccountName: controller-manager
       terminationGracePeriodSeconds: 10
 `
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 4ee7d32c1c2..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
@@ -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/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 3834de9bce4..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
@@ -50,6 +50,6 @@ roleRef:
   name: proxy-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
 `
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 0966bd6d5d9..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,12 +37,18 @@ func (f *Kustomization) SetTemplateDefaults() error {
 
 	f.TemplateBody = kustomizeRBACTemplate
 
-	f.IfExistsAction = file.Error
+	f.IfExistsAction = machinery.Error
 
 	return nil
 }
 
 const kustomizeRBACTemplate = `resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
 - role.yaml
 - role_binding.yaml
 - leader_election_role.yaml
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 8c67aac8a54..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
@@ -50,6 +50,6 @@ roleRef:
   name: leader-election-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
 `
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 40f1bd783e1..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
@@ -50,6 +50,6 @@ roleRef:
   name: manager-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
 `
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
new file mode 100644
index 00000000000..14227777734
--- /dev/null
+++ b/pkg/plugins/golang/v3/scaffolds/internal/templates/config/rbac/service_account.go
@@ -0,0 +1,48 @@
+/*
+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 rbac
+
+import (
+	"path/filepath"
+
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
+)
+
+var _ machinery.Template = &ServiceAccount{}
+
+// ServiceAccount scaffolds a file that defines the service account the manager is deployed in.
+type ServiceAccount struct {
+	machinery.TemplateMixin
+}
+
+// SetTemplateDefaults implements file.Template
+func (f *ServiceAccount) SetTemplateDefaults() error {
+	if f.Path == "" {
+		f.Path = filepath.Join("config", "rbac", "service_account.yaml")
+	}
+
+	f.TemplateBody = serviceAccountTemplate
+
+	return nil
+}
+
+const serviceAccountTemplate = `apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: controller-manager
+  namespace: system
+`
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 e8a21cbe6ce..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,15 +21,18 @@ import (
 	"path/filepath"
 	"time"
 
-	"sigs.k8s.io/kubebuilder/v3/pkg/model/file"
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
 )
 
-var _ file.Template = &Boilerplate{}
+// DefaultBoilerplatePath is the default path to the boilerplate file
+var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")
+
+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
@@ -62,7 +65,7 @@ func (f Boilerplate) Validate() error {
 // SetTemplateDefaults implements file.Template
 func (f *Boilerplate) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("hack", "boilerplate.go.txt")
+		f.Path = DefaultBoilerplatePath
 	}
 
 	if f.License == "" {
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 b195ee7eef7..51a020e1495 100644
--- a/pkg/plugins/golang/v3/scaffolds/webhook.go
+++ b/pkg/plugins/golang/v3/scaffolds/webhook.go
@@ -19,58 +19,63 @@ package scaffolds
 import (
 	"fmt"
 
+	"github.com/spf13/afero"
+
 	"sigs.k8s.io/kubebuilder/v3/pkg/config"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
+	"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/internal/cmdutil"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/machinery"
+	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/golang/v3/scaffolds/internal/templates/hack"
 )
 
-var _ cmdutil.Scaffolder = &webhookScaffolder{}
+var _ plugins.Scaffolder = &webhookScaffolder{}
 
 type webhookScaffolder struct {
-	config      config.Config
-	boilerplate string
-	resource    resource.Resource
+	config   config.Config
+	resource resource.Resource
+
+	// fs is the filesystem that will be used by the scaffolder
+	fs machinery.Filesystem
 
 	// force indicates whether to scaffold controller files even if it exists or not
 	force bool
 }
 
 // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
-func NewWebhookScaffolder(
-	config config.Config,
-	boilerplate string,
-	resource resource.Resource,
-	force bool,
-) cmdutil.Scaffolder {
+func NewWebhookScaffolder(config config.Config, resource resource.Resource, force bool) plugins.Scaffolder {
 	return &webhookScaffolder{
-		config:      config,
-		boilerplate: boilerplate,
-		resource:    resource,
-		force:       force,
+		config:   config,
+		resource: resource,
+		force:    force,
 	}
 }
 
-// Scaffold implements Scaffolder
+// InjectFS implements cmdutil.Scaffolder
+func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
+	s.fs = fs
+}
+
+// Scaffold implements cmdutil.Scaffolder
 func (s *webhookScaffolder) Scaffold() error {
 	fmt.Println("Writing scaffold for you to edit...")
-	return s.scaffold()
-}
 
-func (s *webhookScaffolder) newUniverse() *model.Universe {
-	return model.NewUniverse(
-		model.WithConfig(s.config),
-		model.WithBoilerplate(s.boilerplate),
-		model.WithResource(&s.resource),
+	// Load the boilerplate
+	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
+	if err != nil {
+		return fmt.Errorf("error scaffolding webhook: unable to load boilerplate: %w", err)
+	}
+
+	// Initialize the machinery.Scaffold that will write the files to disk
+	scaffold := machinery.NewScaffold(s.fs,
+		machinery.WithConfig(s.config),
+		machinery.WithBoilerplate(string(boilerplate)),
+		machinery.WithResource(&s.resource),
 	)
-}
 
-func (s *webhookScaffolder) scaffold() error {
 	// Keep track of these values before the update
 	doDefaulting := s.resource.HasDefaultingWebhook()
 	doValidation := s.resource.HasValidationWebhook()
@@ -80,8 +85,7 @@ func (s *webhookScaffolder) scaffold() error {
 		return fmt.Errorf("error updating resource: %w", err)
 	}
 
-	if err := machinery.NewScaffold().Execute(
-		s.newUniverse(),
+	if err := scaffold.Execute(
 		&api.Webhook{Force: s.force},
 		&templates.MainUpdater{WireWebhook: true},
 		&kdefault.WebhookCAInjectionPatch{},
@@ -100,8 +104,7 @@ You need to implement the conversion.Hub and conversion.Convertible interfaces f
 
 	// TODO: Add test suite for conversion webhook after #1664 has been merged & conversion tests supported in envtest.
 	if doDefaulting || doValidation {
-		if err := machinery.NewScaffold().Execute(
-			s.newUniverse(),
+		if err := scaffold.Execute(
 			&api.WebhookSuite{},
 		); err != nil {
 			return err
diff --git a/pkg/plugins/golang/v3/webhook.go b/pkg/plugins/golang/v3/webhook.go
index 7c81b017fae..5151b11d65b 100644
--- a/pkg/plugins/golang/v3/webhook.go
+++ b/pkg/plugins/golang/v3/webhook.go
@@ -18,22 +18,23 @@ package v3
 
 import (
 	"fmt"
-	"io/ioutil"
-	"path/filepath"
 
 	"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"
 	"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
+	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,39 +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.
-  %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.
-  %s create webhook --group ship --version v1beta1 --kind Frigate --conversion
-`,
-		ctx.CommandName, ctx.CommandName)
-
-	p.commandName = ctx.CommandName
+	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 Group: ship, Version: v1beta1
+  # and Kind: Frigate
+  %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion
+`, cliMeta.CommandName)
 }
 
 func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
 	p.options = &goPlugin.Options{}
-	fs.StringVar(&p.options.Group, "group", "", "resource Group")
-	p.options.Domain = p.config.GetDomain()
-	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,
@@ -89,21 +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
-}
 
-func (p *createWebhookSubcommand) Run() error {
-	// Create the resource from the options
-	p.resource = p.options.NewResource(p.config)
-
-	return cmdutil.Run(p)
+	return nil
 }
 
-func (p *createWebhookSubcommand) Validate() error {
-	if err := p.options.Validate(); err != nil {
-		return err
-	}
+func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
+	p.resource = res
+
+	p.options.UpdateResource(p.resource, p.config)
 
 	if err := p.resource.Validate(); err != nil {
 		return err
@@ -121,7 +109,7 @@ func (p *createWebhookSubcommand) Validate() error {
 		return fmt.Errorf("webhook resource already exists")
 	}
 
-	if !p.config.IsWebhookVersionCompatible(p.resource.Webhooks.WebhookVersion) {
+	if pluginutil.HasDifferentWebhookVersion(p.config, p.resource.Webhooks.WebhookVersion) {
 		return fmt.Errorf("only one webhook version can be used for all resources, cannot add %q",
 			p.resource.Webhooks.WebhookVersion)
 	}
@@ -129,16 +117,8 @@ func (p *createWebhookSubcommand) Validate() error {
 	return nil
 }
 
-func (p *createWebhookSubcommand) GetScaffolder() (cmdutil.Scaffolder, error) {
-	// Load the boilerplate
-	bp, err := ioutil.ReadFile(filepath.Join("hack", "boilerplate.go.txt")) // nolint:gosec
-	if err != nil {
-		return nil, fmt.Errorf("unable to load boilerplate: %v", err)
-	}
-
-	return scaffolds.NewWebhookScaffolder(p.config, string(bp), 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 aa3e7f4a9e9..00000000000
--- a/pkg/plugins/internal/cmdutil/cmdutil.go
+++ /dev/null
@@ -1,60 +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
-
-// Scaffolder interface creates files to set up a controller manager
-type Scaffolder interface {
-	// 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: call the Scaffold method of the Scaffolder instance. Doesn't need any method
-	// - Step 4: finish the command execution
-	PostScaffold() error
-}
-
-// Run executes a command
-func Run(options RunOptions) 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: scaffold
-	if scaffolder != nil {
-		if err := scaffolder.Scaffold(); err != nil {
-			return err
-		}
-	}
-	// Step 4: finish
-	if err := options.PostScaffold(); err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/pkg/plugins/internal/filesystem/errors.go b/pkg/plugins/internal/filesystem/errors.go
deleted file mode 100644
index 7f605d3241a..00000000000
--- a/pkg/plugins/internal/filesystem/errors.go
+++ /dev/null
@@ -1,173 +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 filesystem
-
-import (
-	"errors"
-	"fmt"
-)
-
-// This file contains the errors returned by the file system wrapper
-// They are not exported as they should not be created outside of this package
-// Exported functions are provided to check which kind of error was returned
-
-// fileExistsError is returned if it could not be checked if the file exists
-type fileExistsError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e fileExistsError) Error() string {
-	return fmt.Sprintf("failed to check if %s exists: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e fileExistsError) Unwrap() error {
-	return e.err
-}
-
-// IsFileExistsError checks if the returned error is because the file could not be checked for existence
-func IsFileExistsError(err error) bool {
-	return errors.As(err, &fileExistsError{})
-}
-
-// openFileError is returned if the file could not be opened
-type openFileError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e openFileError) Error() string {
-	return fmt.Sprintf("failed to open %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e openFileError) Unwrap() error {
-	return e.err
-}
-
-// IsOpenFileError checks if the returned error is because the file could not be opened
-func IsOpenFileError(err error) bool {
-	return errors.As(err, &openFileError{})
-}
-
-// createDirectoryError is returned if the directory could not be created
-type createDirectoryError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e createDirectoryError) Error() string {
-	return fmt.Sprintf("failed to create directory for %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e createDirectoryError) Unwrap() error {
-	return e.err
-}
-
-// IsCreateDirectoryError checks if the returned error is because the directory could not be created
-func IsCreateDirectoryError(err error) bool {
-	return errors.As(err, &createDirectoryError{})
-}
-
-// createFileError is returned if the file could not be created
-type createFileError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e createFileError) Error() string {
-	return fmt.Sprintf("failed to create %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e createFileError) Unwrap() error {
-	return e.err
-}
-
-// IsCreateFileError checks if the returned error is because the file could not be created
-func IsCreateFileError(err error) bool {
-	return errors.As(err, &createFileError{})
-}
-
-// readFileError is returned if the file could not be read
-type readFileError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e readFileError) Error() string {
-	return fmt.Sprintf("failed to read from %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e readFileError) Unwrap() error {
-	return e.err
-}
-
-// IsReadFileError checks if the returned error is because the file could not be read
-func IsReadFileError(err error) bool {
-	return errors.As(err, &readFileError{})
-}
-
-// writeFileError is returned if the file could not be written
-type writeFileError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e writeFileError) Error() string {
-	return fmt.Sprintf("failed to write to %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e writeFileError) Unwrap() error {
-	return e.err
-}
-
-// IsWriteFileError checks if the returned error is because the file could not be written to
-func IsWriteFileError(err error) bool {
-	return errors.As(err, &writeFileError{})
-}
-
-// closeFileError is returned if the file could not be created
-type closeFileError struct {
-	path string
-	err  error
-}
-
-// Error implements error interface
-func (e closeFileError) Error() string {
-	return fmt.Sprintf("failed to close %s: %v", e.path, e.err)
-}
-
-// Unwrap implements Wrapper interface
-func (e closeFileError) Unwrap() error {
-	return e.err
-}
-
-// IsCloseFileError checks if the returned error is because the file could not be closed
-func IsCloseFileError(err error) bool {
-	return errors.As(err, &closeFileError{})
-}
diff --git a/pkg/plugins/internal/filesystem/errors_test.go b/pkg/plugins/internal/filesystem/errors_test.go
deleted file mode 100644
index cc3651a1017..00000000000
--- a/pkg/plugins/internal/filesystem/errors_test.go
+++ /dev/null
@@ -1,83 +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 filesystem
-
-import (
-	"errors"
-	"path/filepath"
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-)
-
-func TestErrors(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Error suite")
-}
-
-var _ = Describe("Errors", func() {
-	var (
-		path               = filepath.Join("path", "to", "file")
-		err                = errors.New("test error")
-		fileExistsErr      = fileExistsError{path, err}
-		openFileErr        = openFileError{path, err}
-		createDirectoryErr = createDirectoryError{path, err}
-		createFileErr      = createFileError{path, err}
-		readFileErr        = readFileError{path, err}
-		writeFileErr       = writeFileError{path, err}
-		closeFileErr       = closeFileError{path, err}
-	)
-
-	DescribeTable("IsXxxxError should return true for themselves and false for the rest",
-		func(f func(error) bool, itself error, rest ...error) {
-			Expect(f(itself)).To(BeTrue())
-			for _, err := range rest {
-				Expect(f(err)).To(BeFalse())
-			}
-		},
-		Entry("file exists", IsFileExistsError, fileExistsErr,
-			openFileErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr, closeFileErr),
-		Entry("open file", IsOpenFileError, openFileErr,
-			fileExistsErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr, closeFileErr),
-		Entry("create directory", IsCreateDirectoryError, createDirectoryErr,
-			fileExistsErr, openFileErr, createFileErr, readFileErr, writeFileErr, closeFileErr),
-		Entry("create file", IsCreateFileError, createFileErr,
-			fileExistsErr, openFileErr, createDirectoryErr, readFileErr, writeFileErr, closeFileErr),
-		Entry("read file", IsReadFileError, readFileErr,
-			fileExistsErr, openFileErr, createDirectoryErr, createFileErr, writeFileErr, closeFileErr),
-		Entry("write file", IsWriteFileError, writeFileErr,
-			fileExistsErr, openFileErr, createDirectoryErr, createFileErr, readFileErr, closeFileErr),
-		Entry("close file", IsCloseFileError, closeFileErr,
-			fileExistsErr, openFileErr, createDirectoryErr, createFileErr, readFileErr, writeFileErr),
-	)
-
-	DescribeTable("should contain the wrapped error and error message",
-		func(err error) {
-			Expect(err).To(MatchError(err))
-			Expect(err.Error()).To(ContainSubstring(err.Error()))
-		},
-		Entry("file exists", fileExistsErr),
-		Entry("open file", openFileErr),
-		Entry("create directory", createDirectoryErr),
-		Entry("create file", createFileErr),
-		Entry("read file", readFileErr),
-		Entry("write file", writeFileErr),
-		Entry("close file", closeFileErr),
-	)
-})
diff --git a/pkg/plugins/internal/filesystem/filesystem.go b/pkg/plugins/internal/filesystem/filesystem.go
deleted file mode 100644
index e7e362c5c44..00000000000
--- a/pkg/plugins/internal/filesystem/filesystem.go
+++ /dev/null
@@ -1,181 +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 filesystem
-
-import (
-	"io"
-	"os"
-	"path/filepath"
-
-	"github.com/spf13/afero"
-)
-
-const (
-	createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
-
-	defaultDirectoryPermission os.FileMode = 0700
-	defaultFilePermission      os.FileMode = 0600
-)
-
-// FileSystem is an IO wrapper to create files
-type FileSystem interface {
-	// Exists checks if the file exists
-	Exists(path string) (bool, error)
-
-	// Open opens the file and returns a self-closing io.Reader.
-	Open(path string) (io.ReadCloser, error)
-
-	// Create creates the directory and file and returns a self-closing
-	// io.Writer pointing to that file. If the file exists, it truncates it.
-	Create(path string) (io.Writer, error)
-}
-
-// fileSystem implements FileSystem
-type fileSystem struct {
-	fs       afero.Fs
-	dirPerm  os.FileMode
-	filePerm os.FileMode
-	fileMode int
-}
-
-// New returns a new FileSystem
-func New(options ...Options) FileSystem {
-	// Default values
-	fs := fileSystem{
-		fs:       afero.NewOsFs(),
-		dirPerm:  defaultDirectoryPermission,
-		filePerm: defaultFilePermission,
-		fileMode: createOrUpdate,
-	}
-
-	// Apply options
-	for _, option := range options {
-		option(&fs)
-	}
-
-	return fs
-}
-
-// Options configure FileSystem
-type Options func(system *fileSystem)
-
-// DirectoryPermissions makes FileSystem.Create use the provided directory
-// permissions
-func DirectoryPermissions(dirPerm os.FileMode) Options {
-	return func(fs *fileSystem) {
-		fs.dirPerm = dirPerm
-	}
-}
-
-// FilePermissions makes FileSystem.Create use the provided file permissions
-func FilePermissions(filePerm os.FileMode) Options {
-	return func(fs *fileSystem) {
-		fs.filePerm = filePerm
-	}
-}
-
-// Exists implements FileSystem.Exists
-func (fs fileSystem) Exists(path string) (bool, error) {
-	exists, err := afero.Exists(fs.fs, path)
-	if err != nil {
-		return exists, fileExistsError{path, err}
-	}
-
-	return exists, nil
-}
-
-// Open implements FileSystem.Open
-func (fs fileSystem) Open(path string) (io.ReadCloser, error) {
-	rc, err := fs.fs.Open(path)
-	if err != nil {
-		return nil, openFileError{path, err}
-	}
-
-	return &readFile{path, rc}, nil
-}
-
-// Create implements FileSystem.Create
-func (fs fileSystem) Create(path string) (io.Writer, error) {
-	// Create the directory if needed
-	if err := fs.fs.MkdirAll(filepath.Dir(path), fs.dirPerm); err != nil {
-		return nil, createDirectoryError{path, err}
-	}
-
-	// Create or truncate the file
-	wc, err := fs.fs.OpenFile(path, fs.fileMode, fs.filePerm)
-	if err != nil {
-		return nil, createFileError{path, err}
-	}
-
-	return &writeFile{path, wc}, nil
-}
-
-var _ io.ReadCloser = &readFile{}
-
-// readFile implements io.Reader
-type readFile struct {
-	path string
-	io.ReadCloser
-}
-
-// Read implements io.Reader.ReadCloser
-func (f *readFile) Read(content []byte) (n int, err error) {
-	// Read the content
-	n, err = f.ReadCloser.Read(content)
-	// EOF is a special case error that we can't wrap
-	if err == io.EOF {
-		return
-	}
-	if err != nil {
-		return n, readFileError{f.path, err}
-	}
-
-	return n, nil
-}
-
-// Close implements io.Reader.ReadCloser
-func (f *readFile) Close() error {
-	if err := f.ReadCloser.Close(); err != nil {
-		return closeFileError{f.path, err}
-	}
-
-	return nil
-}
-
-// writeFile implements io.Writer
-type writeFile struct {
-	path string
-	io.WriteCloser
-}
-
-// Write implements io.Writer.Write
-func (f *writeFile) Write(content []byte) (n int, err error) {
-	// Close the file when we end writing
-	defer func() {
-		if closeErr := f.Close(); err == nil && closeErr != nil {
-			err = closeFileError{f.path, err}
-		}
-	}()
-
-	// Write the content
-	n, err = f.WriteCloser.Write(content)
-	if err != nil {
-		return n, writeFileError{f.path, err}
-	}
-
-	return n, nil
-}
diff --git a/pkg/plugins/internal/filesystem/filesystem_test.go b/pkg/plugins/internal/filesystem/filesystem_test.go
deleted file mode 100644
index 9f98ce400ac..00000000000
--- a/pkg/plugins/internal/filesystem/filesystem_test.go
+++ /dev/null
@@ -1,156 +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 filesystem
-
-import (
-	"os"
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestFileSystem(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "FileSystem suite")
-}
-
-var _ = Describe("FileSystem", func() {
-	Describe("New", func() {
-		const (
-			dirPerm  os.FileMode = 0777
-			filePerm os.FileMode = 0666
-		)
-
-		var (
-			fsi FileSystem
-			fs  fileSystem
-			ok  bool
-		)
-
-		Context("when using no options", func() {
-			BeforeEach(func() {
-				fsi = New()
-				fs, ok = fsi.(fileSystem)
-			})
-
-			It("should be a fileSystem instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(fs.fs).NotTo(BeNil())
-			})
-
-			It("should use default directory permission", func() {
-				Expect(fs.dirPerm).To(Equal(defaultDirectoryPermission))
-			})
-
-			It("should use default file permission", func() {
-				Expect(fs.filePerm).To(Equal(defaultFilePermission))
-			})
-
-			It("should use default file mode", func() {
-				Expect(fs.fileMode).To(Equal(createOrUpdate))
-			})
-		})
-
-		Context("when using directory permission option", func() {
-			BeforeEach(func() {
-				fsi = New(DirectoryPermissions(dirPerm))
-				fs, ok = fsi.(fileSystem)
-			})
-
-			It("should be a fileSystem instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(fs.fs).NotTo(BeNil())
-			})
-
-			It("should use provided directory permission", func() {
-				Expect(fs.dirPerm).To(Equal(dirPerm))
-			})
-
-			It("should use default file permission", func() {
-				Expect(fs.filePerm).To(Equal(defaultFilePermission))
-			})
-
-			It("should use default file mode", func() {
-				Expect(fs.fileMode).To(Equal(createOrUpdate))
-			})
-		})
-
-		Context("when using file permission option", func() {
-			BeforeEach(func() {
-				fsi = New(FilePermissions(filePerm))
-				fs, ok = fsi.(fileSystem)
-			})
-
-			It("should be a fileSystem instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(fs.fs).NotTo(BeNil())
-			})
-
-			It("should use default directory permission", func() {
-				Expect(fs.dirPerm).To(Equal(defaultDirectoryPermission))
-			})
-
-			It("should use provided file permission", func() {
-				Expect(fs.filePerm).To(Equal(filePerm))
-			})
-
-			It("should use default file mode", func() {
-				Expect(fs.fileMode).To(Equal(createOrUpdate))
-			})
-		})
-
-		Context("when using both directory and file permission options", func() {
-			BeforeEach(func() {
-				fsi = New(DirectoryPermissions(dirPerm), FilePermissions(filePerm))
-				fs, ok = fsi.(fileSystem)
-			})
-
-			It("should be a fileSystem instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(fs.fs).NotTo(BeNil())
-			})
-
-			It("should use provided directory permission", func() {
-				Expect(fs.dirPerm).To(Equal(dirPerm))
-			})
-
-			It("should use provided file permission", func() {
-				Expect(fs.filePerm).To(Equal(filePerm))
-			})
-
-			It("should use default file mode", func() {
-				Expect(fs.fileMode).To(Equal(createOrUpdate))
-			})
-		})
-	})
-
-	// NOTE: FileSystem.Exists, FileSystem.Open, FileSystem.Open().Read, FileSystem.Create and FileSystem.Create().Write
-	// are hard to test in unitary tests as they deal with actual files
-})
diff --git a/pkg/plugins/internal/filesystem/mock.go b/pkg/plugins/internal/filesystem/mock.go
deleted file mode 100644
index b7d213c1fc3..00000000000
--- a/pkg/plugins/internal/filesystem/mock.go
+++ /dev/null
@@ -1,217 +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 filesystem
-
-import (
-	"bytes"
-	"io"
-)
-
-// mockFileSystem implements FileSystem
-type mockFileSystem struct {
-	path            string
-	exists          func(path string) bool
-	existsError     error
-	openFileError   error
-	createDirError  error
-	createFileError error
-	input           *bytes.Buffer
-	readFileError   error
-	output          *bytes.Buffer
-	writeFileError  error
-	closeFileError  error
-}
-
-// NewMock returns a new FileSystem
-func NewMock(options ...MockOptions) FileSystem {
-	// Default values
-	fs := mockFileSystem{
-		exists: func(_ string) bool { return false },
-		output: new(bytes.Buffer),
-	}
-
-	// Apply options
-	for _, option := range options {
-		option(&fs)
-	}
-
-	return fs
-}
-
-// MockOptions configure FileSystem
-type MockOptions func(system *mockFileSystem)
-
-// MockPath ensures that the file created with this scaffold is at path
-func MockPath(path string) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.path = path
-	}
-}
-
-// MockExists makes FileSystem.Exists use the provided function to check if the file exists
-func MockExists(exists func(path string) bool) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.exists = exists
-	}
-}
-
-// MockExistsError makes FileSystem.Exists return err
-func MockExistsError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.existsError = err
-	}
-}
-
-// MockOpenFileError makes FileSystem.Open return err
-func MockOpenFileError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.openFileError = err
-	}
-}
-
-// MockCreateDirError makes FileSystem.Create return err
-func MockCreateDirError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.createDirError = err
-	}
-}
-
-// MockCreateFileError makes FileSystem.Create return err
-func MockCreateFileError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.createFileError = err
-	}
-}
-
-// MockInput provides a buffer where the content will be read from
-func MockInput(input *bytes.Buffer) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.input = input
-	}
-}
-
-// MockReadFileError  makes the Read method (of the io.Reader returned by FileSystem.Open) return err
-func MockReadFileError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.readFileError = err
-	}
-}
-
-// MockOutput provides a buffer where the content will be written
-func MockOutput(output *bytes.Buffer) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.output = output
-	}
-}
-
-// MockWriteFileError makes the Write method (of the io.Writer returned by FileSystem.Create) return err
-func MockWriteFileError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.writeFileError = err
-	}
-}
-
-// MockCloseFileError makes the Write method (of the io.Writer returned by FileSystem.Create) return err
-func MockCloseFileError(err error) MockOptions {
-	return func(fs *mockFileSystem) {
-		fs.closeFileError = err
-	}
-}
-
-// Exists implements FileSystem.Exists
-func (fs mockFileSystem) Exists(path string) (bool, error) {
-	if fs.existsError != nil {
-		return false, fileExistsError{path, fs.existsError}
-	}
-
-	return fs.exists(path), nil
-}
-
-// Open implements FileSystem.Open
-func (fs mockFileSystem) Open(path string) (io.ReadCloser, error) {
-	if fs.openFileError != nil {
-		return nil, openFileError{path, fs.openFileError}
-	}
-
-	if fs.input == nil {
-		fs.input = bytes.NewBufferString("Hello world!")
-	}
-
-	return &mockReadFile{path, fs.input, fs.readFileError, fs.closeFileError}, nil
-}
-
-// Create implements FileSystem.Create
-func (fs mockFileSystem) Create(path string) (io.Writer, error) {
-	if fs.createDirError != nil {
-		return nil, createDirectoryError{path, fs.createDirError}
-	}
-
-	if fs.createFileError != nil {
-		return nil, createFileError{path, fs.createFileError}
-	}
-
-	return &mockWriteFile{path, fs.output, fs.writeFileError, fs.closeFileError}, nil
-}
-
-// mockReadFile implements io.Reader mocking a readFile for tests
-type mockReadFile struct {
-	path           string
-	input          *bytes.Buffer
-	readFileError  error
-	closeFileError error
-}
-
-// Read implements io.Reader.ReadCloser
-func (f *mockReadFile) Read(content []byte) (n int, err error) {
-	if f.readFileError != nil {
-		return 0, readFileError{path: f.path, err: f.readFileError}
-	}
-
-	return f.input.Read(content)
-}
-
-// Read implements io.Reader.ReadCloser
-func (f *mockReadFile) Close() error {
-	if f.closeFileError != nil {
-		return closeFileError{path: f.path, err: f.closeFileError}
-	}
-
-	return nil
-}
-
-// mockWriteFile implements io.Writer mocking a writeFile for tests
-type mockWriteFile struct {
-	path           string
-	content        *bytes.Buffer
-	writeFileError error
-	closeFileError error
-}
-
-// Write implements io.Writer.Write
-func (f *mockWriteFile) Write(content []byte) (n int, err error) {
-	defer func() {
-		if err == nil && f.closeFileError != nil {
-			err = closeFileError{f.path, f.closeFileError}
-		}
-	}()
-
-	if f.writeFileError != nil {
-		return 0, writeFileError{f.path, f.writeFileError}
-	}
-
-	return f.content.Write(content)
-}
diff --git a/pkg/plugins/internal/filesystem/mock_test.go b/pkg/plugins/internal/filesystem/mock_test.go
deleted file mode 100644
index 37f9e827cce..00000000000
--- a/pkg/plugins/internal/filesystem/mock_test.go
+++ /dev/null
@@ -1,454 +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 filesystem
-
-import (
-	"bytes"
-	"errors"
-	"path/filepath"
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-)
-
-func TestMockFileSystem(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "MockFileSystem suite")
-}
-
-//nolint:dupl
-var _ = Describe("MockFileSystem", func() {
-	var (
-		fsi     FileSystem
-		fs      mockFileSystem
-		ok      bool
-		options []MockOptions
-		testErr = errors.New("test error")
-	)
-
-	JustBeforeEach(func() {
-		fsi = NewMock(options...)
-		fs, ok = fsi.(mockFileSystem)
-	})
-
-	Context("when using no options", func() {
-		BeforeEach(func() {
-			options = make([]MockOptions, 0)
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockPath", func() {
-		var filePath = filepath.Join("path", "to", "file")
-
-		BeforeEach(func() {
-			options = []MockOptions{MockPath(filePath)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should save the provided path", func() {
-			Expect(fs.path).To(Equal(filePath))
-		})
-	})
-
-	Context("when using MockExists", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockExists(func(_ string) bool { return true })}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeTrue())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockExistsError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockExistsError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should error when calling Exists", func() {
-			_, err := fsi.Exists("")
-			Expect(err).To(MatchError(testErr))
-			Expect(IsFileExistsError(err)).To(BeTrue())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockOpenFileError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockOpenFileError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should error when calling Open", func() {
-			_, err := fsi.Open("")
-			Expect(err).To(MatchError(testErr))
-			Expect(IsOpenFileError(err)).To(BeTrue())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockCreateDirError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockCreateDirError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should error when calling Create", func() {
-			_, err := fsi.Create("")
-			Expect(err).To(MatchError(testErr))
-			Expect(IsCreateDirectoryError(err)).To(BeTrue())
-		})
-	})
-
-	Context("when using MockCreateFileError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockCreateFileError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should error when calling Create", func() {
-			_, err := fsi.Create("")
-			Expect(err).To(MatchError(testErr))
-			Expect(IsCreateFileError(err)).To(BeTrue())
-		})
-	})
-
-	Context("when using MockInput", func() {
-		var (
-			input       *bytes.Buffer
-			fileContent = []byte("Hello world!")
-		)
-
-		BeforeEach(func() {
-			input = bytes.NewBufferString("Hello world!")
-			options = []MockOptions{MockInput(input)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files and the content to be accessible", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			output := make([]byte, len(fileContent))
-			n, err := f.Read(output)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(n).To(Equal(len(fileContent)))
-			Expect(output).To(Equal(fileContent))
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockReadFileError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockReadFileError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should error when calling Open().Read", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			output := make([]byte, 0)
-			_, err = f.Read(output)
-			Expect(err).To(MatchError(testErr))
-			Expect(IsReadFileError(err)).To(BeTrue())
-		})
-
-		It("should create writable files", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-	})
-
-	Context("when using MockOutput", func() {
-		var (
-			output      bytes.Buffer
-			fileContent = []byte("Hello world!")
-		)
-
-		BeforeEach(func() {
-			options = []MockOptions{MockOutput(&output)}
-			output.Reset()
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should create writable files and the content should be accesible", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			n, err := f.Write(fileContent)
-			Expect(err).NotTo(HaveOccurred())
-			Expect(n).To(Equal(len(fileContent)))
-			Expect(output.Bytes()).To(Equal(fileContent))
-		})
-	})
-
-	Context("when using MockWriteFileError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockWriteFileError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should open readable files", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Read([]byte(""))
-			Expect(err).NotTo(HaveOccurred())
-		})
-
-		It("should error when calling Create().Write", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).To(MatchError(testErr))
-			Expect(IsWriteFileError(err)).To(BeTrue())
-		})
-	})
-
-	Context("when using MockCloseFileError", func() {
-		BeforeEach(func() {
-			options = []MockOptions{MockCloseFileError(testErr)}
-		})
-
-		It("should be a mockFileSystem instance", func() {
-			Expect(ok).To(BeTrue())
-		})
-
-		It("should claim that files don't exist", func() {
-			exists, err := fsi.Exists("")
-			Expect(err).NotTo(HaveOccurred())
-			Expect(exists).To(BeFalse())
-		})
-
-		It("should error when calling Open().Close", func() {
-			f, err := fsi.Open("")
-			Expect(err).NotTo(HaveOccurred())
-
-			err = f.Close()
-			Expect(err).To(MatchError(testErr))
-			Expect(IsCloseFileError(err)).To(BeTrue())
-		})
-
-		It("should error when calling Create().Write", func() {
-			f, err := fsi.Create("")
-			Expect(err).NotTo(HaveOccurred())
-
-			_, err = f.Write([]byte(""))
-			Expect(err).To(MatchError(testErr))
-			Expect(IsCloseFileError(err)).To(BeTrue())
-		})
-	})
-})
diff --git a/pkg/plugins/internal/machinery/errors.go b/pkg/plugins/internal/machinery/errors.go
deleted file mode 100644
index faba57a1d05..00000000000
--- a/pkg/plugins/internal/machinery/errors.go
+++ /dev/null
@@ -1,74 +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 machinery
-
-import (
-	"errors"
-	"fmt"
-
-	"sigs.k8s.io/kubebuilder/v3/pkg/model/file"
-)
-
-// This file contains the errors returned by the scaffolding machinery
-// They are not exported as they should not be created outside of this package
-// Exported functions are provided to check which kind of error was returned
-
-// fileAlreadyExistsError is returned if the file is expected not to exist but it does
-type fileAlreadyExistsError struct {
-	path string
-}
-
-// Error implements error interface
-func (e fileAlreadyExistsError) Error() string {
-	return fmt.Sprintf("failed to create %s: file already exists", e.path)
-}
-
-// IsFileAlreadyExistsError checks if the returned error is because the file already existed when expected not to
-func IsFileAlreadyExistsError(err error) bool {
-	return errors.As(err, &fileAlreadyExistsError{})
-}
-
-// modelAlreadyExistsError is returned if the file is expected not to exist but a previous model does
-type modelAlreadyExistsError struct {
-	path string
-}
-
-// Error implements error interface
-func (e modelAlreadyExistsError) Error() string {
-	return fmt.Sprintf("failed to create %s: model already exists", e.path)
-}
-
-// IsModelAlreadyExistsError checks if the returned error is because the model already existed when expected not to
-func IsModelAlreadyExistsError(err error) bool {
-	return errors.As(err, &modelAlreadyExistsError{})
-}
-
-// unknownIfExistsActionError is returned if the if-exists-action is unknown
-type unknownIfExistsActionError struct {
-	path           string
-	ifExistsAction file.IfExistsAction
-}
-
-// Error implements error interface
-func (e unknownIfExistsActionError) Error() string {
-	return fmt.Sprintf("unknown behavior if file exists (%d) for %s", e.ifExistsAction, e.path)
-}
-
-// IsUnknownIfExistsActionError checks if the returned error is because the if-exists-action is unknown
-func IsUnknownIfExistsActionError(err error) bool {
-	return errors.As(err, &unknownIfExistsActionError{})
-}
diff --git a/pkg/plugins/internal/machinery/errors_test.go b/pkg/plugins/internal/machinery/errors_test.go
deleted file mode 100644
index adcf32b4f70..00000000000
--- a/pkg/plugins/internal/machinery/errors_test.go
+++ /dev/null
@@ -1,71 +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 machinery
-
-import (
-	"errors"
-	"path/filepath"
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-)
-
-func TestErrors(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Error suite")
-}
-
-var _ = Describe("Errors", func() {
-	var (
-		path                     = filepath.Join("path", "to", "file")
-		err                      = errors.New("test error")
-		fileAlreadyExistsErr     = fileAlreadyExistsError{path}
-		modelAlreadyExistsErr    = modelAlreadyExistsError{path}
-		unknownIfExistsActionErr = unknownIfExistsActionError{path, -1}
-	)
-
-	DescribeTable("IsXxxxError should return true for themselves and false for the rest",
-		func(f func(error) bool, itself error, rest ...error) {
-			Expect(f(itself)).To(BeTrue())
-			for _, err := range rest {
-				Expect(f(err)).To(BeFalse())
-			}
-		},
-		Entry("file exists", IsFileAlreadyExistsError, fileAlreadyExistsErr,
-			err, modelAlreadyExistsErr, unknownIfExistsActionErr),
-		Entry("model exists", IsModelAlreadyExistsError, modelAlreadyExistsErr,
-			err, fileAlreadyExistsErr, unknownIfExistsActionErr),
-		Entry("unknown if exists action", IsUnknownIfExistsActionError, unknownIfExistsActionErr,
-			err, fileAlreadyExistsErr, modelAlreadyExistsErr),
-	)
-
-	DescribeTable("should contain the wrapped error and error message",
-		func(err error) {
-			Expect(err).To(MatchError(err))
-			Expect(err.Error()).To(ContainSubstring(err.Error()))
-		},
-	)
-
-	// NOTE: the following test increases coverage
-	It("should print a descriptive error message", func() {
-		Expect(fileAlreadyExistsErr.Error()).To(ContainSubstring("file already exists"))
-		Expect(modelAlreadyExistsErr.Error()).To(ContainSubstring("model already exists"))
-		Expect(unknownIfExistsActionErr.Error()).To(ContainSubstring("unknown behavior if file exists"))
-	})
-})
diff --git a/pkg/plugins/internal/machinery/scaffold_test.go b/pkg/plugins/internal/machinery/scaffold_test.go
deleted file mode 100644
index d7e2a6c35e8..00000000000
--- a/pkg/plugins/internal/machinery/scaffold_test.go
+++ /dev/null
@@ -1,560 +0,0 @@
-/*
-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 (
-	"bytes"
-	"errors"
-	"testing"
-
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/ginkgo/extensions/table"
-	. "github.com/onsi/gomega"
-
-	"sigs.k8s.io/kubebuilder/v3/pkg/model"
-	"sigs.k8s.io/kubebuilder/v3/pkg/model/file"
-	"sigs.k8s.io/kubebuilder/v3/pkg/plugins/internal/filesystem"
-)
-
-func TestScaffold(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Scaffold suite")
-}
-
-var _ = Describe("Scaffold", func() {
-	Describe("NewScaffold", func() {
-		var (
-			si Scaffold
-			s  *scaffold
-			ok bool
-		)
-
-		Context("when using no plugins", func() {
-			BeforeEach(func() {
-				si = NewScaffold()
-				s, ok = si.(*scaffold)
-			})
-
-			It("should be a scaffold instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(s.fs).NotTo(BeNil())
-			})
-
-			It("should not have any plugin", func() {
-				Expect(len(s.plugins)).To(Equal(0))
-			})
-		})
-
-		Context("when using one plugin", func() {
-			BeforeEach(func() {
-				si = NewScaffold(fakePlugin{})
-				s, ok = si.(*scaffold)
-			})
-
-			It("should be a scaffold instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(s.fs).NotTo(BeNil())
-			})
-
-			It("should have one plugin", func() {
-				Expect(len(s.plugins)).To(Equal(1))
-			})
-		})
-
-		Context("when using several plugins", func() {
-			BeforeEach(func() {
-				si = NewScaffold(fakePlugin{}, fakePlugin{}, fakePlugin{})
-				s, ok = si.(*scaffold)
-			})
-
-			It("should be a scaffold instance", func() {
-				Expect(ok).To(BeTrue())
-			})
-
-			It("should not have a nil fs", func() {
-				Expect(s.fs).NotTo(BeNil())
-			})
-
-			It("should have several plugins", func() {
-				Expect(len(s.plugins)).To(Equal(3))
-			})
-		})
-	})
-
-	Describe("Scaffold.Execute", func() {
-		const fileContent = "Hello world!"
-
-		var (
-			output  bytes.Buffer
-			testErr = errors.New("error text")
-		)
-
-		BeforeEach(func() {
-			output.Reset()
-		})
-
-		DescribeTable("successes",
-			func(expected string, files ...file.Builder) {
-				s := &scaffold{
-					fs: filesystem.NewMock(
-						filesystem.MockOutput(&output),
-					),
-				}
-
-				Expect(s.Execute(model.NewUniverse(), files...)).To(Succeed())
-				Expect(output.String()).To(Equal(expected))
-			},
-			Entry("should write the file",
-				fileContent,
-				fakeTemplate{body: fileContent},
-			),
-			Entry("should skip optional models if already have one",
-				fileContent,
-				fakeTemplate{body: fileContent},
-				fakeTemplate{},
-			),
-			Entry("should overwrite required models if already have one",
-				fileContent,
-				fakeTemplate{},
-				fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: fileContent},
-			),
-			Entry("should format a go file",
-				"package file\n",
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "file.go"}, body: "package    file"},
-			),
-		)
-
-		DescribeTable("file builders related errors",
-			func(f func(error) bool, files ...file.Builder) {
-				s := &scaffold{fs: filesystem.NewMock()}
-
-				Expect(f(s.Execute(model.NewUniverse(), files...))).To(BeTrue())
-			},
-			Entry("should fail if unable to validate a file builder",
-				file.IsValidateError,
-				fakeRequiresValidation{validateErr: testErr},
-			),
-			Entry("should fail if unable to set default values for a template",
-				file.IsSetTemplateDefaultsError,
-				fakeTemplate{err: testErr},
-			),
-			Entry("should fail if an unexpected previous model is found",
-				IsModelAlreadyExistsError,
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename"}},
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}},
-			),
-			Entry("should fail if behavior if file exists is not defined",
-				IsUnknownIfExistsActionError,
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename"}},
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}},
-			),
-		)
-
-		// Following errors are unwrapped, so we need to check for substrings
-		DescribeTable("template related errors",
-			func(errMsg string, files ...file.Builder) {
-				s := &scaffold{fs: filesystem.NewMock()}
-
-				err := s.Execute(model.NewUniverse(), files...)
-				Expect(err).To(HaveOccurred())
-				Expect(err.Error()).To(ContainSubstring(errMsg))
-			},
-			Entry("should fail if a template is broken",
-				"template: ",
-				fakeTemplate{body: "{{ .Field }"},
-			),
-			Entry("should fail if a template params aren't provided",
-				"template: ",
-				fakeTemplate{body: "{{ .Field }}"},
-			),
-			Entry("should fail if unable to format a go file",
-				"expected 'package', found ",
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "file.go"}, body: fileContent},
-			),
-		)
-
-		DescribeTable("insert strings",
-			func(input, expected string, files ...file.Builder) {
-				s := &scaffold{
-					fs: filesystem.NewMock(
-						filesystem.MockInput(bytes.NewBufferString(input)),
-						filesystem.MockOutput(&output),
-						filesystem.MockExists(func(_ string) bool { return len(input) != 0 }),
-					),
-				}
-
-				Expect(s.Execute(model.NewUniverse(), files...)).To(Succeed())
-				Expect(output.String()).To(Equal(expected))
-			},
-			Entry("should insert lines for go files",
-				`
-//+kubebuilder:scaffold:-
-`,
-				`
-1
-2
-//+kubebuilder:scaffold:-
-`,
-				fakeInserter{codeFragments: file.CodeFragmentsMap{
-					file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}},
-				},
-			),
-			Entry("should insert lines for yaml files",
-				`
-#+kubebuilder:scaffold:-
-`,
-				`
-1
-2
-#+kubebuilder:scaffold:-
-`,
-				fakeInserter{codeFragments: file.CodeFragmentsMap{
-					file.NewMarkerFor("file.yaml", "-"): {"1\n", "2\n"}},
-				},
-			),
-			Entry("should use models if there is no file",
-				"",
-				`
-1
-2
-//+kubebuilder:scaffold:-
-`,
-				fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: `
-//+kubebuilder:scaffold:-
-`},
-				fakeInserter{codeFragments: file.CodeFragmentsMap{
-					file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}},
-				},
-			),
-			Entry("should use required models over files",
-				fileContent,
-				`
-1
-2
-//+kubebuilder:scaffold:-
-`,
-				fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: `
-//+kubebuilder:scaffold:-
-`},
-				fakeInserter{codeFragments: file.CodeFragmentsMap{
-					file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"}},
-				},
-			),
-			Entry("should use files over optional models",
-				`
-//+kubebuilder:scaffold:-
-`,
-				`
-1
-2
-//+kubebuilder:scaffold:-
-`,
-				fakeTemplate{body: fileContent},
-				fakeInserter{
-					codeFragments: file.CodeFragmentsMap{
-						file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"},
-					},
-				},
-			),
-			Entry("should filter invalid markers",
-				`
-//+kubebuilder:scaffold:-
-//+kubebuilder:scaffold:*
-`,
-				`
-1
-2
-//+kubebuilder:scaffold:-
-//+kubebuilder:scaffold:*
-`,
-				fakeInserter{
-					markers: []file.Marker{file.NewMarkerFor("file.go", "-")},
-					codeFragments: file.CodeFragmentsMap{
-						file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"},
-						file.NewMarkerFor("file.go", "*"): {"3\n", "4\n"},
-					},
-				},
-			),
-			Entry("should filter already existing one-line code fragments",
-				`
-1
-//+kubebuilder:scaffold:-
-3
-4
-//+kubebuilder:scaffold:*
-`,
-				`
-1
-2
-//+kubebuilder:scaffold:-
-3
-4
-//+kubebuilder:scaffold:*
-`,
-				fakeInserter{
-					codeFragments: file.CodeFragmentsMap{
-						file.NewMarkerFor("file.go", "-"): {"1\n", "2\n"},
-						file.NewMarkerFor("file.go", "*"): {"3\n", "4\n"},
-					},
-				},
-			),
-			Entry("should not insert anything if no code fragment",
-				"", // input is provided through a template as mock fs doesn't copy it to the output buffer if no-op
-				`
-//+kubebuilder:scaffold:-
-`,
-				fakeTemplate{body: `
-//+kubebuilder:scaffold:-
-`},
-				fakeInserter{
-					codeFragments: file.CodeFragmentsMap{
-						file.NewMarkerFor("file.go", "-"): {},
-					},
-				},
-			),
-		)
-
-		DescribeTable("insert strings related errors",
-			func(f func(error) bool, files ...file.Builder) {
-				s := &scaffold{
-					fs: filesystem.NewMock(
-						filesystem.MockExists(func(_ string) bool { return true }),
-					),
-				}
-
-				err := s.Execute(model.NewUniverse(), files...)
-				Expect(err).To(HaveOccurred())
-				Expect(f(err)).To(BeTrue())
-			},
-			Entry("should fail if inserting into a model that fails when a file exists and it does exist",
-				IsFileAlreadyExistsError,
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.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",
-				IsUnknownIfExistsActionError,
-				fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}},
-				fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}},
-			),
-		)
-
-		It("should fail if a plugin fails", func() {
-			s := &scaffold{
-				fs:      filesystem.NewMock(),
-				plugins: []model.Plugin{fakePlugin{err: testErr}},
-			}
-
-			err := s.Execute(
-				model.NewUniverse(),
-				fakeTemplate{},
-			)
-			Expect(err).To(MatchError(testErr))
-			Expect(model.IsPluginError(err)).To(BeTrue())
-		})
-
-		Context("write when the file already exists", func() {
-			var s Scaffold
-
-			BeforeEach(func() {
-				s = &scaffold{
-					fs: filesystem.NewMock(
-						filesystem.MockExists(func(_ string) bool { return true }),
-						filesystem.MockOutput(&output),
-					),
-				}
-			})
-
-			It("should skip the file by default", func() {
-				Expect(s.Execute(
-					model.NewUniverse(),
-					fakeTemplate{body: fileContent},
-				)).To(Succeed())
-				Expect(output.String()).To(BeEmpty())
-			})
-
-			It("should write the file if configured to do so", func() {
-				Expect(s.Execute(
-					model.NewUniverse(),
-					fakeTemplate{fakeBuilder: fakeBuilder{ifExistsAction: file.Overwrite}, body: fileContent},
-				)).To(Succeed())
-				Expect(output.String()).To(Equal(fileContent))
-			})
-
-			It("should error if configured to do so", func() {
-				err := s.Execute(
-					model.NewUniverse(),
-					fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: file.Error}, body: fileContent},
-				)
-				Expect(err).To(HaveOccurred())
-				Expect(IsFileAlreadyExistsError(err)).To(BeTrue())
-				Expect(output.String()).To(BeEmpty())
-			})
-		})
-
-		DescribeTable("filesystem errors",
-			func(
-				mockErrorF func(error) filesystem.MockOptions,
-				checkErrorF func(error) bool,
-				files ...file.Builder,
-			) {
-				s := &scaffold{
-					fs: filesystem.NewMock(
-						mockErrorF(testErr),
-					),
-				}
-
-				err := s.Execute(model.NewUniverse(), files...)
-				Expect(err).To(HaveOccurred())
-				Expect(checkErrorF(err)).To(BeTrue())
-			},
-			Entry("should fail if fs.Exists failed (at file writing)",
-				filesystem.MockExistsError, filesystem.IsFileExistsError,
-				fakeTemplate{},
-			),
-			Entry("should fail if fs.Exists failed (at model updating)",
-				filesystem.MockExistsError, filesystem.IsFileExistsError,
-				fakeTemplate{},
-				fakeInserter{},
-			),
-			Entry("should fail if fs.Open was unable to open the file",
-				filesystem.MockOpenFileError, filesystem.IsOpenFileError,
-				fakeInserter{},
-			),
-			Entry("should fail if fs.Open().Read was unable to read the file",
-				filesystem.MockReadFileError, filesystem.IsReadFileError,
-				fakeInserter{},
-			),
-			Entry("should fail if fs.Open().Close was unable to close the file",
-				filesystem.MockCloseFileError, filesystem.IsCloseFileError,
-				fakeInserter{},
-			),
-			Entry("should fail if fs.Create was unable to create the directory",
-				filesystem.MockCreateDirError, filesystem.IsCreateDirectoryError,
-				fakeTemplate{},
-			),
-			Entry("should fail if fs.Create was unable to create the file",
-				filesystem.MockCreateFileError, filesystem.IsCreateFileError,
-				fakeTemplate{},
-			),
-			Entry("should fail if fs.Create().Write was unable to write the file",
-				filesystem.MockWriteFileError, filesystem.IsWriteFileError,
-				fakeTemplate{},
-			),
-			Entry("should fail if fs.Create().Write was unable to close the file",
-				filesystem.MockCloseFileError, filesystem.IsCloseFileError,
-				fakeTemplate{},
-			),
-		)
-	})
-})
-
-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{}
-
-// fakeBuilder is used to mock a file.Builder
-type fakeBuilder struct {
-	path           string
-	ifExistsAction file.IfExistsAction
-}
-
-// GetPath implements file.Builder
-func (f fakeBuilder) GetPath() string {
-	return f.path
-}
-
-// GetIfExistsAction implements file.Builder
-func (f fakeBuilder) GetIfExistsAction() file.IfExistsAction {
-	return f.ifExistsAction
-}
-
-var _ file.RequiresValidation = fakeRequiresValidation{}
-
-// fakeRequiresValidation is used to mock a file.RequiresValidation in order to test Scaffold
-type fakeRequiresValidation struct {
-	fakeBuilder
-
-	validateErr error
-}
-
-// Validate implements file.RequiresValidation
-func (f fakeRequiresValidation) Validate() error {
-	return f.validateErr
-}
-
-var _ file.Template = fakeTemplate{}
-
-// fakeTemplate is used to mock a file.File in order to test Scaffold
-type fakeTemplate struct {
-	fakeBuilder
-
-	body string
-	err  error
-}
-
-// GetBody implements file.Template
-func (f fakeTemplate) GetBody() string {
-	return f.body
-}
-
-// SetTemplateDefaults implements file.Template
-func (f fakeTemplate) SetTemplateDefaults() error {
-	if f.err != nil {
-		return f.err
-	}
-
-	return nil
-}
-
-type fakeInserter struct {
-	fakeBuilder
-
-	markers       []file.Marker
-	codeFragments file.CodeFragmentsMap
-}
-
-// GetMarkers implements file.UpdatableTemplate
-func (f fakeInserter) GetMarkers() []file.Marker {
-	if f.markers != nil {
-		return f.markers
-	}
-
-	markers := make([]file.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 {
-	return f.codeFragments
-}
diff --git a/pkg/plugins/internal/util/go_version_test.go b/pkg/plugins/internal/util/go_version_test.go
deleted file mode 100644
index 1814dcdd5b4..00000000000
--- a/pkg/plugins/internal/util/go_version_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package util
-
-import (
-	"testing"
-)
-
-func TestCheckGoVersion(t *testing.T) {
-
-	tests := []struct {
-		ver       string
-		isInvalid bool
-	}{
-		{"go1.8", true},
-		{"go1.9", true},
-		{"go1.10", true},
-		{"go1.11", true},
-		{"go1.11rc", true},
-		{"go1.11.1", true},
-		{"go1.12rc2", true},
-		{"go1.13", false},
-	}
-
-	for _, test := range tests {
-		err := checkGoVersion(test.ver)
-		if err != nil {
-			// go error, but the version isn't invalid
-			if !test.isInvalid {
-				t.Errorf("Go version check failed valid version '%s' with error '%s'", test.ver, err)
-			}
-		} else {
-			// got no error, but the version is invalid
-			if test.isInvalid {
-				t.Errorf("version '%s' is invalid, but got no error", test.ver)
-			}
-		}
-	}
-
-}
diff --git a/pkg/plugins/scaffolder.go b/pkg/plugins/scaffolder.go
new file mode 100644
index 00000000000..b738caa999a
--- /dev/null
+++ b/pkg/plugins/scaffolder.go
@@ -0,0 +1,28 @@
+/*
+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 plugins
+
+import (
+	"sigs.k8s.io/kubebuilder/v3/pkg/machinery"
+)
+
+// 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/pkged.go b/pkged.go
new file mode 100644
index 00000000000..70a6fb240ee
--- /dev/null
+++ b/pkged.go
@@ -0,0 +1,12 @@
+// Code generated by pkger; DO NOT EDIT.
+
+// +build !skippkger
+
+package kubebuilder
+
+import (
+	"github.com/markbates/pkger"
+	"github.com/markbates/pkger/pkging/mem"
+)
+
+var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec5d6b73a2ccf2ff2a5bbcce46869b62d579114c445c358f26723bf5d45330104087cb11f0766abffbbf18103151a3bbd9a4eafca9ad8dccd033cc8deee9df7437ff25bce0258c89f67f89d873e2db792bbef5c2c63c356d33f590652f1a4b3abb7bef2d8836d198c6f6226e442b2f4916219c379cb0112f60e34459e28690fc285c247f19894bb4cf3de2861819be4db489c3e2f72124dac45f069c1b8efdad72ef1b0c83c4f082f85b34776cebdb8b87ecf81b0cfdc843b6f5cd0b92f05be2dadf9cf09be905c6c2b3e35be2867836168e9d9ce84934771a96b15879c13f866f71cca96edd1ac40d3109c313f51037c4d048a04bb4ff4ddc127fdf104f89816ca29d2c52bb484c6c230e03a24d0461f2cd0be2c44059abcd34f9662c0d0f1926b2bf79c137fcc06fd080ae4ddc1062d8cd7a99d56b85f0163f09f73ebbfc7b37d6f8bee3256e6adec2d06ff8c6626e1a891d67fdc3837afa66f6d70b9c866ffb5985f776846b33d3172f7b98b949ec98b821b2615ed871dc7841466257339cad17e1349e1c7bd1405e9c1419f61a5f2d36511296170d23af314f402f7271138bb455bd69c5c63e61c3c3a445b12ce0df6434bc20b11781811ab6b5321656fc9a0c212f4a3cb8cf717da3922a8b2f8cc04a130f1db915a76682ecfd0ddf62f789ac5c2505994aa2da81d835c0418a62b983340ba84afad523135419a7354bf287a97d5b7d0386af481bd1dc5b1337841dc0d0f202a772d930e20054d3a611db1c739093bd5c9b6a8e6b576b6bccb2855e4947b69f25178b709135e4c54f0e57a4139ae9cb8b81c2866b2fecb3abf5fc527eb7787554a2f892f7e2a277c7f64ddbbaf82dbb842e4e2c3c69ae11bbc54f032e209d2d88b20fd9bb6920a79a05a3b49a7cf193385c24d5acc04e928501ed6a5e18e3c9a966452142d5f4eb220bfb05d930415e72901d7b8183ec17e439eec153e34d0c0d841af6da8676b03c762b0df09a2cf3133b4e5078d0bb34888d17dbb58d5c526006e5850d2f2c5ed39cdccf244ffed3303d677f99c4bbebe215f53ddf2e7e1a7e8a122f32f060e18cffa461625bd1c20b928c371337446027f9df869b2451e512ffd98d6a99b96b76919731c36811624698a5d3457607cf7218e381c9787bdef6eca791c9b7225d8c36be72ec75545e34e24d9018d9b82dd220c9bb535cedd7ba91843ee6776fee1443f5263fde64cd2a964e9c2c6088e72c4e165ee0e05b9b00163ffbea8b99246e88a22569e0c1d0aa5c35d2e4057087e9164e66134bdc104b3bb0c245c309911138b7e1c269ac1b3b41e11ad03528f232aa28441b4093ec3bd4f8277b8f2ea5dbf1c773c4e96269ef84ce193a776ebd9ca7782b6fce10bfd3e36cc959419cfdf7ed38369c53d51d2c6a27c52fcdbb74d1225c6fde21a41a6e64c0f9192acf0a8c33b717617a72a2e24d5c30bf6377b377af11db305dd80dd3b3bc457a72343169b23082f8255cf8e788766b38abf012ba20abefef1be2d98e93725f17a408e559e5562ecf1a8656d6c8f67f894b76d243c30b76dbcd5fddb78be130b4ae2fd970c25b3fb47005b2bd883dbccb05b780257efefc7943bce41d3da76db4f13e1c22af61a0c8351a300c5e3ce7bb63078dc4f62394cbc76c836dc70d682f92efbe1118d976a09d6b33d9af65278687f093825caf38a0bc21626f6b136d9ebb21fc8c29b529c034991603188073fec14cab4d502405be93f477403d93ad36d36ad3e42d4583162059067c275b6d3263415efc8f958d733ee419bfcc34267b49b401d76c36290ae029b4b374abd9bc2146c80be6449bbe21a42024da14dba469a6c5df1053cf22da2c096e0831bba2c81b626265159137c45f86f50f74c27f48a2fd6ff206fffbfb86b8cb5a1a4736cc9ef994fd000eb02cd76c02f28618c5590e0d7892a4418bf979430c2fa02f7bf8f386e85c492f788bc4bdac0cc7365986fe79433c959321a010ce63dc5d01cdf34962c8ec4e17194e7e43b403fc3b88236361e3cb7171998dcbdf78a1b9f97aa8d5d65a6dadd5d65a6dadd5d65a6dadd5d65a6dadd5d65a6dadd5d65a6dbd466d2d385ad68bb973a13af0cb3aeccf1bc23212633782995a1324fb67ef9f801bf607b4e88611046162245e18dcee286f37868fceabd7274bed546dd06ced746d26536b8f2bd9806bd34c9ba66e29866e019e21d9374af68b81e277b46c9a06ad56a96583032d9b273f50cbe659ae49ef345a8e62298a6bb6e8d35a76464f951af0ae8ba7b5ec93f45768d92d12d074b3d4b2f13cecd4ecd657aad9d926fcb5aabd5fdff9cd5c91ae6ac07b1d76afb0e6ef4aa1af16ebe35061adaaa139f52b5695eb8c7b4e5633b78f676ea7594bc9f5089b8e1d43d11c5d7d700668b4d295612a53b26788fcd2a087e998923796887c4319b996283f9bb4bcd1a8692af7d04a7fbaf3b4ecfe03704dbf1be8aae4493dc0777c802cb13bd7d489fbe8848e247653d3e7494d9d4426c56e715ee7cec1653b20d114d6d5297933f0d17240a1d4f0e599d50133ed09f8d0e79347efcec9da3998b3aea9c85b287667fa93c0bf3cf14d7b73973efbdd244f83adaef6294361838132423018ffc8cae90a3bc7ed72fef52fe2d3787c18248b10217b71215efa96be64e51cf94bb029d5a201c5d3bf0d9bb2870c1dfc49d814b000f017c3a6450f2f864df7f49733748a6c019e67f60c9d236bdcb4c64d6bdcb4c64d6bdcb4c64d6bdcb4c64d6bdcb4c64d6bdcb4c64dffc7a185b71aead7a0a76fdad120c177234ddceff8d5b806463d5770a77eb30c7b09924ab759a6cdf2b73cc3b6289ea65a1f8ea4521fa878b79a34c7ec945cbe45514c0b70a7f5ee1647f27c49beebe169bdfb24fd157a3745d22c49967a379e861a48ad81d4afe27667b9cc5b44d5f47952eab0cf50946783f9686906938d49ad635ded8f4ac4730e90d5eb471a3d4c2722da6a4a3fd6a75d6089ee18fa3c63ab80ef041895e53ade9d63897202c5b56b89539c963ac236ab7f87a84a1d6166522ca92928d5d5fe769f7f974822eb9aca9493c40468fe34817edfd53660859f332ee91c494489a658e8d113028d965243692d0d5af674a50f0c653dd755294f3f818de677670355d898b4cb3e06da6a309ba6c3bb6a5ddd8d4e0f8bb6e6ff074f77dee0096c756504a03f4d4c116d2d514e074a77aef7fa08d2437ed8615683d95d3abc0f99d1fd782b39953a3ba423796462a9c2d6eaf591a690bcd19b90f03e5c0eb60f9bd1a628fb1c32c37b6735d8483ff665ef12a923250365bdd4e9c912d293b9aef6377fa9938da54cbd23eda4fe7a7e581d3cbf272c6130a98e2d6e9346f1a9257623d397375391df58f7a1f3783f2687ab2add9d63fadd447f0e1ddce6def0c7fe7942aa29201bf71211afb64753fb01dc8647fa52aea1445326733823d7c3194cf3317ce01eefef9861e7d818c8295e6f4f20d6b3b157a54457d648a3279149315e27489a9228a77a47e05fc6e15722e70d92fabeb24d370ce7f155d2fd64b19d6c6758ee42d9ceb60177dbe438a6c5737f40b6d37f4eb6d38064d9b3b21d006e475ef6f09c6c3f417f856c67298a244129dbf134d4b2bd96ed5f27db4fb28ab792fdd839e944615dcd5f230973cb682f957b1364aa42aca913b4931890169056e1e455c951e5f0ef4904984999de6be97ab003189b749f7cf4eed84c8a56e9a44e5937ad2bfdd0a4f8c5401d2118e8083a87120bf6fa4b4be46726b5e2a4eee47e5a95f03d7d69f6e4449f82a5a5b0e41b0925e6f953b14b1af7a133a02709ecf08bc7de30b176e3b7c9cf6db39d05a4e48de5a354df8099aef64978427ae2b3e3bb57edf465577fe053b337e7a41e96e8655b2c9f8f2d05a013e38b9f7530b6943c83be4c3ebe1a0b5d947d4d9563eb012c7571ca49f7e3cdf0753b8ab2cf455b8ff433c9db1f35ed8d804c7fecbca8e48faf94b03cff1d867e140676905c2362cf94dbcb5870898c65da806db3d42d001c07489ee13f5cc6327f4ec63224d7e4c11919dba4d9d2b0a8ece119197b8afe0a19dba2289e6d55642ca8656c2d63bf50c69ee115a785ec38138ebdfed214d708ce4b43a32c3f81229fea0a4b6634ba8f82c17ca7d2dc6186ba17b402a92b60658a5d527f2e04406faf56e7aa2a56d15d4361110caa42952c858ea974d34c353e1090a755dc4c10fb860279adda07c497c25ca5464bd3d7237dc3b29a02e24335f33dc1ba53e7cb3149b4621c5e09fac4a4e5d4ea0aae253a9cd4e1f10601fa7c6c8a591b76e5fb2fbbf28300b9a6b2aa3e2f1b2fa0a1a20ef100ce7829c705f1bb3ea7b6d24dcc52300a9449ad81a9c8272089f37dd8e53d2bddd56b81fc666ebc72dcbfd0f86b61bd63edb5b07ed72996062d96fb7de3ae439f589efd93c65d14db6c9e51445f09b0bc83e4c5c65d7bfacb85240d48aed96ad53eb1b56d576ddb55db76d5b65db56d576ddb55db76d5b65db56d576ddb55db76fdff416b16d61719732dac8c609977f81ae4f964a9d26eab495f7cb6cb825b40b13c4f51a0f9e1b833f7e770671670806c9d3ddb65418923ef7a78f66cf738fd352a758b64786e6fb7954d438d3bd7b8f3a771b2d30ce537cf7231eca923188c2293624a0b2cabd7772d510eece7d029eb29ce2dcbb33f72141b8a9c5a0f05e4bb0d7fd8749c4822f225515e979662c8429aef2e4d2ad90ee623048349a4fb68a6a91334a57800fd111aa0090bc5a923f91364791286baf75069b76329ecdc14a79cd44b9a59bd96d8252db5f0e57d1809180e0d92e6e04940666f8470ba845b878e25ba11dc0811dcdc79aac878524fdeeacad8d1d4a1a33d092b53ecce74c55d9ae204416fe598f424d4d53e6da823248988947a56648a2b47f3a70ef4e5ffe8ca88c4e58379563792c4eecad89d357b826fd292a3f97264fad0d1fcb56bfa715c6d8f16c8599da84ff791245a4b436103a937594abd11b23a426475f2f375e809005228d537026988d337fec552c75d424f5899f40499c124d294756c3f09aed4135c4b1c85f95cb04bab53d683cf85074a094f4795e302771cc8a92eaed1a327dceb5b723fa6c5796ee53c7863f968561e3d54cedf212db81a55ced56325efa08e33e7f591464dab742b4d9d848fdedd121f3bf85f70cebcb0e3305dc03cf2e46909be27db896c9afa354f67966e72cdb707c6d762e19918ab0aeee69f03c35b7c8ba6af081099f7f072307c4f7f85e4666886a99c18e3d9a8d1f01a0dafd1f01a0dafd1f01a0dafd1f01a0dafd1f01a0dafd1f01a0dff5fc490f60ae9a762e1e5631b7bf7c2ef0bd3805705843c5b74a760731c7521264eb749f6966d8226c7d0f4c7db62b7fe1c26ce5134db3a8789b37c8bdd91973d3c83899fa2bf42b366c91660f69a359e861a13af31f14fe267ef30963fe8bcec775786bc87cca55edfd39461aaa9326988fcc65023d712d1d2f4d8c5636f889d8dadd9c30f8342a97e1f3a636a0d203d4110f597a638fd612a32a92913d7121f30fcbeb3fedd8787c4b069027bfda5dd9b27d0e7637d1c6d2c658d5d7f064f820b45d4833e0f606fc849ddd8cbdb22a7968861f6ac9da9b175b7031f2d2544fe903ac24657474b4bedcf7435b7f41e3c09a44925c80cfac8f211b2e8614647e96adf83dbd05136fd19f465d712a79e3a0e130c794fad8d49cbabe2be6ba993d0a4fb91ed774943e1d3811f3370c346e6a66f65cf80bebccdc36deeaca64907d2b287c3638add59960fe9cb9e3f78227f68aa10c9bebc81145a9ab3d0c91dc0d933ede0a9e1385a180a3b7ff4847bb3276f2d51de4cb3319df623d39f44a60f7f6d4e947d799c96fb489f614bfdfdfcdc5fd0c655e848621299fe98931e46b1a58e485ded3f99d41a5db13eb696d2c7c714c51ad9f5f9390f273a42636534c356f477d9f37656eb13a4fb5d60f6c63f2e85f3074f64f2e98ed57b36508d0e7bd559fbe972a5153b495e186f9a6dd3fc6d8b0414dd04fcc7c79bfec8af3af16c93298fc3199a6b7274ab7936de74932ec39e945d3c176ffa04fd155b8b2668e5a15b9ef6f3506f2deaadc5676d2dcef0863f1c66da1b3a323e4ed697a6b85e5af9916da2296ca4fbf2d6ea0df7aed59ee04abd1132452d81140a4c5f9e4b22da425a46d013eea79ee09afed8d19ef2235f43d1238dea92fa53768ff9216d8491898f9f274b8d96135d6149491cb9a62778fa93e067b241ef08aed51142ab37c964fc724065b4c3f458986bdc76b2fbf43c478f936de8c86277a353320937c27df6fce7d29be96e35983dac257114eaca6821892e69f584eda3d75aea223f831bf6f51135de43e90ab3c4de6eea881cd0131752c97640cb2b9deebbba988d139fc95f6477d8d012419c8d21f4eeb27e7ad9bec1288ebf0db19bea94bc7db38f137178ed64ef75f566bfd087f408e8aa74f9fe8092633d18453ac522bd03a25df94b656bc5db6fab2b6b7f5accf3a327345f4eec818e8620a7b575c72ff7140726186f439b177b814a9bb2b5b93b9a377d9894aeea62257cf941ff2b21cd0fcc4d262924f3fbbbfddf6b57f26c6f32088e8e470a697d56ec558f990ca4c743aab35bcbcfd66cb1eff6d8d8a4462ef6e4dbbd2f68b757bbabecbd8a7b8577de6e8ecfcce95657461b5d9d3c9e0c43e0e521002a26294599b1635128363b426ad2e3dccca4d747ba8f185d19c7526f14997e6e8ad2a787b92eb3f3302ccc500c3adbcbf289a146a8e391c960fc5526128d5f72bb7fcfe79eba34ae0dc3b729eab6450192a100fdd1716d58f223bffef10ae76935791a9cf3b9679b5ce9435ff6f01cce7382fef2cd18036892e3f631eba83aae4dbd19fbd4cdd825fef5b9d1e1bbe8ce6869aac2d2f42b468b1bb66bfa5dcf14a74efea18fd7e8ce81e4cccb90c0857751a2ab135717bba4868de0de06b6192805fdea524999d1ad5dcd97e3d237bef44b5fe13079a63f7de5ab2eed772c4e5491e217fab3df878ee44947b4f84ff02daf4ef2ebd00a57dacd9f2f5d7e7684a69a171e16706d006e798e26499a62dffaa4ffae10f9c82f8ebc12227c93e6b0c67dda80be592200650fcf1ad01fa7bf4288302c4d57bed39ccf432d456a29f27952e41d16b117276f142a5558c14345f0415785d8a411569a4f2a5027d9f4a789834accb6113245796689fc66176ec454baa446b9434de923b3f7266ccc85cfc8eaee2328ae238dea6e1fbdbbf589a8aeafc46511ea44ec7abab23e8c3c7aedf3af8b3f373329ac1ccf5f473b1d946163aa91450517fad69bc8a88327808a6d43628ab2ab8b99b897635d196130bf1aeac55080abef95fc77b62879a4da43ff852321610ec2eaf4112cfc285e85d4c9d64f642ae875a89d7c8c680164f335bc1792c3f03a5939804c85dfd8cfa133dc0a2363fc262eddda523020c5bd2d3b5a5959b9e7bb95f9aa1c5e070a58c260ce49f7d2ea5999efe3d989f2c654505a7c53ad07fdee4c9705047db4d4bb238443e6f4869c74ffb0da2bb8e4a53e2f2e1491a2abfd6d363f8f9e507d9fcb43918377d9bbbb18a879df9741880fdf33a10c593450b37bec41d96a84e46a4cc4c193b02ae21f92a3fb22d26e6f92ad51647585250cc69cd4dbf9dbc449eea791c758c4c08532224d5a3ad78683504a1510aedc22e6fe20936c4ccbf1ff7387ab95ed77773f9e47013875b4d29451d687fd37f48abcddfbb47f8f6412fa68062ba013beff8a1f9561ae3a38d4569ac769ac84bef2f640db9939dbad8d1c482dfca8e09188ca56019aeea22def232a63001069f4640967bf220b72b0f0d3c347ed25b0652f6d1446be1d24bbb8a4df631b2eecab50a28b6b296123faa28fc6d26d966cb3e096a4388e6518e6e361a33ff7a9031e507c13bbb49e848d5a7b17d85d07cfa146c7c9afd8ef375986e2f83d6844d75f8cadb7fb9fb8ddbf984d1c41913ec075f5b50a51b5ff9916a1744f9fbbbc15dfbb4fbf5eba1da9a052a4d91ba60625b33bb138a527aed59377fd19e8ea7cb785c1b41ade5e1ca5bd1fe76ac9578b924c16c49101ed6b24c7a942253604980b050568b3cc2dc33314cd50e0e305c59f8b9bcf531cc7600fcad376a4a0242f7b78d68ef438fd15a282679b74256e3e9e865a54d4a2e2b344c529be701a10aa72f3aa92f196a3ff0e7873fc24becae53f91e3468bd0b713d74eaffa58c9c952a5733c7f693c1bba4df3b7140368966bb6980fe7b97f2e8e3acf907c8b3bf3fdef164782d260aeece1d9ef901da7bf9ce7b2a04903c0eebde2f93a9e4dcd733f91e79e640cbf72a85b41950ac46fba8b4e32054bd347a4491fdf969b0a9f1aea6409f1d739d89949f79189cddbf8e4c064aa449ee49149b19125f29be3dbf74b10fd774e073c90e8ea646328a3ed4029db7f29827812bdab989221d39fac4c0aa515f436c923c6e060eae5f34b64b757208b85a9de093431d1d4c9cc787825f4ae45d1bed674e95a1fb5338e69806c5d2add9836e06f01437134c9311f7fd8fc9111d05f4937966c82b39e691cc996d2aaece159e9769cfe0ae94637c916dddc6b14781e6af1568bb7cf126f67fdd1bece67a96a135bb10f5633262c7b59fd86c2069d00a79f7445c3f564ed9dd0fda5a5de5de2eb960998e36d1dbfabd15485e006fb3b0523a0f911d268fc71caa4b4cddd0b6441a346f86b22d53e979fb4bad846f943fcc9721fad0beca90ffa7ce4f8389f93e9e576e2811ce77e7c6465be84d2c65af2a4533e7f85359895686a2ed82d5fde6841793fd0d5f18ffccb26eb08d2c575cfc2e1e57675147e79c53d79a58b5d12cf778774a0d82535ca29ea98205394b37baffd077f141ff1f44ef909eebe98622add15dcf0db220460d547b0788685f2cfa8918ea50a736ce35e1c9de20dcaddf5cfd6551d99c1e4d8f8947dc7c7a3bfe91bfa9beff4b18f9ebef14dbc784d7d940f63f7702d9f3a9e1d646d5626d9bb5f98aa64e3f4357ce0bd0d67be0db0ebe06f75f0b73af85b1dfcad0efe56077fab83bfd5c1dfeae06f75f0b73af85b1dfced5a44eae7ff010000ffff010000ffff5a3234d444bb0000`)))
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/ci.sh b/test/e2e/ci.sh
index e09ae41b8e0..d6fe18c0db3 100755
--- a/test/e2e/ci.sh
+++ b/test/e2e/ci.sh
@@ -17,8 +17,8 @@
 source "$(dirname "$0")/../common.sh"
 source "$(dirname "$0")/setup.sh"
 
-kind_cluster="kind"
-create_cluster ${KIND_K8S_VERSION} $kind_cluster
+export KIND_CLUSTER="kind"
+create_cluster ${KIND_K8S_VERSION}
 trap delete_cluster EXIT
 
 test_cluster
diff --git a/test/e2e/local.sh b/test/e2e/local.sh
index 0d0d82e0249..51eaf993d38 100755
--- a/test/e2e/local.sh
+++ b/test/e2e/local.sh
@@ -17,13 +17,13 @@
 source "$(dirname "$0")/../common.sh"
 source "$(dirname "$0")/setup.sh"
 
-kind_cluster=local-kubebuilder-e2e
-create_cluster ${KIND_K8S_VERSION:-v1.18.0} $kind_cluster
+export KIND_CLUSTER="local-kubebuilder-e2e"
+create_cluster ${KIND_K8S_VERSION:-v1.18.0}
 if [ -z "${SKIP_KIND_CLEANUP:-}" ]; then
-    trap delete_cluster EXIT
+  trap delete_cluster EXIT
 fi
 
-kind export kubeconfig --kubeconfig $tmp_root/kubeconfig --name $kind_cluster
+kind export kubeconfig --kubeconfig $tmp_root/kubeconfig --name $KIND_CLUSTER
 export KUBECONFIG=$tmp_root/kubeconfig
 
 test_cluster -v -ginkgo.v
diff --git a/test/e2e/setup.sh b/test/e2e/setup.sh
index e0146ebe73f..989bf72e338 100755
--- a/test/e2e/setup.sh
+++ b/test/e2e/setup.sh
@@ -19,26 +19,32 @@ export PATH=$kb_root_dir/bin:$PATH
 fetch_tools
 install_kind
 
-# Creates a kind cluster given a k8s version and a cluster name.
+# Creates a named kind cluster given a k8s version.
+# The KIND_CLUSTER variable defines the cluster name and
+# is expected to be defined in the calling environment.
 #
 # Usage:
 #
-#   create_cluster  
+#   export KIND_CLUSTER=
+#   create_cluster 
 function create_cluster {
-  if ! kind get clusters | grep -q $2 ; then
-    kind create cluster -v 4 --name $2 --retain --wait=1m --config $(dirname "$0")/kind-config.yaml --image=kindest/node:$1
+  : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"}
+  if ! kind get clusters | grep -q $KIND_CLUSTER ; then
+    kind create cluster -v 4 --name $KIND_CLUSTER --retain --wait=1m --config $(dirname "$0")/kind-config.yaml --image=kindest/node:$1
   fi
 }
 
-# Deletes a kind cluster by cluster name. The kind cluster needs to be defined as a variable instead of an argument
-# so that this function can be used with `trap`
+# Deletes a kind cluster by cluster name.
+# The KIND_CLUSTER variable defines the cluster name and
+# is expected to be defined in the calling environment.
 #
 # Usage:
 #
-#   kind_cluster=
+#   export KIND_CLUSTER=
 #   delete_cluster
 function delete_cluster {
-    kind delete cluster --name $kind_cluster
+  : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"}
+  kind delete cluster --name $KIND_CLUSTER
 }
 
 function test_cluster {
diff --git a/test/e2e/utils/kubectl.go b/test/e2e/utils/kubectl.go
index b963de7dac0..40e6f79b93e 100644
--- a/test/e2e/utils/kubectl.go
+++ b/test/e2e/utils/kubectl.go
@@ -27,7 +27,8 @@ import (
 // Kubectl contains context to run kubectl commands
 type Kubectl struct {
 	*CmdContext
-	Namespace string
+	Namespace      string
+	ServiceAccount string
 }
 
 // Command is a general func to run kubectl commands
diff --git a/test/e2e/utils/test_context.go b/test/e2e/utils/test_context.go
index c37949d3ec0..57e72b70cb7 100644
--- a/test/e2e/utils/test_context.go
+++ b/test/e2e/utils/test_context.go
@@ -56,8 +56,9 @@ func NewTestContext(binaryName string, env ...string) (*TestContext, error) {
 
 	// Use kubectl to get Kubernetes client and cluster version.
 	kubectl := &Kubectl{
-		Namespace:  fmt.Sprintf("e2e-%s-system", testSuffix),
-		CmdContext: cc,
+		Namespace:      fmt.Sprintf("e2e-%s-system", testSuffix),
+		ServiceAccount: fmt.Sprintf("e2e-%s-controller-manager", testSuffix),
+		CmdContext:     cc,
 	}
 	k8sVersion, err := kubectl.Version()
 	if err != 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/e2e/v3/plugin_cluster_test.go b/test/e2e/v3/plugin_cluster_test.go
index 589b94cb3c0..e03d6a8e46e 100644
--- a/test/e2e/v3/plugin_cluster_test.go
+++ b/test/e2e/v3/plugin_cluster_test.go
@@ -69,6 +69,11 @@ var _ = Describe("kubebuilder", func() {
 			})
 
 			It("should generate a runnable project", func() {
+				// go/v3 uses a unqiue-per-project service account name,
+				// while go/v2 still uses "default".
+				tmp := kbc.Kubectl.ServiceAccount
+				kbc.Kubectl.ServiceAccount = "default"
+				defer func() { kbc.Kubectl.ServiceAccount = tmp }()
 				GenerateV2(kbc)
 				Run(kbc)
 			})
@@ -166,7 +171,7 @@ func Run(kbc *utils.TestContext) {
 	_, err = kbc.Kubectl.Command(
 		"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
 		fmt.Sprintf("--clusterrole=e2e-%s-metrics-reader", kbc.TestSuffix),
-		fmt.Sprintf("--serviceaccount=%s:default", kbc.Kubectl.Namespace))
+		fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount))
 	ExpectWithOffset(1, err).NotTo(HaveOccurred())
 
 	_ = curlMetrics(kbc)
@@ -263,7 +268,11 @@ func Run(kbc *utils.TestContext) {
 // curlMetrics curl's the /metrics endpoint, returning all logs once a 200 status is returned.
 func curlMetrics(kbc *utils.TestContext) string {
 	By("reading the metrics token")
-	b64Token, err := kbc.Kubectl.Get(true, "secrets", "-o=jsonpath={.items[0].data.token}")
+	// Filter token query by service account in case more than one exists in a namespace.
+	query := fmt.Sprintf(`{.items[?(@.metadata.annotations.kubernetes\.io/service-account\.name=="%s")].data.token}`,
+		kbc.Kubectl.ServiceAccount,
+	)
+	b64Token, err := kbc.Kubectl.Get(true, "secrets", "-o=jsonpath="+query)
 	ExpectWithOffset(2, err).NotTo(HaveOccurred())
 	token, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64Token))
 	ExpectWithOffset(2, err).NotTo(HaveOccurred())
@@ -271,10 +280,11 @@ func curlMetrics(kbc *utils.TestContext) string {
 
 	By("creating a curl pod")
 	cmdOpts := []string{
-		"run", "--generator=run-pod/v1", "curl", "--image=curlimages/curl:7.68.0", "--restart=OnFailure", "--",
+		"run", "--generator=run-pod/v1", "curl", "--image=curlimages/curl:7.68.0", "--restart=OnFailure",
+		"--serviceaccount=" + kbc.Kubectl.ServiceAccount, "--",
 		"curl", "-v", "-k", "-H", fmt.Sprintf(`Authorization: Bearer %s`, token),
-		fmt.Sprintf("https://e2e-%v-controller-manager-metrics-service.e2e-%v-system.svc:8443/metrics",
-			kbc.TestSuffix, kbc.TestSuffix),
+		fmt.Sprintf("https://e2e-%s-controller-manager-metrics-service.%s.svc:8443/metrics",
+			kbc.TestSuffix, kbc.Kubectl.Namespace),
 	}
 	_, err = kbc.Kubectl.CommandInNamespace(cmdOpts...)
 	ExpectWithOffset(2, err).NotTo(HaveOccurred())
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..c8b327dcca8 100644
--- a/testdata/project-v2-addon/PROJECT
+++ b/testdata/project-v2-addon/PROJECT
@@ -1,13 +1,49 @@
 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-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-addon/go.mod b/testdata/project-v2-addon/go.mod
index c853a92bb0f..158ead9deb9 100644
--- a/testdata/project-v2-addon/go.mod
+++ b/testdata/project-v2-addon/go.mod
@@ -4,6 +4,8 @@ go 1.13
 
 require (
 	github.com/go-logr/logr v0.1.0
+	github.com/onsi/ginkgo v1.12.1
+	github.com/onsi/gomega v1.10.1
 	k8s.io/apimachinery v0.18.6
 	k8s.io/client-go v0.18.6
 	sigs.k8s.io/controller-runtime v0.6.4
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-multigroup/go.mod b/testdata/project-v2-multigroup/go.mod
index 48e0bcb882c..d0ffad76e06 100644
--- a/testdata/project-v2-multigroup/go.mod
+++ b/testdata/project-v2-multigroup/go.mod
@@ -4,6 +4,8 @@ go 1.13
 
 require (
 	github.com/go-logr/logr v0.1.0
+	github.com/onsi/ginkgo v1.12.1
+	github.com/onsi/gomega v1.10.1
 	k8s.io/api v0.18.6
 	k8s.io/apimachinery v0.18.6
 	k8s.io/client-go v0.18.6
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/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-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 57b94c83092..0d462758f6c 100644
--- a/testdata/project-v2/go.mod
+++ b/testdata/project-v2/go.mod
@@ -4,7 +4,10 @@ go 1.13
 
 require (
 	github.com/go-logr/logr v0.1.0
+	github.com/onsi/ginkgo v1.12.1
+	github.com/onsi/gomega v1.10.1
 	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/Makefile b/testdata/project-v3-addon/Makefile
index 16d0b8de6c8..939a04def8d 100644
--- a/testdata/project-v3-addon/Makefile
+++ b/testdata/project-v3-addon/Makefile
@@ -46,7 +46,7 @@ vet: ## Run go vet against code.
 ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
 test: manifests generate fmt vet ## Run tests.
 	mkdir -p ${ENVTEST_ASSETS_DIR}
-	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh
+	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh
 	source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
 
 ##@ Build
diff --git a/testdata/project-v3-addon/PROJECT b/testdata/project-v3-addon/PROJECT
index f5b6f774bc0..8dc17cbd77c 100644
--- a/testdata/project-v3-addon/PROJECT
+++ b/testdata/project-v3-addon/PROJECT
@@ -1,5 +1,22 @@
 domain: testproject.org
-layout: go.kubebuilder.io/v3
+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-addon/config/manager/manager.yaml b/testdata/project-v3-addon/config/manager/manager.yaml
index 46a82393bfd..79adfe72a55 100644
--- a/testdata/project-v3-addon/config/manager/manager.yaml
+++ b/testdata/project-v3-addon/config/manager/manager.yaml
@@ -52,4 +52,5 @@ spec:
           requests:
             cpu: 100m
             memory: 20Mi
+      serviceAccountName: controller-manager
       terminationGracePeriodSeconds: 10
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-addon/config/rbac/auth_proxy_role_binding.yaml b/testdata/project-v3-addon/config/rbac/auth_proxy_role_binding.yaml
index 48ed1e4b85c..ec7acc0a1b7 100644
--- a/testdata/project-v3-addon/config/rbac/auth_proxy_role_binding.yaml
+++ b/testdata/project-v3-addon/config/rbac/auth_proxy_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: proxy-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-addon/config/rbac/kustomization.yaml b/testdata/project-v3-addon/config/rbac/kustomization.yaml
index 66c28338fe0..731832a6ac3 100644
--- a/testdata/project-v3-addon/config/rbac/kustomization.yaml
+++ b/testdata/project-v3-addon/config/rbac/kustomization.yaml
@@ -1,4 +1,10 @@
 resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
 - role.yaml
 - role_binding.yaml
 - leader_election_role.yaml
diff --git a/testdata/project-v3-addon/config/rbac/leader_election_role_binding.yaml b/testdata/project-v3-addon/config/rbac/leader_election_role_binding.yaml
index eed16906f4d..1d1321ed4f0 100644
--- a/testdata/project-v3-addon/config/rbac/leader_election_role_binding.yaml
+++ b/testdata/project-v3-addon/config/rbac/leader_election_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: leader-election-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-addon/config/rbac/role_binding.yaml b/testdata/project-v3-addon/config/rbac/role_binding.yaml
index 8f2658702c8..2070ede4462 100644
--- a/testdata/project-v3-addon/config/rbac/role_binding.yaml
+++ b/testdata/project-v3-addon/config/rbac/role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: manager-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-addon/config/rbac/service_account.yaml b/testdata/project-v3-addon/config/rbac/service_account.yaml
new file mode 100644
index 00000000000..7cd6025bfc4
--- /dev/null
+++ b/testdata/project-v3-addon/config/rbac/service_account.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: controller-manager
+  namespace: system
diff --git a/testdata/project-v3-addon/go.mod b/testdata/project-v3-addon/go.mod
index 9419f0ce644..79cebd20f2f 100644
--- a/testdata/project-v3-addon/go.mod
+++ b/testdata/project-v3-addon/go.mod
@@ -4,8 +4,10 @@ go 1.15
 
 require (
 	github.com/go-logr/logr v0.3.0
+	github.com/onsi/ginkgo v1.14.1
+	github.com/onsi/gomega v1.10.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v0.19.2
-	sigs.k8s.io/controller-runtime v0.7.0
+	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-config/Makefile b/testdata/project-v3-config/Makefile
index 16d0b8de6c8..939a04def8d 100644
--- a/testdata/project-v3-config/Makefile
+++ b/testdata/project-v3-config/Makefile
@@ -46,7 +46,7 @@ vet: ## Run go vet against code.
 ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
 test: manifests generate fmt vet ## Run tests.
 	mkdir -p ${ENVTEST_ASSETS_DIR}
-	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh
+	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh
 	source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
 
 ##@ Build
diff --git a/testdata/project-v3-config/PROJECT b/testdata/project-v3-config/PROJECT
index 804ab898289..a16b39f303d 100644
--- a/testdata/project-v3-config/PROJECT
+++ b/testdata/project-v3-config/PROJECT
@@ -1,6 +1,14 @@
 componentConfig: true
 domain: testproject.org
-layout: go.kubebuilder.io/v3
+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/manager/manager.yaml b/testdata/project-v3-config/config/manager/manager.yaml
index 082357842b1..3bb0ff3aabc 100644
--- a/testdata/project-v3-config/config/manager/manager.yaml
+++ b/testdata/project-v3-config/config/manager/manager.yaml
@@ -50,4 +50,5 @@ spec:
           requests:
             cpu: 100m
             memory: 20Mi
+      serviceAccountName: controller-manager
       terminationGracePeriodSeconds: 10
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-config/config/rbac/auth_proxy_role_binding.yaml b/testdata/project-v3-config/config/rbac/auth_proxy_role_binding.yaml
index 48ed1e4b85c..ec7acc0a1b7 100644
--- a/testdata/project-v3-config/config/rbac/auth_proxy_role_binding.yaml
+++ b/testdata/project-v3-config/config/rbac/auth_proxy_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: proxy-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-config/config/rbac/kustomization.yaml b/testdata/project-v3-config/config/rbac/kustomization.yaml
index 66c28338fe0..731832a6ac3 100644
--- a/testdata/project-v3-config/config/rbac/kustomization.yaml
+++ b/testdata/project-v3-config/config/rbac/kustomization.yaml
@@ -1,4 +1,10 @@
 resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
 - role.yaml
 - role_binding.yaml
 - leader_election_role.yaml
diff --git a/testdata/project-v3-config/config/rbac/leader_election_role_binding.yaml b/testdata/project-v3-config/config/rbac/leader_election_role_binding.yaml
index eed16906f4d..1d1321ed4f0 100644
--- a/testdata/project-v3-config/config/rbac/leader_election_role_binding.yaml
+++ b/testdata/project-v3-config/config/rbac/leader_election_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: leader-election-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
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/config/rbac/role_binding.yaml b/testdata/project-v3-config/config/rbac/role_binding.yaml
index 8f2658702c8..2070ede4462 100644
--- a/testdata/project-v3-config/config/rbac/role_binding.yaml
+++ b/testdata/project-v3-config/config/rbac/role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: manager-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-config/config/rbac/service_account.yaml b/testdata/project-v3-config/config/rbac/service_account.yaml
new file mode 100644
index 00000000000..7cd6025bfc4
--- /dev/null
+++ b/testdata/project-v3-config/config/rbac/service_account.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: controller-manager
+  namespace: system
diff --git a/testdata/project-v3-config/controllers/admiral_controller.go b/testdata/project-v3-config/controllers/admiral_controller.go
index baffc35d661..515d172fe96 100644
--- a/testdata/project-v3-config/controllers/admiral_controller.go
+++ b/testdata/project-v3-config/controllers/admiral_controller.go
@@ -46,7 +46,7 @@ type AdmiralReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *AdmiralReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("admiral", req.NamespacedName)
 
diff --git a/testdata/project-v3-config/controllers/captain_controller.go b/testdata/project-v3-config/controllers/captain_controller.go
index cda79c4cfe7..fd69e3f79a4 100644
--- a/testdata/project-v3-config/controllers/captain_controller.go
+++ b/testdata/project-v3-config/controllers/captain_controller.go
@@ -46,7 +46,7 @@ type CaptainReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("captain", req.NamespacedName)
 
diff --git a/testdata/project-v3-config/controllers/firstmate_controller.go b/testdata/project-v3-config/controllers/firstmate_controller.go
index 287e6af5c39..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.0/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/controllers/laker_controller.go b/testdata/project-v3-config/controllers/laker_controller.go
index 8cd49b8c169..cae6fdcf051 100644
--- a/testdata/project-v3-config/controllers/laker_controller.go
+++ b/testdata/project-v3-config/controllers/laker_controller.go
@@ -44,7 +44,7 @@ type LakerReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *LakerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("laker", req.NamespacedName)
 
diff --git a/testdata/project-v3-config/go.mod b/testdata/project-v3-config/go.mod
index 17104c35fcb..073e09a9498 100644
--- a/testdata/project-v3-config/go.mod
+++ b/testdata/project-v3-config/go.mod
@@ -4,7 +4,11 @@ go 1.15
 
 require (
 	github.com/go-logr/logr v0.3.0
+	github.com/onsi/ginkgo v1.14.1
+	github.com/onsi/gomega v1.10.2
+	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v0.19.2
-	sigs.k8s.io/controller-runtime v0.7.0
+	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-multigroup/Makefile b/testdata/project-v3-multigroup/Makefile
index 16d0b8de6c8..939a04def8d 100644
--- a/testdata/project-v3-multigroup/Makefile
+++ b/testdata/project-v3-multigroup/Makefile
@@ -46,7 +46,7 @@ vet: ## Run go vet against code.
 ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
 test: manifests generate fmt vet ## Run tests.
 	mkdir -p ${ENVTEST_ASSETS_DIR}
-	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh
+	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh
 	source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
 
 ##@ Build
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-multigroup/config/manager/manager.yaml b/testdata/project-v3-multigroup/config/manager/manager.yaml
index 46a82393bfd..79adfe72a55 100644
--- a/testdata/project-v3-multigroup/config/manager/manager.yaml
+++ b/testdata/project-v3-multigroup/config/manager/manager.yaml
@@ -52,4 +52,5 @@ spec:
           requests:
             cpu: 100m
             memory: 20Mi
+      serviceAccountName: controller-manager
       terminationGracePeriodSeconds: 10
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-multigroup/config/rbac/auth_proxy_role_binding.yaml b/testdata/project-v3-multigroup/config/rbac/auth_proxy_role_binding.yaml
index 48ed1e4b85c..ec7acc0a1b7 100644
--- a/testdata/project-v3-multigroup/config/rbac/auth_proxy_role_binding.yaml
+++ b/testdata/project-v3-multigroup/config/rbac/auth_proxy_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: proxy-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-multigroup/config/rbac/kustomization.yaml b/testdata/project-v3-multigroup/config/rbac/kustomization.yaml
index 66c28338fe0..731832a6ac3 100644
--- a/testdata/project-v3-multigroup/config/rbac/kustomization.yaml
+++ b/testdata/project-v3-multigroup/config/rbac/kustomization.yaml
@@ -1,4 +1,10 @@
 resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
 - role.yaml
 - role_binding.yaml
 - leader_election_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/leader_election_role_binding.yaml b/testdata/project-v3-multigroup/config/rbac/leader_election_role_binding.yaml
index eed16906f4d..1d1321ed4f0 100644
--- a/testdata/project-v3-multigroup/config/rbac/leader_election_role_binding.yaml
+++ b/testdata/project-v3-multigroup/config/rbac/leader_election_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: leader-election-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-multigroup/config/rbac/role_binding.yaml b/testdata/project-v3-multigroup/config/rbac/role_binding.yaml
index 8f2658702c8..2070ede4462 100644
--- a/testdata/project-v3-multigroup/config/rbac/role_binding.yaml
+++ b/testdata/project-v3-multigroup/config/rbac/role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: manager-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3-multigroup/config/rbac/service_account.yaml b/testdata/project-v3-multigroup/config/rbac/service_account.yaml
new file mode 100644
index 00000000000..7cd6025bfc4
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/rbac/service_account.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: controller-manager
+  namespace: system
diff --git a/testdata/project-v3-multigroup/controllers/apps/deployment_controller.go b/testdata/project-v3-multigroup/controllers/apps/deployment_controller.go
index a18548c0177..10b6d4e6c2f 100644
--- a/testdata/project-v3-multigroup/controllers/apps/deployment_controller.go
+++ b/testdata/project-v3-multigroup/controllers/apps/deployment_controller.go
@@ -45,7 +45,7 @@ type DeploymentReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("deployment", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/crew/captain_controller.go b/testdata/project-v3-multigroup/controllers/crew/captain_controller.go
index dd374df743e..a10a4c1550b 100644
--- a/testdata/project-v3-multigroup/controllers/crew/captain_controller.go
+++ b/testdata/project-v3-multigroup/controllers/crew/captain_controller.go
@@ -46,7 +46,7 @@ type CaptainReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("captain", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/foo.policy/healthcheckpolicy_controller.go b/testdata/project-v3-multigroup/controllers/foo.policy/healthcheckpolicy_controller.go
index ec94e54bc18..51463f8aa8d 100644
--- a/testdata/project-v3-multigroup/controllers/foo.policy/healthcheckpolicy_controller.go
+++ b/testdata/project-v3-multigroup/controllers/foo.policy/healthcheckpolicy_controller.go
@@ -46,7 +46,7 @@ type HealthCheckPolicyReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *HealthCheckPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("healthcheckpolicy", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/lakers_controller.go b/testdata/project-v3-multigroup/controllers/lakers_controller.go
index c947f2bf0af..5c075c47d91 100644
--- a/testdata/project-v3-multigroup/controllers/lakers_controller.go
+++ b/testdata/project-v3-multigroup/controllers/lakers_controller.go
@@ -46,7 +46,7 @@ type LakersReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *LakersReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("lakers", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/sea-creatures/kraken_controller.go b/testdata/project-v3-multigroup/controllers/sea-creatures/kraken_controller.go
index 2763d74eb57..0737d5515fb 100644
--- a/testdata/project-v3-multigroup/controllers/sea-creatures/kraken_controller.go
+++ b/testdata/project-v3-multigroup/controllers/sea-creatures/kraken_controller.go
@@ -46,7 +46,7 @@ type KrakenReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *KrakenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("kraken", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/sea-creatures/leviathan_controller.go b/testdata/project-v3-multigroup/controllers/sea-creatures/leviathan_controller.go
index 8b99f172461..0f99b078985 100644
--- a/testdata/project-v3-multigroup/controllers/sea-creatures/leviathan_controller.go
+++ b/testdata/project-v3-multigroup/controllers/sea-creatures/leviathan_controller.go
@@ -46,7 +46,7 @@ type LeviathanReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *LeviathanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("leviathan", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/ship/cruiser_controller.go b/testdata/project-v3-multigroup/controllers/ship/cruiser_controller.go
index 458dec378b4..0140467461f 100644
--- a/testdata/project-v3-multigroup/controllers/ship/cruiser_controller.go
+++ b/testdata/project-v3-multigroup/controllers/ship/cruiser_controller.go
@@ -46,7 +46,7 @@ type CruiserReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *CruiserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("cruiser", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/ship/destroyer_controller.go b/testdata/project-v3-multigroup/controllers/ship/destroyer_controller.go
index bf41442f79c..bd29dcd21e3 100644
--- a/testdata/project-v3-multigroup/controllers/ship/destroyer_controller.go
+++ b/testdata/project-v3-multigroup/controllers/ship/destroyer_controller.go
@@ -46,7 +46,7 @@ type DestroyerReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *DestroyerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("destroyer", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/controllers/ship/frigate_controller.go b/testdata/project-v3-multigroup/controllers/ship/frigate_controller.go
index 5900a0b9b49..580724a61ca 100644
--- a/testdata/project-v3-multigroup/controllers/ship/frigate_controller.go
+++ b/testdata/project-v3-multigroup/controllers/ship/frigate_controller.go
@@ -46,7 +46,7 @@ type FrigateReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *FrigateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("frigate", req.NamespacedName)
 
diff --git a/testdata/project-v3-multigroup/go.mod b/testdata/project-v3-multigroup/go.mod
index e5604171902..96c31eb7e3f 100644
--- a/testdata/project-v3-multigroup/go.mod
+++ b/testdata/project-v3-multigroup/go.mod
@@ -4,8 +4,10 @@ go 1.15
 
 require (
 	github.com/go-logr/logr v0.3.0
+	github.com/onsi/ginkgo v1.14.1
+	github.com/onsi/gomega v1.10.2
 	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v0.19.2
-	sigs.k8s.io/controller-runtime v0.7.0
+	sigs.k8s.io/controller-runtime v0.7.2
 )
diff --git a/testdata/project-v3/Makefile b/testdata/project-v3/Makefile
index 16d0b8de6c8..939a04def8d 100644
--- a/testdata/project-v3/Makefile
+++ b/testdata/project-v3/Makefile
@@ -46,7 +46,7 @@ vet: ## Run go vet against code.
 ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
 test: manifests generate fmt vet ## Run tests.
 	mkdir -p ${ENVTEST_ASSETS_DIR}
-	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.0/hack/setup-envtest.sh
+	test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh
 	source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
 
 ##@ Build
diff --git a/testdata/project-v3/PROJECT b/testdata/project-v3/PROJECT
index 8dde5b76ffe..5767a104d67 100644
--- a/testdata/project-v3/PROJECT
+++ b/testdata/project-v3/PROJECT
@@ -1,5 +1,13 @@
 domain: testproject.org
-layout: go.kubebuilder.io/v3
+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/manager/manager.yaml b/testdata/project-v3/config/manager/manager.yaml
index 46a82393bfd..79adfe72a55 100644
--- a/testdata/project-v3/config/manager/manager.yaml
+++ b/testdata/project-v3/config/manager/manager.yaml
@@ -52,4 +52,5 @@ spec:
           requests:
             cpu: 100m
             memory: 20Mi
+      serviceAccountName: controller-manager
       terminationGracePeriodSeconds: 10
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
diff --git a/testdata/project-v3/config/rbac/auth_proxy_role_binding.yaml b/testdata/project-v3/config/rbac/auth_proxy_role_binding.yaml
index 48ed1e4b85c..ec7acc0a1b7 100644
--- a/testdata/project-v3/config/rbac/auth_proxy_role_binding.yaml
+++ b/testdata/project-v3/config/rbac/auth_proxy_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: proxy-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3/config/rbac/kustomization.yaml b/testdata/project-v3/config/rbac/kustomization.yaml
index 66c28338fe0..731832a6ac3 100644
--- a/testdata/project-v3/config/rbac/kustomization.yaml
+++ b/testdata/project-v3/config/rbac/kustomization.yaml
@@ -1,4 +1,10 @@
 resources:
+# All RBAC will be applied under this service account in
+# the deployment namespace. You may comment out this resource
+# if your manager will use a service account that exists at
+# runtime. Be sure to update RoleBinding and ClusterRoleBinding
+# subjects if changing service account names.
+- service_account.yaml
 - role.yaml
 - role_binding.yaml
 - leader_election_role.yaml
diff --git a/testdata/project-v3/config/rbac/leader_election_role_binding.yaml b/testdata/project-v3/config/rbac/leader_election_role_binding.yaml
index eed16906f4d..1d1321ed4f0 100644
--- a/testdata/project-v3/config/rbac/leader_election_role_binding.yaml
+++ b/testdata/project-v3/config/rbac/leader_election_role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: leader-election-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
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/config/rbac/role_binding.yaml b/testdata/project-v3/config/rbac/role_binding.yaml
index 8f2658702c8..2070ede4462 100644
--- a/testdata/project-v3/config/rbac/role_binding.yaml
+++ b/testdata/project-v3/config/rbac/role_binding.yaml
@@ -8,5 +8,5 @@ roleRef:
   name: manager-role
 subjects:
 - kind: ServiceAccount
-  name: default
+  name: controller-manager
   namespace: system
diff --git a/testdata/project-v3/config/rbac/service_account.yaml b/testdata/project-v3/config/rbac/service_account.yaml
new file mode 100644
index 00000000000..7cd6025bfc4
--- /dev/null
+++ b/testdata/project-v3/config/rbac/service_account.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: controller-manager
+  namespace: system
diff --git a/testdata/project-v3/controllers/admiral_controller.go b/testdata/project-v3/controllers/admiral_controller.go
index 61b4456a335..207c44e6ace 100644
--- a/testdata/project-v3/controllers/admiral_controller.go
+++ b/testdata/project-v3/controllers/admiral_controller.go
@@ -46,7 +46,7 @@ type AdmiralReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *AdmiralReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("admiral", req.NamespacedName)
 
diff --git a/testdata/project-v3/controllers/captain_controller.go b/testdata/project-v3/controllers/captain_controller.go
index e0bb6503abc..827aab5052e 100644
--- a/testdata/project-v3/controllers/captain_controller.go
+++ b/testdata/project-v3/controllers/captain_controller.go
@@ -46,7 +46,7 @@ type CaptainReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("captain", req.NamespacedName)
 
diff --git a/testdata/project-v3/controllers/firstmate_controller.go b/testdata/project-v3/controllers/firstmate_controller.go
index 99eeb637700..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.0/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/controllers/laker_controller.go b/testdata/project-v3/controllers/laker_controller.go
index 8cd49b8c169..cae6fdcf051 100644
--- a/testdata/project-v3/controllers/laker_controller.go
+++ b/testdata/project-v3/controllers/laker_controller.go
@@ -44,7 +44,7 @@ type LakerReconciler struct {
 // the user.
 //
 // For more details, check Reconcile and its Result here:
-// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.0/pkg/reconcile
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile
 func (r *LakerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 	_ = r.Log.WithValues("laker", req.NamespacedName)
 
diff --git a/testdata/project-v3/go.mod b/testdata/project-v3/go.mod
index 85b8c51e260..705bc66c458 100644
--- a/testdata/project-v3/go.mod
+++ b/testdata/project-v3/go.mod
@@ -4,7 +4,11 @@ go 1.15
 
 require (
 	github.com/go-logr/logr v0.3.0
+	github.com/onsi/ginkgo v1.14.1
+	github.com/onsi/gomega v1.10.2
+	k8s.io/api v0.19.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v0.19.2
-	sigs.k8s.io/controller-runtime v0.7.0
+	sigs.k8s.io/controller-runtime v0.7.2
+	sigs.k8s.io/kubebuilder-declarative-pattern v0.0.0-20210113160450-b84d99da0217
 )