From a1dbe01f845cbc173c96f04815e4b109a28b82e5 Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Sun, 15 Sep 2024 15:09:40 +0100 Subject: [PATCH] Add support to scaffold controllers for External Types Introduces the option to allow users scaffold controllers for external types by running: kubebuilder create api --group certmanager --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=certmanager.io --- docs/book/src/SUMMARY.md | 2 +- docs/book/src/reference/project-config.md | 48 +-- docs/book/src/reference/reference.md | 2 +- docs/book/src/reference/submodule-layouts.md | 6 +- .../reference/using_an_external_resource.md | 101 +++++++ .../src/reference/using_an_external_type.md | 275 ------------------ pkg/model/resource/resource.go | 8 + pkg/plugins/golang/options.go | 24 +- pkg/plugins/golang/v4/api.go | 31 +- pkg/plugins/golang/v4/scaffolds/api.go | 0 .../controllers/controller_suitetest.go | 1 + .../v4/scaffolds/internal/templates/main.go | 4 +- pkg/rescaffold/migrate.go | 8 + test/e2e/alphagenerate/generate_test.go | 29 ++ test/testdata/generate.sh | 5 + testdata/project-v4-multigroup/PROJECT | 7 + testdata/project-v4-multigroup/cmd/main.go | 11 + .../config/rbac/role.yaml | 26 ++ .../project-v4-multigroup/dist/install.yaml | 26 ++ testdata/project-v4-multigroup/go.mod | 24 +- .../certmanager/certificate_controller.go | 62 ++++ .../certificate_controller_test.go | 32 ++ .../controller/certmanager/suite_test.go | 95 ++++++ testdata/project-v4/PROJECT | 7 + testdata/project-v4/cmd/main.go | 10 + testdata/project-v4/config/rbac/role.yaml | 26 ++ testdata/project-v4/dist/install.yaml | 26 ++ testdata/project-v4/go.mod | 24 +- .../controller/certificate_controller.go | 62 ++++ .../controller/certificate_controller_test.go | 32 ++ .../internal/controller/suite_test.go | 5 + 31 files changed, 686 insertions(+), 333 deletions(-) create mode 100644 docs/book/src/reference/using_an_external_resource.md delete mode 100644 docs/book/src/reference/using_an_external_type.md mode change 100755 => 100644 pkg/plugins/golang/v4/scaffolds/api.go create mode 100644 testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller.go create mode 100644 testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller_test.go create mode 100644 testdata/project-v4-multigroup/internal/controller/certmanager/suite_test.go create mode 100644 testdata/project-v4/internal/controller/certificate_controller.go create mode 100644 testdata/project-v4/internal/controller/certificate_controller_test.go diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 582f7f1b444..22a89db7bd0 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -102,7 +102,7 @@ - [Manager and CRDs Scope](./reference/scopes.md) - [Sub-Module Layouts](./reference/submodule-layouts.md) - - [Using an external Type / API](./reference/using_an_external_type.md) + - [Using an external Resource / API](./reference/using_an_external_resource.md) - [Configuring EnvTest](./reference/envtest.md) diff --git a/docs/book/src/reference/project-config.md b/docs/book/src/reference/project-config.md index 07105348d62..2b7be3bafe8 100644 --- a/docs/book/src/reference/project-config.md +++ b/docs/book/src/reference/project-config.md @@ -130,28 +130,29 @@ version: "3" Now let's check its layout fields definition: -| Field | Description | -|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `layout` | Defines the global plugins, e.g. a project `init` with `--plugins="go/v4,deploy-image/v1-alpha"` means that any sub-command used will always call its implementation for both plugins in a chain. | -| `domain` | Store the domain of the project. This information can be provided by the user when the project is generate with the `init` sub-command and the `domain` flag. | -| `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. | -| `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. | -| `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. | -| `resources` | An array of all resources which were scaffolded in the project. | -| `resources.api` | The API scaffolded in the project via the sub-command `create api`. | -| `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. | -| `resources.api.namespaced` | The API RBAC permissions which can be namespaced or cluster scoped. | -| `resources.controller` | Indicates whether a controller was scaffolded for the API. | -| `resources.domain` | The domain of the resource which is provided by the `--domain` flag when the sub-command `create api` is used. | -| `resources.group` | The GKV group of the resource which is provided by the `--group` flag when the sub-command `create api` is used. | -| `resources.version` | The GKV version of the resource which is provided by the `--version` flag when the sub-command `create api` is used. | -| `resources.kind` | Store GKV Kind of the resource which is provided by the `--kind` flag when the sub-command `create api` is used. | -| `resources.path` | The import path for the API resource. It will be `/api/` unless the API added to the project is an external or core-type. For the core-types scenarios, the paths used are mapped [here][core-types]. | -| `resources.webhooks`| Store the webhooks data when the sub-command `create webhook` is used. | -| `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | -| `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | -| `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | -| `resources.webhooks.validation` | It is `true` when the webhook was scaffold with the `--programmatic-validation` flag which means that is a validation webhook. | +| Field | Description | +|-------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `layout` | Defines the global plugins, e.g. a project `init` with `--plugins="go/v4,deploy-image/v1-alpha"` means that any sub-command used will always call its implementation for both plugins in a chain. | +| `domain` | Store the domain of the project. This information can be provided by the user when the project is generate with the `init` sub-command and the `domain` flag. | +| `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. | +| `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. | +| `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. | +| `resources` | An array of all resources which were scaffolded in the project. | +| `resources.api` | The API scaffolded in the project via the sub-command `create api`. | +| `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. | +| `resources.api.namespaced` | The API RBAC permissions which can be namespaced or cluster scoped. | +| `resources.controller` | Indicates whether a controller was scaffolded for the API. | +| `resources.domain` | The domain of the resource which was provided by the `--domain` flag when the project was initialized or via the flag `--external-api-domain` when it was used to scaffold controllers for an [External Type][external-type]. | +| `resources.group` | The GKV group of the resource which is provided by the `--group` flag when the sub-command `create api` is used. | +| `resources.version` | The GKV version of the resource which is provided by the `--version` flag when the sub-command `create api` is used. | +| `resources.kind` | Store GKV Kind of the resource which is provided by the `--kind` flag when the sub-command `create api` is used. | +| `resources.path` | The import path for the API resource. It will be `/api/` unless the API added to the project is an external or core-type. For the core-types scenarios, the paths used are mapped [here][core-types]. Or either the path informed by the flag `--external-api-path` | +| `resources.external` | It is `true` when the flag `--external-api-path` was used to generated the scaffold for an [External Type][external-type]. | +| `resources.webhooks` | Store the webhooks data when the sub-command `create webhook` is used. | +| `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | +| `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | +| `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | +| `resources.webhooks.validation` | It is `true` when the webhook was scaffold with the `--programmatic-validation` flag which means that is a validation webhook. | [project]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v3/PROJECT [versioning]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#Versioning @@ -160,4 +161,5 @@ Now let's check its layout fields definition: [olm]: https://olm.operatorframework.io/ [plugins-doc]: ../plugins/creating-plugins.html#why-use-the-kubebuilder-style [doc-design-helper]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/helper_to_upgrade_projects_by_rescaffolding.md -[operator-sdk]: https://sdk.operatorframework.io/ \ No newline at end of file +[operator-sdk]: https://sdk.operatorframework.io/ +[external-type]: ./using_an_external_resource.md \ No newline at end of file diff --git a/docs/book/src/reference/reference.md b/docs/book/src/reference/reference.md index 6a40cafa4bd..5d4f02a0dbe 100644 --- a/docs/book/src/reference/reference.md +++ b/docs/book/src/reference/reference.md @@ -35,7 +35,7 @@ - [Platform Support](platform.md) - [Sub-Module Layouts](submodule-layouts.md) - - [Using an external Type / API](using_an_external_type.md) + - [Using an external Resource / API](using_an_external_resource.md) - [Metrics](metrics.md) - [Reference](metrics-reference.md) diff --git a/docs/book/src/reference/submodule-layouts.md b/docs/book/src/reference/submodule-layouts.md index 2d44e9f8528..63a069311bd 100644 --- a/docs/book/src/reference/submodule-layouts.md +++ b/docs/book/src/reference/submodule-layouts.md @@ -5,9 +5,11 @@ This part describes how to modify a scaffolded project for use with multiple `go Sub-Module Layouts (in a way you could call them a special form of [Monorepo's][monorepo]) are a special use case and can help in scenarios that involve reuse of APIs without introducing indirect dependencies that should not be available in the project consuming the API externally. diff --git a/docs/book/src/reference/using_an_external_resource.md b/docs/book/src/reference/using_an_external_resource.md new file mode 100644 index 00000000000..7259f8414e5 --- /dev/null +++ b/docs/book/src/reference/using_an_external_resource.md @@ -0,0 +1,101 @@ + +# Using External Resources + +In some cases, your project may need to work with resources that aren't defined by your own APIs. These external resources fall into two main categories: + +- **Core Types**: API types defined by Kubernetes itself, such as `Pods`, `Services`, and `Deployments`. +- **External Types**: API types defined in other projects, such as CRDs defined by another solution. + +## Managing External Types + +### Creating a Controller for External Types + +To create a controller for an external type without scaffolding a resource, use the `create api` command with the `--resource=false` option and specify the path to the external API type using the `--external-api-path` option. This generates a controller for types defined outside your project, such as CRDs managed by other operators. + +The command looks like this: + +```shell +kubebuilder create api --group --version v1alpha1 --kind --controller --resource=false --external-api-path= +``` + +- `--external-api-path`: Provide the Go import path where the external types are defined. + +For example, if you're managing Certificates from Cert Manager: + +```shell +kubebuilder create api --group certmanager --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 +``` + +This scaffolds a controller for the external type but skips creating new resource definitions since the type is defined in an external project. + +### Creating a Webhook to Manage an External Type + + + +## Managing Core Types + +Core Kubernetes API types, such as `Pods`, `Services`, and `Deployments`, are predefined by Kubernetes. +To create a controller for these core types without scaffolding the resource, +use the Kubernetes group name described in the following +table and specify the version and kind. + +| Group | K8s API Group | +|---------------------------|------------------------------------| +| admission | k8s.io/admission | +| admissionregistration | k8s.io/admissionregistration | +| apps | apps | +| auditregistration | k8s.io/auditregistration | +| apiextensions | k8s.io/apiextensions | +| authentication | k8s.io/authentication | +| authorization | k8s.io/authorization | +| autoscaling | autoscaling | +| batch | batch | +| certificates | k8s.io/certificates | +| coordination | k8s.io/coordination | +| core | core | +| events | k8s.io/events | +| extensions | extensions | +| imagepolicy | k8s.io/imagepolicy | +| networking | k8s.io/networking | +| node | k8s.io/node | +| metrics | k8s.io/metrics | +| policy | policy | +| rbac.authorization | k8s.io/rbac.authorization | +| scheduling | k8s.io/scheduling | +| setting | k8s.io/setting | +| storage | k8s.io/storage | + +The command to create a controller to manage `Pods` looks like this: + +```shell +kubebuilder create api --group core --version v1 --kind Pod --controller=true --resource=false +``` + +This scaffolds a controller for the Core type `corev1.Pod` but skips creating new resource +definitions since the type is already defined in the Kubernetes API. + + + + +### Creating a Webhook to Manage a Core Type + + + +[webhook-for-core-types]: ./webhook-for-core-types.md diff --git a/docs/book/src/reference/using_an_external_type.md b/docs/book/src/reference/using_an_external_type.md deleted file mode 100644 index 22a31a7d9ab..00000000000 --- a/docs/book/src/reference/using_an_external_type.md +++ /dev/null @@ -1,275 +0,0 @@ -# Using an External Type - -There are several different external types that may be referenced when writing a controller. -* Custom Resource Definitions (CRDs) that are defined in the current project (such as via `kubebuilder create api`). -* Core Kubernetes Resources (e.g. Deployments or Pods). -* CRDs that are created and installed in another project. -* A custom API defined via the aggregation layer, served by an extension API server for which the primary API server acts as a proxy. - -Currently, kubebuilder handles the first two, CRDs and Core Resources, seamlessly. You must scaffold the latter two, External CRDs and APIs created via aggregation, manually. - -In order to use a Kubernetes Custom Resource that has been defined in another project -you will need to have several items of information. -* The Domain of the CR -* The Group under the Domain -* The Go import path of the CR Type definition -* The Custom Resource Type you want to depend on. - -The Domain and Group variables have been discussed in other parts of the documentation. The import path would be located in the project that installs the CR. -The Custom Resource Type is usually a Go Type of the same name as the CustomResourceDefinition in kubernetes, e.g. for a `Pod` there will be a type `Pod` in the `v1` group. -For Kubernetes Core Types, the domain can be omitted. -`` -This document uses `my` and `their` prefixes as a naming convention for repos, groups, and types to clearly distinguish between your own project and the external one you are referencing. - -In our example we will assume the following external API Type: - -`github.com/theiruser/theirproject` is another kubebuilder project on whose CRD we want to depend and extend on. -Thus, it contains a `go.mod` in its repository root. The import path for the go types would be `github.com/theiruser/theirproject/apis/theirgroup/v1alpha1`. - -The Domain of the CR is `theirs.com`, the Group is `theirgroup` and the kind and go type would be `ExternalType`. - -If there is an interest to have multiple Controllers running in different Groups (e.g. because one is an owned CRD and one is an external Type), please first -reconfigure the Project to use a multi-group layout as described in the [Multi-Group documentation](../migration/multi-group.md). - -### Prerequisites - -The following guide assumes that you have already created a project using `kubebuilder init` in a directory in the GOPATH. Please reference the [Getting Started Guide](../getting-started.md) for more information. - -Note that if you did not pass `--domain` to `kubebuilder init` you will need to modify it for the individual api types as the default is `my.domain`, not `theirs.com`. -Similarly, if you intend to use your own domain, please configure your own domain with `kubebuilder init` and do not use `theirs.com for the domain. - -### Add a controller for the external Type - -Run the command `create api` to scaffold only the controller to manage the external type: - -```shell -kubebuilder create api --group --version v1alpha1 --kind --controller --resource=false -``` - -Note that the `resource` argument is set to false, as we are not attempting to create our own CustomResourceDefinition, -but instead rely on an external one. - -This will result in a `PROJECT` entry with the default domain of the `PROJECT` (`my.domain` if not specified in `kubebuilder init`). -For use of other domains, such as `theirs.com`, one will have to manually adjust the `PROJECT` file with the correct domain for the entry: - - - -file: PROJECT -``` -domain: my.domain -layout: -- go.kubebuilder.io/v4 -projectName: testkube -repo: example.com -resources: -- controller: true - domain: my.domain ## <- Replace the domain with theirs.com domain - group: mygroup - kind: ExternalType - version: v1alpha1 -version: "3" -``` - -At the same time, the generated RBAC manifests need to be adjusted: - -file: internal/controller/externaltype_controller.go -```go -// ExternalTypeReconciler reconciles a ExternalType object -type ExternalTypeReconciler struct { - client.Client - Scheme *runtime.Scheme -} - -// external types can be added like this -// +kubebuilder:rbac:groups=theirgroup.theirs.com,resources=externaltypes,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=theirgroup.theirs.com,resources=externaltypes/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=theirgroup.theirs.com,resources=externaltypes/finalizers,verbs=update -// core types can be added like this -// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=core,resources=pods/status,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update -``` - -### Register your Types - - - -Edit the following lines to the main.go file to register the external types: - -file: cmd/main.go -```go -package apis - -import ( - theirgroupv1alpha1 "github.com/theiruser/theirproject/apis/theirgroup/v1alpha1" -) - -func init() { - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(theirgroupv1alpha1.AddToScheme(scheme)) // this contains the external API types - // +kubebuilder:scaffold:scheme -} -``` - -## Edit the Controller `SetupWithManager` function - -### Use the correct imports for your API and uncomment the controlled resource - -file: internal/controllers/externaltype_controller.go -```go -package controllers - -import ( - theirgroupv1alpha1 "github.com/theiruser/theirproject/apis/theirgroup/v1alpha1" -) - -//... - -// SetupWithManager sets up the controller with the Manager. -func (r *ExternalTypeReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&theirgroupv1alpha1.ExternalType{}). - Complete(r) -} - -``` - -Note that core resources may simply be imported by depending on the API's from upstream Kubernetes and do not need additional `AddToScheme` registrations: - -file: internal/controllers/externaltype_controller.go -```go -package controllers -// contains core resources like Deployment -import ( - v1 "k8s.io/api/apps/v1" -) - - -// SetupWithManager sets up the controller with the Manager. -func (r *ExternalTypeReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&v1.Pod{}). - Complete(r) -} -``` - -### Update dependencies - -``` -go mod tidy -``` - -### Generate RBACs with updated Groups and Resources - -``` -make manifests -``` - -## Prepare for testing - -### Register your resource in the Scheme - -Edit the `CRDDirectoryPaths` in your test suite and add the correct `AddToScheme` entry during suite initialization: - -file: internal/controllers/suite_test.go -```go -package controller - -import ( - "fmt" - "path/filepath" - "runtime" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - // +kubebuilder:scaffold:imports - theirgroupv1alpha1 "github.com/theiruser/theirproject/apis/theirgroup/v1alpha1" -) - -var cfg *rest.Config -var k8sClient client.Client -var testEnv *envtest.Environment - -func TestControllers(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Controller Suite") -} - - -var _ = BeforeSuite(func() { - //... - By("bootstrapping test environment") - testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{ - // if you are using vendoring and rely on a kubebuilder based project, you can simply rely on the vendored config directory - filepath.Join("..", "..", "..", "vendor", "github.com", "theiruser", "theirproject", "config", "crds"), - // otherwise you can simply download the CRD from any source and place it within the config/crd/bases directory, - filepath.Join("..", "..", "config", "crd", "bases"), - }, - ErrorIfCRDPathMissing: false, - - // The BinaryAssetsDirectory is only required if you want to run the tests directly - // without call the makefile target test. If not informed it will look for the - // default path defined in controller-runtime which is /usr/local/kubebuilder/. - // Note that you must have the required binaries setup under the bin directory to perform - // the tests directly. When we run make test it will be setup and used automatically. - BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", - fmt.Sprintf("1.28.3-%s-%s", runtime.GOOS, runtime.GOARCH)), - } - - var err error - // cfg is defined in this file globally. - cfg, err = testEnv.Start() - Expect(err).NotTo(HaveOccurred()) - Expect(cfg).NotTo(BeNil()) - - // +kubebuilder:scaffold:scheme - Expect(theirgroupv1alpha1.AddToScheme(scheme.Scheme)).To(Succeed()) - - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).NotTo(HaveOccurred()) - Expect(k8sClient).NotTo(BeNil()) - - -}) - -``` - -### Verifying API Availability in the Cluster - -Since we are now using external types, you will now have to rely on them being installed into the cluster. -If the APIs are not available at the time the manager starts, all informers listening to the non-available types -will fail, causing the manager to exit with an error similar to - -``` -failed to get informer from cache {"error": "Timeout: failed waiting for *v1alpha1.ExternalType Informer to sync"} -``` - -This will signal that the API Server is not yet ready to serve the external types. - -## Helpful Tips - -### Locate your domain and group variables - -The following kubectl commands may be useful - -```shell -kubectl api-resources --verbs=list -o name -kubectl api-resources --verbs=list -o name | grep my.domain -``` - diff --git a/pkg/model/resource/resource.go b/pkg/model/resource/resource.go index 7d4a9c40929..c455c1f4b57 100644 --- a/pkg/model/resource/resource.go +++ b/pkg/model/resource/resource.go @@ -42,6 +42,9 @@ type Resource struct { // Webhooks holds the information related to the associated webhooks. Webhooks *Webhooks `json:"webhooks,omitempty"` + + // External specifies if the resource is defined externally. + External bool `json:"external,omitempty"` } // Validate checks that the Resource is valid. @@ -119,6 +122,11 @@ func (r Resource) HasConversionWebhook() bool { return r.Webhooks != nil && r.Webhooks.Conversion } +// IsExternal returns true if the resource was scaffold as external. +func (r Resource) IsExternal() bool { + return r.External +} + // IsRegularPlural returns true if the plural is the regular plural form for the kind. func (r Resource) IsRegularPlural() bool { return r.Plural == RegularPlural(r.Kind) diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index 865f6137949..e91b57260df 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -56,6 +56,13 @@ type Options struct { // Plural is the resource's kind plural form. Plural string + // ExternalAPIPath allows to inform a path for APIs not defined in the project + ExternalAPIPath string + + // ExternalAPIPath allows to inform the resource domain to build the Qualified Group + // to generate the RBAC markers + ExternalAPIDomain string + // Namespaced is true if the resource should be namespaced. Namespaced bool @@ -104,20 +111,29 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { } } + if len(opts.ExternalAPIPath) > 0 { + res.External = true + } + // 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 loadedRes, err := c.GetResource(res.GVK) alreadyHasAPI = err == nil && loadedRes.HasAPI() if !alreadyHasAPI { - if domain, found := coreGroups[res.Group]; found { - res.Domain = domain - res.Path = path.Join("k8s.io", "api", res.Group, res.Version) + if res.External { + res.Path = opts.ExternalAPIPath + res.Domain = opts.ExternalAPIDomain + } else { + // Handle core types + if domain, found := coreGroups[res.Group]; found { + res.Domain = domain + res.Path = path.Join("k8s.io", "api", res.Group, res.Version) + } } } } diff --git a/pkg/plugins/golang/v4/api.go b/pkg/plugins/golang/v4/api.go index 3f0f27958d5..592b48281f2 100644 --- a/pkg/plugins/golang/v4/api.go +++ b/pkg/plugins/golang/v4/api.go @@ -108,6 +108,15 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.options.DoController, "controller", true, "if set, generate the controller without prompting the user") p.controllerFlag = fs.Lookup("controller") + + fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "", + "Specify the Go package import path for the external API. This is used to scaffold controllers for resources "+ + "defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).") + + fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "", + "Specify the domain name for the external API. This domain is used to generate accurate RBAC "+ + "markers and permissions for the external resources (e.g., cert-manager.io).") + } func (p *createAPISubcommand) InjectConfig(c config.Config) error { @@ -118,9 +127,6 @@ func (p *createAPISubcommand) InjectConfig(c config.Config) 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. - // Ask for API and Controller if not specified reader := bufio.NewReader(os.Stdin) if !p.resourceFlag.Changed { log.Println("Create Resource [y/n]") @@ -131,6 +137,25 @@ func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { p.options.DoController = util.YesNo(reader) } + // Ensure that external API options cannot be used when creating an API in the project. + if p.options.DoAPI { + if len(p.options.ExternalAPIPath) != 0 || len(p.options.ExternalAPIDomain) != 0 { + return errors.New("Cannot use '--external-api-path' or '--external-api-domain' " + + "when creating an API in the project with '--resource=true'. " + + "Use '--resource=false' when referencing an external API.") + } + } + + // Ensure that if any external API flag is set, both must be provided. + if len(p.options.ExternalAPIPath) != 0 || len(p.options.ExternalAPIDomain) != 0 { + if len(p.options.ExternalAPIPath) == 0 || len(p.options.ExternalAPIDomain) == 0 { + return errors.New("Both '--external-api-path' and '--external-api-domain' must be " + + "specified together when referencing an external API.") + } + } + + // add check to ensure that if resource is true externapi and exytermal api domain cannot be set + // if one of those is filled then both should be informed p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { diff --git a/pkg/plugins/golang/v4/scaffolds/api.go b/pkg/plugins/golang/v4/scaffolds/api.go old mode 100755 new mode 100644 diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go index e2f8796bc5f..b28bd11a326 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go @@ -136,6 +136,7 @@ package controller {{end}} import ( + "context" "fmt" "path/filepath" "runtime" diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go index 031a83e9795..8bf26f575ec 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/main.go @@ -135,7 +135,7 @@ func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { // Generate import code fragments imports := make([]string, 0) - if f.WireResource { + if f.WireResource || f.Resource.IsExternal() { imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path)) } @@ -150,7 +150,7 @@ func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap { // Generate add scheme code fragments addScheme := make([]string, 0) - if f.WireResource { + if f.WireResource || f.Resource.IsExternal() { addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias())) } diff --git a/pkg/rescaffold/migrate.go b/pkg/rescaffold/migrate.go index 410a3f4db8f..bde0bd78b4d 100644 --- a/pkg/rescaffold/migrate.go +++ b/pkg/rescaffold/migrate.go @@ -275,11 +275,19 @@ func createAPI(resource resource.Resource) error { args = append(args, "api") args = append(args, getGVKFlags(resource)...) args = append(args, getAPIResourceFlags(resource)...) + + // Add the external API path flag if the resource is external + if resource.IsExternal() { + args = append(args, "--external-api-path", resource.Path) + args = append(args, "--external-api-domain", resource.Domain) + } + return util.RunCmd("kubebuilder create api", "kubebuilder", args...) } func getAPIResourceFlags(resource resource.Resource) []string { var args []string + if resource.API == nil || resource.API.IsEmpty() { // create API without creating resource args = append(args, "--resource=false") diff --git a/test/e2e/alphagenerate/generate_test.go b/test/e2e/alphagenerate/generate_test.go index ae2c81d8098..45353c8d77b 100644 --- a/test/e2e/alphagenerate/generate_test.go +++ b/test/e2e/alphagenerate/generate_test.go @@ -152,6 +152,18 @@ func generateProject(kbc *utils.TestContext) { ) Expect(err).NotTo(HaveOccurred(), "Failed to scaffold API with namespaced true") + By("creating an external API with cert-manager") + err = kbc.CreateAPI( + "--group", "certmanager", + "--version", "v1", + "--kind", "Certificate", + "--controller=true", + "--resource=false", + "--make=false", + "--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1", + "--external-api-domain=cert-manager.io", + ) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) } func regenerateProject(kbc *utils.TestContext, projectOutputDir string) { @@ -247,6 +259,23 @@ func validateProjectFile(kbc *utils.TestContext, projectFile string) { Expect(captainResource.Controller).To(BeTrue(), "Captain API should have a controller") Expect(captainResource.API.Namespaced).To(BeTrue(), "Captain API should be namespaced") Expect(captainResource.Webhooks).To(BeNil(), "Capitan API should not have webhooks") + + By("validating the External API with kind Certificate from certManager") + certmanagerGVK := resource.GVK{ + Group: "certmanager", + Domain: "cert-manager.io", + Version: "v1", + Kind: "Certificate", + } + Expect(projectConfig.HasResource(certmanagerGVK)).To(BeTrue(), + "Certificate Resource should be present in the PROJECT file") + certmanagerResource, err := projectConfig.GetResource(certmanagerGVK) + Expect(err).NotTo(HaveOccurred(), "Captain API should be retrievable") + Expect(certmanagerResource.Controller).To(BeTrue(), "Certificate API should have a controller") + Expect(certmanagerResource.API).To(BeNil(), "Certificate API should not have API scaffold") + Expect(certmanagerResource.Webhooks).To(BeNil(), "Certificate API should not have webhooks") + Expect(certmanagerResource.Path).To(Equal("github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"), + "Certificate API should have expected path") } func getConfigFromProjectFile(projectFilePath string) config.Config { diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index bc16751b992..b9888de408d 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -45,6 +45,8 @@ function scaffold_test_project { $kb create webhook --group crew --version v1 --kind FirstMate --conversion $kb create api --group crew --version v1 --kind Admiral --plural=admirales --controller=true --resource=true --namespaced=false --make=false $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting + # Controller for External types + $kb create api --group certmanager --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io fi if [[ $project =~ multigroup ]]; then @@ -65,9 +67,12 @@ function scaffold_test_project { $kb create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true --make=false $kb create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true --make=false $kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false + # Controller for Core types $kb create api --group apps --version v1 --kind Deployment --controller=true --resource=false --make=false $kb create api --group foo --version v1 --kind Bar --controller=true --resource=true --make=false $kb create api --group fiz --version v1 --kind Bar --controller=true --resource=true --make=false + # Controller for External types + $kb create api --group certmanager --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io fi if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index ea5536b0583..8d854bab7a5 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -125,6 +125,13 @@ resources: kind: Bar path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1 version: v1 +- controller: true + domain: cert-manager.io + external: true + group: certmanager + kind: Certificate + path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 + version: v1 - api: crdVersion: v1 namespaced: true diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index e7dc7e0a55e..b42a857df1f 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -35,6 +35,8 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1" examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1" fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1" @@ -46,6 +48,7 @@ import ( shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1" shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1" appscontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/apps" + certmanagercontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/certmanager" crewcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/crew" examplecomcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/example.com" fizcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/fiz" @@ -73,6 +76,7 @@ func init() { utilruntime.Must(foopolicyv1.AddToScheme(scheme)) utilruntime.Must(foov1.AddToScheme(scheme)) utilruntime.Must(fizv1.AddToScheme(scheme)) + utilruntime.Must(certmanagerv1.AddToScheme(scheme)) utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -267,6 +271,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Bar") os.Exit(1) } + if err = (&certmanagercontroller.CertificateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Certificate") + os.Exit(1) + } if err = (&examplecomcontroller.MemcachedReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/testdata/project-v4-multigroup/config/rbac/role.yaml b/testdata/project-v4-multigroup/config/rbac/role.yaml index 3bf7c0bed2f..598a1c592a7 100644 --- a/testdata/project-v4-multigroup/config/rbac/role.yaml +++ b/testdata/project-v4-multigroup/config/rbac/role.yaml @@ -30,6 +30,32 @@ rules: - get - patch - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/finalizers + verbs: + - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/status + verbs: + - get + - patch + - update - apiGroups: - "" resources: diff --git a/testdata/project-v4-multigroup/dist/install.yaml b/testdata/project-v4-multigroup/dist/install.yaml index 05a31522eeb..d755f2547d2 100644 --- a/testdata/project-v4-multigroup/dist/install.yaml +++ b/testdata/project-v4-multigroup/dist/install.yaml @@ -1161,6 +1161,32 @@ rules: - get - patch - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/finalizers + verbs: + - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/status + verbs: + - get + - patch + - update - apiGroups: - "" resources: diff --git a/testdata/project-v4-multigroup/go.mod b/testdata/project-v4-multigroup/go.mod index 03db01b03e8..1fdc0ee0701 100644 --- a/testdata/project-v4-multigroup/go.mod +++ b/testdata/project-v4-multigroup/go.mod @@ -3,6 +3,7 @@ module sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup go 1.22.0 require ( + github.com/cert-manager/cert-manager v1.15.3 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 k8s.io/api v0.31.0 @@ -13,13 +14,13 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -27,9 +28,9 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -41,7 +42,7 @@ require ( github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -56,7 +57,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect @@ -67,15 +68,15 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect @@ -89,9 +90,10 @@ require ( k8s.io/apiserver v0.31.0 // indirect k8s.io/component-base v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect + sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller.go b/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller.go new file mode 100644 index 00000000000..71fe01ee254 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 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 certmanager + +import ( + "context" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// CertificateReconciler reconciles a Certificate object +type CertificateReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates/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 Certificate 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.19.0/pkg/reconcile +func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CertificateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&certmanagerv1.Certificate{}). + Named("certmanager-certificate"). + Complete(r) +} diff --git a/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller_test.go b/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller_test.go new file mode 100644 index 00000000000..3959874773b --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/certmanager/certificate_controller_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 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 certmanager + +import ( + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("Certificate Controller", func() { + Context("When reconciling a resource", func() { + + It("should successfully reconcile the resource", func() { + + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/testdata/project-v4-multigroup/internal/controller/certmanager/suite_test.go b/testdata/project-v4-multigroup/internal/controller/certmanager/suite_test.go new file mode 100644 index 00000000000..8a55c1d28e9 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/certmanager/suite_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2024 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 certmanager + +import ( + "context" + "fmt" + "path/filepath" + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + // The BinaryAssetsDirectory is only required if you want to run the tests directly + // without call the makefile target test. If not informed it will look for the + // default path defined in controller-runtime which is /usr/local/kubebuilder/. + // Note that you must have the required binaries setup under the bin directory to perform + // the tests directly. When we run make test it will be setup and used automatically. + BinaryAssetsDirectory: filepath.Join("..", "..", "..", "bin", "k8s", + fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = certmanagerv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/testdata/project-v4/PROJECT b/testdata/project-v4/PROJECT index 91fdfce8a83..e65c3db0df4 100644 --- a/testdata/project-v4/PROJECT +++ b/testdata/project-v4/PROJECT @@ -45,4 +45,11 @@ resources: webhooks: defaulting: true webhookVersion: v1 +- controller: true + domain: cert-manager.io + external: true + group: certmanager + kind: Certificate + path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 + version: v1 version: "3" diff --git a/testdata/project-v4/cmd/main.go b/testdata/project-v4/cmd/main.go index d2a65954c40..4e07a81bc63 100644 --- a/testdata/project-v4/cmd/main.go +++ b/testdata/project-v4/cmd/main.go @@ -35,6 +35,8 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/controller" // +kubebuilder:scaffold:imports @@ -49,6 +51,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(crewv1.AddToScheme(scheme)) + utilruntime.Must(certmanagerv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -186,6 +189,13 @@ func main() { os.Exit(1) } } + if err = (&controller.CertificateReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Certificate") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/testdata/project-v4/config/rbac/role.yaml b/testdata/project-v4/config/rbac/role.yaml index f9d77efa7db..b7de1698cfb 100644 --- a/testdata/project-v4/config/rbac/role.yaml +++ b/testdata/project-v4/config/rbac/role.yaml @@ -4,6 +4,32 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/finalizers + verbs: + - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/status + verbs: + - get + - patch + - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v4/dist/install.yaml b/testdata/project-v4/dist/install.yaml index c052674a787..bc03981dc80 100644 --- a/testdata/project-v4/dist/install.yaml +++ b/testdata/project-v4/dist/install.yaml @@ -404,6 +404,32 @@ kind: ClusterRole metadata: name: project-v4-manager-role rules: +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/finalizers + verbs: + - update +- apiGroups: + - certmanager.cert-manager.io + resources: + - certificates/status + verbs: + - get + - patch + - update - apiGroups: - crew.testproject.org resources: diff --git a/testdata/project-v4/go.mod b/testdata/project-v4/go.mod index 09810a66577..0b093aacf12 100644 --- a/testdata/project-v4/go.mod +++ b/testdata/project-v4/go.mod @@ -3,6 +3,7 @@ module sigs.k8s.io/kubebuilder/testdata/project-v4 go 1.22.0 require ( + github.com/cert-manager/cert-manager v1.15.3 github.com/onsi/ginkgo/v2 v2.19.0 github.com/onsi/gomega v1.33.1 k8s.io/api v0.31.0 @@ -13,13 +14,13 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect @@ -27,9 +28,9 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -41,7 +42,7 @@ require ( github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -56,7 +57,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect @@ -67,15 +68,15 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect @@ -89,9 +90,10 @@ require ( k8s.io/apiserver v0.31.0 // indirect k8s.io/component-base v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect + sigs.k8s.io/gateway-api v1.1.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/testdata/project-v4/internal/controller/certificate_controller.go b/testdata/project-v4/internal/controller/certificate_controller.go new file mode 100644 index 00000000000..13e02a06098 --- /dev/null +++ b/testdata/project-v4/internal/controller/certificate_controller.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 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 controller + +import ( + "context" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// CertificateReconciler reconciles a Certificate object +type CertificateReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=certmanager.cert-manager.io,resources=certificates/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 Certificate 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.19.0/pkg/reconcile +func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CertificateReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&certmanagerv1.Certificate{}). + Named("certificate"). + Complete(r) +} diff --git a/testdata/project-v4/internal/controller/certificate_controller_test.go b/testdata/project-v4/internal/controller/certificate_controller_test.go new file mode 100644 index 00000000000..258f99dc795 --- /dev/null +++ b/testdata/project-v4/internal/controller/certificate_controller_test.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 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 controller + +import ( + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("Certificate Controller", func() { + Context("When reconciling a resource", func() { + + It("should successfully reconcile the resource", func() { + + // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. + // Example: If you expect a certain status condition after reconciliation, verify it here. + }) + }) +}) diff --git a/testdata/project-v4/internal/controller/suite_test.go b/testdata/project-v4/internal/controller/suite_test.go index a0cd2217f5a..1bb1c72bada 100644 --- a/testdata/project-v4/internal/controller/suite_test.go +++ b/testdata/project-v4/internal/controller/suite_test.go @@ -33,6 +33,8 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" // +kubebuilder:scaffold:imports ) @@ -80,6 +82,9 @@ var _ = BeforeSuite(func() { err = crewv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = certmanagerv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})