Repository: kubernetes-sigs/kubebuilder (language: go)

Issue:
Kubebuilder with multi-group and same Kind names doesn't generate proper crd/patch and rbac manifests
### What broke? What's expected?

In a multi-group project when 2 groups have the same CRD name for example

```
---
apiVersion: foo.rmb938.com/v1alpha1
kind: Bar
...
---
apiVersion: faz.rmb938.com/v1alpha1
kind: Bar
```

The crd patches generated in `config/crd/patches` and the rbac generated (for viewer and editor) in `config/rbac` are only for the first group and kind that gets generated. They should be generated for all groups.

### Reproducing this issue

Run the following

```
kubebuilder create api --controller=false --group foo --version v1alpha1 --kind Bar
kubebuilder create api --controller=false --group faz --version v1alpha1 --kind Bar
```

### KubeBuilder (CLI) Version

Version: main.version{KubeBuilderVersion:"3.3.0", KubernetesVendor:"1.23.1", GitCommit:"47859bf2ebf96a64db69a2f7074ffdec7f15c1ec", BuildDate:"2022-01-18T17:03:29Z", GoOs:"darwin", GoArch:"amd64"}

### PROJECT version

3

### Plugin versions

```yaml
layout:
- go.kubebuilder.io/v3
```


### Other versions

```
 %  go version
go version go1.17.3 darwin/amd64
```

```
sigs.k8s.io/controller-runtime v0.11.0
```

```
sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0
```

```
%  kubectl version
Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T21:51:21Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.5-gke.1302", GitCommit:"639f3a74abf258418493e9b75f2f98a08da29733", GitTreeState:"clean", BuildDate:"2021-10-21T21:35:48Z", GoVersion:"go1.16.7b7", Compiler:"gc", Platform:"linux/amd64"}
```

```
%  kustomize version
{Version:kustomize/v4.4.0 GitCommit:63ec6bdb3d737a7c66901828c5743656c49b60e1 BuildDate:2021-09-27T16:13:36Z GoOs:darwin GoArch:amd64}
```

### Extra Labels

_No response_


Discussion / hints:
Hi @rmb938, 

Could you please provide some additional info?

1) Are the multi-group support enabled in your project? see: https://book.kubebuilder.io/migration/multi-group.html
2) Could you please copy and paste here the PROJECT file content? 

See that we have sample projects generated at [testdata/](https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata) directory and I could not find the issue raised by you in the project which has multi-group support (see, [kubebuilder/testdata/project-v3-multigroup](https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v3-multigroup)).


> The crd patches generated in config/crd/patches and the rbac generated (for viewer and editor) in config/rbac are only for the first group and kind that gets generated. They should be generated for all groups.

Note the config/crd/patches generated for all groups:

- https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v3-multigroup/config/crd/kustomization.yaml
- https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v3-multigroup/config/crd/patches

Also, see the rbac generated for all: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v3-multigroup/config/rbac

If you want see how the sample/testdata project is generated see the script: https://github.com/kubernetes-sigs/kubebuilder/blob/master/test/testdata/generate.sh

 
I created a kubebuilder project and push it to a repo for easy reference.

PROJECT file: https://github.com/rmb938/multi-group-test/blob/main/PROJECT as you can see there's 2 groups each with a kind that is named the same.

APIs https://github.com/rmb938/multi-group-test/tree/main/apis

Controllers https://github.com/rmb938/multi-group-test/tree/main/controllers

CRD Patches https://github.com/rmb938/multi-group-test/tree/main/config/crd/patches Notice they only exist for the single group `foo`, the one for group `fiz` does not exist.

RBAC https://github.com/rmb938/multi-group-test/tree/main/config/rbac notice that the editor and viewer roles only exist for `foo` not for `fiz`.

You can replicate this project by running the following commands:

```
kubebuilder init --domain rmb938.com --repo github.com/rmb938/multi-group-test
kubebuilder edit --multigroup=true
kubebuilder create api --group foo --version v1alpha1 --kind Bar
kubebuilder create api --group fiz --version v1alpha1 --kind Bar
make generate
make manifests
```
Hi @rmb938, 

Thank you for the clarifications

That shows that when the multi-group support is enabled we are not using the group in the name of the files and generating them. So we need to:

**a) Ensure that we will consider the group to do the scaffolds**

Such as, change the boilerplates to use the group, see: https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches. In the same way that we do here (https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/plugins/golang/v2/scaffolds/internal/templates/api/group.go#L37-L43).  ( The same implementation actually)

**b) Update the sample/testdata to have the same kind for different groups to ensure the scenario**
The change must be done in:  https://github.com/kubernetes-sigs/kubebuilder/blob/master/test/testdata/generate.sh for the sample [kubebuilder/testdata/project-v3-multigroup](https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v3-multigroup). 

Could you help out with this fix? WDYT about a collab with a PR to fix this scenario? 



 
Yup I can get that done. I've been working on some internal kubebuilder scaffolds myself so I'm familiar with the modifications. I'll get the changes committed and a PR ready.

Fix patch:
diff --git a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go
index cc688e50f63..042f2228add 100644
--- a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go
+++ b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go
@@ -27,13 +27,18 @@ var _ machinery.Template = &EnableCAInjectionPatch{}
 // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD
 type EnableCAInjectionPatch struct {
 	machinery.TemplateMixin
+	machinery.MultiGroupMixin
 	machinery.ResourceMixin
 }
 
 // SetTemplateDefaults implements file.Template
 func (f *EnableCAInjectionPatch) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[plural].yaml")
+		if f.MultiGroup {
+			f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[group]_%[plural].yaml")
+		} else {
+			f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[plural].yaml")
+		}
 	}
 	f.Path = f.Resource.Replacer().Replace(f.Path)
 
diff --git a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go
index 61f41a02c27..35662c647ae 100644
--- a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go
+++ b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go
@@ -27,13 +27,19 @@ var _ machinery.Template = &EnableWebhookPatch{}
 // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD
 type EnableWebhookPatch struct {
 	machinery.TemplateMixin
+	machinery.MultiGroupMixin
 	machinery.ResourceMixin
 }
 
 // SetTemplateDefaults implements file.Template
 func (f *EnableWebhookPatch) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[plural].yaml")
+		if f.MultiGroup {
+			f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[group]_%[plural].yaml")
+		} else {
+			f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[plural].yaml")
+		}
+
 	}
 	f.Path = f.Resource.Replacer().Replace(f.Path)
 
diff --git a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_editor_role.go b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_editor_role.go
index a8832864277..ebd32254be7 100644
--- a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_editor_role.go
+++ b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_editor_role.go
@@ -27,13 +27,19 @@ var _ machinery.Template = &CRDEditorRole{}
 // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals
 type CRDEditorRole struct {
 	machinery.TemplateMixin
+	machinery.MultiGroupMixin
 	machinery.ResourceMixin
 }
 
 // SetTemplateDefaults implements file.Template
 func (f *CRDEditorRole) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("config", "rbac", "%[kind]_editor_role.yaml")
+		if f.MultiGroup {
+			f.Path = filepath.Join("config", "rbac", "%[group]_%[kind]_editor_role.yaml")
+		} else {
+			f.Path = filepath.Join("config", "rbac", "%[kind]_editor_role.yaml")
+		}
+
 	}
 	f.Path = f.Resource.Replacer().Replace(f.Path)
 
diff --git a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_viewer_role.go b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_viewer_role.go
index d83c3295f53..ba9edfca951 100644
--- a/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_viewer_role.go
+++ b/pkg/plugins/common/kustomize/v1/scaffolds/internal/templates/config/rbac/crd_viewer_role.go
@@ -27,13 +27,19 @@ var _ machinery.Template = &CRDViewerRole{}
 // CRDViewerRole scaffolds a file that defines the role that allows to view plurals
 type CRDViewerRole struct {
 	machinery.TemplateMixin
+	machinery.MultiGroupMixin
 	machinery.ResourceMixin
 }
 
 // SetTemplateDefaults implements file.Template
 func (f *CRDViewerRole) SetTemplateDefaults() error {
 	if f.Path == "" {
-		f.Path = filepath.Join("config", "rbac", "%[kind]_viewer_role.yaml")
+		if f.MultiGroup {
+			f.Path = filepath.Join("config", "rbac", "%[group]_%[kind]_viewer_role.yaml")
+		} else {
+			f.Path = filepath.Join("config", "rbac", "%[kind]_viewer_role.yaml")
+		}
+
 	}
 	f.Path = f.Resource.Replacer().Replace(f.Path)
 


Test patch:
diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh
index 151fb507afc..9296b2260b4 100755
--- a/test/testdata/generate.sh
+++ b/test/testdata/generate.sh
@@ -94,6 +94,9 @@ function scaffold_test_project {
     $kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false
 
     $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
 
     if [ $project == "project-v3-multigroup" ]; then
       $kb create api --version v1 --kind Lakers --controller=true --resource=true --make=false
@@ -127,4 +130,4 @@ scaffold_test_project project-v3
 scaffold_test_project project-v3-multigroup
 scaffold_test_project project-v3-addon --plugins="go/v3,declarative"
 scaffold_test_project project-v3-config --component-config
-scaffold_test_project project-v3-v1beta1
\ No newline at end of file
+scaffold_test_project project-v3-v1beta1
diff --git a/testdata/project-v2-multigroup/PROJECT b/testdata/project-v2-multigroup/PROJECT
index eab6e3e2b0a..de2471edb9c 100644
--- a/testdata/project-v2-multigroup/PROJECT
+++ b/testdata/project-v2-multigroup/PROJECT
@@ -23,4 +23,10 @@ resources:
 - group: foo.policy
   kind: HealthCheckPolicy
   version: v1
+- group: foo
+  kind: Bar
+  version: v1
+- group: fiz
+  kind: Bar
+  version: v1
 version: "2"
diff --git a/testdata/project-v2-multigroup/apis/fiz/v1/bar_types.go b/testdata/project-v2-multigroup/apis/fiz/v1/bar_types.go
new file mode 100644
index 00000000000..398da384cda
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/fiz/v1/bar_types.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 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 (
+	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
+//+kubebuilder:subresource:status
+
+// 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/testdata/project-v2-multigroup/apis/fiz/v1/groupversion_info.go b/testdata/project-v2-multigroup/apis/fiz/v1/groupversion_info.go
new file mode 100644
index 00000000000..8fbae6ea708
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/fiz/v1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 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 contains API Schema definitions for the fiz v1 API group
+//+kubebuilder:object:generate=true
+//+groupName=fiz.testproject.org
+package v1
+
+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: "fiz.testproject.org", Version: "v1"}
+
+	// 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/testdata/project-v2-multigroup/apis/fiz/v1/zz_generated.deepcopy.go b/testdata/project-v2-multigroup/apis/fiz/v1/zz_generated.deepcopy.go
new file mode 100644
index 00000000000..f46b6a1cb10
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/fiz/v1/zz_generated.deepcopy.go
@@ -0,0 +1,115 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2022 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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+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/testdata/project-v2-multigroup/apis/foo/v1/bar_types.go b/testdata/project-v2-multigroup/apis/foo/v1/bar_types.go
new file mode 100644
index 00000000000..398da384cda
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/foo/v1/bar_types.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 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 (
+	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
+//+kubebuilder:subresource:status
+
+// 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/testdata/project-v2-multigroup/apis/foo/v1/groupversion_info.go b/testdata/project-v2-multigroup/apis/foo/v1/groupversion_info.go
new file mode 100644
index 00000000000..06774db9bf0
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/foo/v1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 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 contains API Schema definitions for the foo v1 API group
+//+kubebuilder:object:generate=true
+//+groupName=foo.testproject.org
+package v1
+
+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: "foo.testproject.org", Version: "v1"}
+
+	// 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/testdata/project-v2-multigroup/apis/foo/v1/zz_generated.deepcopy.go b/testdata/project-v2-multigroup/apis/foo/v1/zz_generated.deepcopy.go
new file mode 100644
index 00000000000..f46b6a1cb10
--- /dev/null
+++ b/testdata/project-v2-multigroup/apis/foo/v1/zz_generated.deepcopy.go
@@ -0,0 +1,115 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2022 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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+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/testdata/project-v2-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml b/testdata/project-v2-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml
new file mode 100644
index 00000000000..cfdbb86c839
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml
@@ -0,0 +1,57 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.3.0
+  creationTimestamp: null
+  name: bars.fiz.testproject.org
+spec:
+  group: fiz.testproject.org
+  names:
+    kind: Bar
+    listKind: BarList
+    plural: bars
+    singular: bar
+  scope: Namespaced
+  subresources:
+    status: {}
+  validation:
+    openAPIV3Schema:
+      description: Bar is the Schema for the bars API
+      properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: BarSpec defines the desired state of Bar
+          properties:
+            foo:
+              description: Foo is an example field of Bar. Edit bar_types.go to remove/update
+              type: string
+          type: object
+        status:
+          description: BarStatus defines the observed state of Bar
+          type: object
+      type: object
+  version: v1
+  versions:
+  - name: v1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/testdata/project-v2-multigroup/config/crd/bases/foo.testproject.org_bars.yaml b/testdata/project-v2-multigroup/config/crd/bases/foo.testproject.org_bars.yaml
new file mode 100644
index 00000000000..6795b94d0a8
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/crd/bases/foo.testproject.org_bars.yaml
@@ -0,0 +1,57 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.3.0
+  creationTimestamp: null
+  name: bars.foo.testproject.org
+spec:
+  group: foo.testproject.org
+  names:
+    kind: Bar
+    listKind: BarList
+    plural: bars
+    singular: bar
+  scope: Namespaced
+  subresources:
+    status: {}
+  validation:
+    openAPIV3Schema:
+      description: Bar is the Schema for the bars API
+      properties:
+        apiVersion:
+          description: 'APIVersion defines the versioned schema of this representation
+            of an object. Servers should convert recognized schemas to the latest
+            internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+          type: string
+        kind:
+          description: 'Kind is a string value representing the REST resource this
+            object represents. Servers may infer this from the endpoint the client
+            submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+          type: string
+        metadata:
+          type: object
+        spec:
+          description: BarSpec defines the desired state of Bar
+          properties:
+            foo:
+              description: Foo is an example field of Bar. Edit bar_types.go to remove/update
+              type: string
+          type: object
+        status:
+          description: BarStatus defines the observed state of Bar
+          type: object
+      type: object
+  version: v1
+  versions:
+  - name: v1
+    served: true
+    storage: true
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/testdata/project-v2-multigroup/config/crd/kustomization.yaml b/testdata/project-v2-multigroup/config/crd/kustomization.yaml
index 9857ccc8d2f..4adeb7a0fa2 100644
--- a/testdata/project-v2-multigroup/config/crd/kustomization.yaml
+++ b/testdata/project-v2-multigroup/config/crd/kustomization.yaml
@@ -9,6 +9,8 @@ resources:
 - bases/sea-creatures.testproject.org_krakens.yaml
 - bases/sea-creatures.testproject.org_leviathans.yaml
 - bases/foo.policy.testproject.org_healthcheckpolicies.yaml
+- bases/foo.testproject.org_bars.yaml
+- bases/fiz.testproject.org_bars.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
@@ -21,6 +23,7 @@ patchesStrategicMerge:
 #- patches/webhook_in_krakens.yaml
 #- patches/webhook_in_leviathans.yaml
 #- patches/webhook_in_healthcheckpolicies.yaml
+#- patches/webhook_in_bars.yaml
 #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
 # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
@@ -32,6 +35,7 @@ patchesStrategicMerge:
 #- patches/cainjection_in_krakens.yaml
 #- patches/cainjection_in_leviathans.yaml
 #- patches/cainjection_in_healthcheckpolicies.yaml
+#- patches/cainjection_in_bars.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/testdata/project-v2-multigroup/config/crd/patches/cainjection_in_bars.yaml b/testdata/project-v2-multigroup/config/crd/patches/cainjection_in_bars.yaml
new file mode 100644
index 00000000000..b70829763ab
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/crd/patches/cainjection_in_bars.yaml
@@ -0,0 +1,8 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+# CRD conversion requires k8s 1.13 or later.
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: bars.foo.testproject.org
diff --git a/testdata/project-v2-multigroup/config/crd/patches/webhook_in_bars.yaml b/testdata/project-v2-multigroup/config/crd/patches/webhook_in_bars.yaml
new file mode 100644
index 00000000000..19154af0fbd
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/crd/patches/webhook_in_bars.yaml
@@ -0,0 +1,17 @@
+# The following patch enables conversion webhook for CRD
+# CRD conversion requires k8s 1.13 or later.
+apiVersion: apiextensions.k8s.io/v1beta1
+kind: CustomResourceDefinition
+metadata:
+  name: bars.foo.testproject.org
+spec:
+  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: system
+        name: webhook-service
+        path: /convert
diff --git a/testdata/project-v2-multigroup/config/rbac/bar_editor_role.yaml b/testdata/project-v2-multigroup/config/rbac/bar_editor_role.yaml
new file mode 100644
index 00000000000..e9915a5b9c3
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/rbac/bar_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-editor-role
+rules:
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v2-multigroup/config/rbac/bar_viewer_role.yaml b/testdata/project-v2-multigroup/config/rbac/bar_viewer_role.yaml
new file mode 100644
index 00000000000..a4569ac9654
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/rbac/bar_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-viewer-role
+rules:
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v2-multigroup/config/rbac/role.yaml b/testdata/project-v2-multigroup/config/rbac/role.yaml
index 7ae49be3caa..e15ab18ab92 100644
--- a/testdata/project-v2-multigroup/config/rbac/role.yaml
+++ b/testdata/project-v2-multigroup/config/rbac/role.yaml
@@ -46,6 +46,26 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - foo.policy.testproject.org
   resources:
@@ -66,6 +86,26 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - sea-creatures.testproject.org
   resources:
diff --git a/testdata/project-v2-multigroup/config/samples/fiz_v1_bar.yaml b/testdata/project-v2-multigroup/config/samples/fiz_v1_bar.yaml
new file mode 100644
index 00000000000..5f4d6ced1e2
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/samples/fiz_v1_bar.yaml
@@ -0,0 +1,6 @@
+apiVersion: fiz.testproject.org/v1
+kind: Bar
+metadata:
+  name: bar-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/testdata/project-v2-multigroup/config/samples/foo_v1_bar.yaml b/testdata/project-v2-multigroup/config/samples/foo_v1_bar.yaml
new file mode 100644
index 00000000000..f43fb43849f
--- /dev/null
+++ b/testdata/project-v2-multigroup/config/samples/foo_v1_bar.yaml
@@ -0,0 +1,6 @@
+apiVersion: foo.testproject.org/v1
+kind: Bar
+metadata:
+  name: bar-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/testdata/project-v2-multigroup/controllers/fiz/bar_controller.go b/testdata/project-v2-multigroup/controllers/fiz/bar_controller.go
new file mode 100644
index 00000000000..621df6df8bd
--- /dev/null
+++ b/testdata/project-v2-multigroup/controllers/fiz/bar_controller.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 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 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"
+
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/fiz/v1"
+)
+
+// BarReconciler reconciles a Bar object
+type BarReconciler struct {
+	client.Client
+	Log    logr.Logger
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=fiz.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=fiz.testproject.org,resources=bars/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 Bar 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 *BarReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
+	_ = context.Background()
+	_ = r.Log.WithValues("bar", req.NamespacedName)
+
+	// TODO(user): your logic here
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&fizv1.Bar{}).
+		Complete(r)
+}
diff --git a/testdata/project-v2-multigroup/controllers/fiz/suite_test.go b/testdata/project-v2-multigroup/controllers/fiz/suite_test.go
new file mode 100644
index 00000000000..5f3bb0037a7
--- /dev/null
+++ b/testdata/project-v2-multigroup/controllers/fiz/suite_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2022 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 controllers
+
+import (
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "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"
+	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/fiz/v1"
+	//+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
+
+func TestAPIs(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Controller Suite",
+		[]Reporter{printer.NewlineReporter{}})
+}
+
+var _ = BeforeSuite(func(done Done) {
+	logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
+	}
+
+	var err error
+	cfg, err = testEnv.Start()
+	Expect(err).ToNot(HaveOccurred())
+	Expect(cfg).ToNot(BeNil())
+
+	err = fizv1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	//+kubebuilder:scaffold:scheme
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).ToNot(HaveOccurred())
+	Expect(k8sClient).ToNot(BeNil())
+
+	close(done)
+}, 60)
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).ToNot(HaveOccurred())
+})
diff --git a/testdata/project-v2-multigroup/controllers/foo/bar_controller.go b/testdata/project-v2-multigroup/controllers/foo/bar_controller.go
new file mode 100644
index 00000000000..0bc99b0dd3d
--- /dev/null
+++ b/testdata/project-v2-multigroup/controllers/foo/bar_controller.go
@@ -0,0 +1,63 @@
+/*
+Copyright 2022 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 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"
+
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/foo/v1"
+)
+
+// BarReconciler reconciles a Bar object
+type BarReconciler struct {
+	client.Client
+	Log    logr.Logger
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=foo.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=foo.testproject.org,resources=bars/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 Bar 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 *BarReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
+	_ = context.Background()
+	_ = r.Log.WithValues("bar", req.NamespacedName)
+
+	// TODO(user): your logic here
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&foov1.Bar{}).
+		Complete(r)
+}
diff --git a/testdata/project-v2-multigroup/controllers/foo/suite_test.go b/testdata/project-v2-multigroup/controllers/foo/suite_test.go
new file mode 100644
index 00000000000..6c44388deab
--- /dev/null
+++ b/testdata/project-v2-multigroup/controllers/foo/suite_test.go
@@ -0,0 +1,81 @@
+/*
+Copyright 2022 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 controllers
+
+import (
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "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"
+	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/foo/v1"
+	//+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
+
+func TestAPIs(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Controller Suite",
+		[]Reporter{printer.NewlineReporter{}})
+}
+
+var _ = BeforeSuite(func(done Done) {
+	logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
+	}
+
+	var err error
+	cfg, err = testEnv.Start()
+	Expect(err).ToNot(HaveOccurred())
+	Expect(cfg).ToNot(BeNil())
+
+	err = foov1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	//+kubebuilder:scaffold:scheme
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).ToNot(HaveOccurred())
+	Expect(k8sClient).ToNot(BeNil())
+
+	close(done)
+}, 60)
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).ToNot(HaveOccurred())
+})
diff --git a/testdata/project-v2-multigroup/main.go b/testdata/project-v2-multigroup/main.go
index 53cf8617c7d..61478e2f292 100644
--- a/testdata/project-v2-multigroup/main.go
+++ b/testdata/project-v2-multigroup/main.go
@@ -28,7 +28,9 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/crew/v1"
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/fiz/v1"
 	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/foo.policy/v1"
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/foo/v1"
 	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/sea-creatures/v1beta1"
 	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/sea-creatures/v1beta2"
 	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/ship/v1"
@@ -36,6 +38,8 @@ import (
 	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/apis/ship/v2alpha1"
 	appscontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/apps"
 	crewcontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/crew"
+	fizcontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/fiz"
+	foocontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/foo"
 	foopolicycontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/foo.policy"
 	seacreaturescontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/sea-creatures"
 	shipcontroller "sigs.k8s.io/kubebuilder/testdata/project-v2-multigroup/controllers/ship"
@@ -57,6 +61,8 @@ func init() {
 	utilruntime.Must(seacreaturesv1beta1.AddToScheme(scheme))
 	utilruntime.Must(seacreaturesv1beta2.AddToScheme(scheme))
 	utilruntime.Must(foopolicyv1.AddToScheme(scheme))
+	utilruntime.Must(foov1.AddToScheme(scheme))
+	utilruntime.Must(fizv1.AddToScheme(scheme))
 	//+kubebuilder:scaffold:scheme
 }
 
@@ -163,6 +169,22 @@ func main() {
 		setupLog.Error(err, "unable to create controller", "controller", "Deployment")
 		os.Exit(1)
 	}
+	if err = (&foocontroller.BarReconciler{
+		Client: mgr.GetClient(),
+		Log:    ctrl.Log.WithName("controllers").WithName("Bar"),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Bar")
+		os.Exit(1)
+	}
+	if err = (&fizcontroller.BarReconciler{
+		Client: mgr.GetClient(),
+		Log:    ctrl.Log.WithName("controllers").WithName("Bar"),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Bar")
+		os.Exit(1)
+	}
 	//+kubebuilder:scaffold:builder
 
 	setupLog.Info("starting manager")
diff --git a/testdata/project-v3-multigroup/PROJECT b/testdata/project-v3-multigroup/PROJECT
index fa59fa454f5..938072fdc06 100644
--- a/testdata/project-v3-multigroup/PROJECT
+++ b/testdata/project-v3-multigroup/PROJECT
@@ -84,6 +84,24 @@ resources:
   kind: Deployment
   path: k8s.io/api/apps/v1
   version: v1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: testproject.org
+  group: foo
+  kind: Bar
+  path: sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/foo/v1
+  version: v1
+- api:
+    crdVersion: v1
+    namespaced: true
+  controller: true
+  domain: testproject.org
+  group: fiz
+  kind: Bar
+  path: sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/fiz/v1
+  version: v1
 - api:
     crdVersion: v1
     namespaced: true
diff --git a/testdata/project-v3-multigroup/apis/fiz/v1/bar_types.go b/testdata/project-v3-multigroup/apis/fiz/v1/bar_types.go
new file mode 100644
index 00000000000..398da384cda
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/fiz/v1/bar_types.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 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 (
+	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
+//+kubebuilder:subresource:status
+
+// 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/testdata/project-v3-multigroup/apis/fiz/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/fiz/v1/groupversion_info.go
new file mode 100644
index 00000000000..8fbae6ea708
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/fiz/v1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 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 contains API Schema definitions for the fiz v1 API group
+//+kubebuilder:object:generate=true
+//+groupName=fiz.testproject.org
+package v1
+
+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: "fiz.testproject.org", Version: "v1"}
+
+	// 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/testdata/project-v3-multigroup/apis/fiz/v1/zz_generated.deepcopy.go b/testdata/project-v3-multigroup/apis/fiz/v1/zz_generated.deepcopy.go
new file mode 100644
index 00000000000..f46b6a1cb10
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/fiz/v1/zz_generated.deepcopy.go
@@ -0,0 +1,115 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2022 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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+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/testdata/project-v3-multigroup/apis/foo/v1/bar_types.go b/testdata/project-v3-multigroup/apis/foo/v1/bar_types.go
new file mode 100644
index 00000000000..398da384cda
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/foo/v1/bar_types.go
@@ -0,0 +1,64 @@
+/*
+Copyright 2022 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 (
+	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
+//+kubebuilder:subresource:status
+
+// 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/testdata/project-v3-multigroup/apis/foo/v1/groupversion_info.go b/testdata/project-v3-multigroup/apis/foo/v1/groupversion_info.go
new file mode 100644
index 00000000000..06774db9bf0
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/foo/v1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2022 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 contains API Schema definitions for the foo v1 API group
+//+kubebuilder:object:generate=true
+//+groupName=foo.testproject.org
+package v1
+
+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: "foo.testproject.org", Version: "v1"}
+
+	// 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/testdata/project-v3-multigroup/apis/foo/v1/zz_generated.deepcopy.go b/testdata/project-v3-multigroup/apis/foo/v1/zz_generated.deepcopy.go
new file mode 100644
index 00000000000..f46b6a1cb10
--- /dev/null
+++ b/testdata/project-v3-multigroup/apis/foo/v1/zz_generated.deepcopy.go
@@ -0,0 +1,115 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright 2022 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.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1
+
+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/testdata/project-v3-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml b/testdata/project-v3-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml
new file mode 100644
index 00000000000..9e9c2b5a1a3
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml
@@ -0,0 +1,56 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.8.0
+  creationTimestamp: null
+  name: bars.fiz.testproject.org
+spec:
+  group: fiz.testproject.org
+  names:
+    kind: Bar
+    listKind: BarList
+    plural: bars
+    singular: bar
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        description: Bar is the Schema for the bars API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BarSpec defines the desired state of Bar
+            properties:
+              foo:
+                description: Foo is an example field of Bar. Edit bar_types.go to
+                  remove/update
+                type: string
+            type: object
+          status:
+            description: BarStatus defines the observed state of Bar
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/testdata/project-v3-multigroup/config/crd/bases/foo.testproject.org_bars.yaml b/testdata/project-v3-multigroup/config/crd/bases/foo.testproject.org_bars.yaml
new file mode 100644
index 00000000000..24002b5b0c8
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/bases/foo.testproject.org_bars.yaml
@@ -0,0 +1,56 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.8.0
+  creationTimestamp: null
+  name: bars.foo.testproject.org
+spec:
+  group: foo.testproject.org
+  names:
+    kind: Bar
+    listKind: BarList
+    plural: bars
+    singular: bar
+  scope: Namespaced
+  versions:
+  - name: v1
+    schema:
+      openAPIV3Schema:
+        description: Bar is the Schema for the bars API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: BarSpec defines the desired state of Bar
+            properties:
+              foo:
+                description: Foo is an example field of Bar. Edit bar_types.go to
+                  remove/update
+                type: string
+            type: object
+          status:
+            description: BarStatus defines the observed state of Bar
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/testdata/project-v3-multigroup/config/crd/kustomization.yaml b/testdata/project-v3-multigroup/config/crd/kustomization.yaml
index 23b9a6c97df..be93af7e522 100644
--- a/testdata/project-v3-multigroup/config/crd/kustomization.yaml
+++ b/testdata/project-v3-multigroup/config/crd/kustomization.yaml
@@ -9,6 +9,8 @@ resources:
 - bases/sea-creatures.testproject.org_krakens.yaml
 - bases/sea-creatures.testproject.org_leviathans.yaml
 - bases/foo.policy.testproject.org_healthcheckpolicies.yaml
+- bases/foo.testproject.org_bars.yaml
+- bases/fiz.testproject.org_bars.yaml
 - bases/testproject.org_lakers.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
@@ -22,6 +24,7 @@ patchesStrategicMerge:
 #- patches/webhook_in_krakens.yaml
 #- patches/webhook_in_leviathans.yaml
 #- patches/webhook_in_healthcheckpolicies.yaml
+#- patches/webhook_in_bars.yaml
 #- patches/webhook_in_lakers.yaml
 #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
@@ -34,6 +37,7 @@ patchesStrategicMerge:
 #- patches/cainjection_in_krakens.yaml
 #- patches/cainjection_in_leviathans.yaml
 #- patches/cainjection_in_healthcheckpolicies.yaml
+#- patches/cainjection_in_bars.yaml
 #- patches/cainjection_in_lakers.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_lakers.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in__lakers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_lakers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in__lakers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_captains.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_crew_captains.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_captains.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_crew_captains.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_fiz_bars.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_fiz_bars.yaml
new file mode 100644
index 00000000000..515b99a8104
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_fiz_bars.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: bars.fiz.testproject.org
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_healthcheckpolicies.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_foo.policy_healthcheckpolicies.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_healthcheckpolicies.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_foo.policy_healthcheckpolicies.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_foo_bars.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_foo_bars.yaml
new file mode 100644
index 00000000000..35fd01aedea
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_foo_bars.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: bars.foo.testproject.org
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_krakens.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_sea-creatures_krakens.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_krakens.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_sea-creatures_krakens.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_leviathans.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_sea-creatures_leviathans.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_leviathans.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_sea-creatures_leviathans.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_cruisers.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_cruisers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_cruisers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_cruisers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_destroyers.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_destroyers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_destroyers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_destroyers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_frigates.yaml b/testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_frigates.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/cainjection_in_frigates.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/cainjection_in_ship_frigates.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_lakers.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in__lakers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_lakers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in__lakers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_captains.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_crew_captains.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_captains.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_crew_captains.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_fiz_bars.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_fiz_bars.yaml
new file mode 100644
index 00000000000..eddaa868fdd
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_fiz_bars.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: bars.fiz.testproject.org
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_healthcheckpolicies.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_foo.policy_healthcheckpolicies.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_healthcheckpolicies.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_foo.policy_healthcheckpolicies.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_foo_bars.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_foo_bars.yaml
new file mode 100644
index 00000000000..831ad1b8164
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_foo_bars.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: bars.foo.testproject.org
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_krakens.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_sea-creatures_krakens.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_krakens.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_sea-creatures_krakens.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_leviathans.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_sea-creatures_leviathans.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_leviathans.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_sea-creatures_leviathans.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_cruisers.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_cruisers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_cruisers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_cruisers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_destroyers.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_destroyers.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_destroyers.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_destroyers.yaml
diff --git a/testdata/project-v3-multigroup/config/crd/patches/webhook_in_frigates.yaml b/testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_frigates.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/crd/patches/webhook_in_frigates.yaml
rename to testdata/project-v3-multigroup/config/crd/patches/webhook_in_ship_frigates.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/lakers_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/_lakers_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/lakers_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/_lakers_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/lakers_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/_lakers_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/lakers_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/_lakers_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/captain_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/crew_captain_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/captain_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/crew_captain_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/captain_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/crew_captain_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/captain_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/crew_captain_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/fiz_bar_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/fiz_bar_editor_role.yaml
new file mode 100644
index 00000000000..6b34df93f0b
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/rbac/fiz_bar_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-editor-role
+rules:
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v3-multigroup/config/rbac/fiz_bar_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/fiz_bar_viewer_role.yaml
new file mode 100644
index 00000000000..64d4dd9ea85
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/rbac/fiz_bar_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-viewer-role
+rules:
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v3-multigroup/config/rbac/healthcheckpolicy_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/foo.policy_healthcheckpolicy_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/healthcheckpolicy_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/foo.policy_healthcheckpolicy_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/healthcheckpolicy_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/foo.policy_healthcheckpolicy_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/healthcheckpolicy_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/foo.policy_healthcheckpolicy_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/foo_bar_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/foo_bar_editor_role.yaml
new file mode 100644
index 00000000000..e9915a5b9c3
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/rbac/foo_bar_editor_role.yaml
@@ -0,0 +1,24 @@
+# permissions for end users to edit bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-editor-role
+rules:
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v3-multigroup/config/rbac/foo_bar_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/foo_bar_viewer_role.yaml
new file mode 100644
index 00000000000..a4569ac9654
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/rbac/foo_bar_viewer_role.yaml
@@ -0,0 +1,20 @@
+# permissions for end users to view bars.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  name: bar-viewer-role
+rules:
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
diff --git a/testdata/project-v3-multigroup/config/rbac/role.yaml b/testdata/project-v3-multigroup/config/rbac/role.yaml
index 90652f752c9..569506669d7 100644
--- a/testdata/project-v3-multigroup/config/rbac/role.yaml
+++ b/testdata/project-v3-multigroup/config/rbac/role.yaml
@@ -57,6 +57,32 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - fiz.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - foo.policy.testproject.org
   resources:
@@ -83,6 +109,32 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - foo.testproject.org
+  resources:
+  - bars/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - sea-creatures.testproject.org
   resources:
diff --git a/testdata/project-v3-multigroup/config/rbac/kraken_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/sea-creatures_kraken_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/kraken_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/sea-creatures_kraken_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/kraken_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/sea-creatures_kraken_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/kraken_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/sea-creatures_kraken_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/leviathan_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/sea-creatures_leviathan_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/leviathan_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/sea-creatures_leviathan_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/leviathan_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/sea-creatures_leviathan_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/leviathan_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/sea-creatures_leviathan_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/cruiser_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_cruiser_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/cruiser_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_cruiser_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/cruiser_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_cruiser_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/cruiser_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_cruiser_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/destroyer_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_destroyer_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/destroyer_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_destroyer_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/destroyer_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_destroyer_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/destroyer_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_destroyer_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/frigate_editor_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_frigate_editor_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/frigate_editor_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_frigate_editor_role.yaml
diff --git a/testdata/project-v3-multigroup/config/rbac/frigate_viewer_role.yaml b/testdata/project-v3-multigroup/config/rbac/ship_frigate_viewer_role.yaml
similarity index 100%
rename from testdata/project-v3-multigroup/config/rbac/frigate_viewer_role.yaml
rename to testdata/project-v3-multigroup/config/rbac/ship_frigate_viewer_role.yaml
diff --git a/testdata/project-v3-multigroup/config/samples/fiz_v1_bar.yaml b/testdata/project-v3-multigroup/config/samples/fiz_v1_bar.yaml
new file mode 100644
index 00000000000..5f4d6ced1e2
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/samples/fiz_v1_bar.yaml
@@ -0,0 +1,6 @@
+apiVersion: fiz.testproject.org/v1
+kind: Bar
+metadata:
+  name: bar-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/testdata/project-v3-multigroup/config/samples/foo_v1_bar.yaml b/testdata/project-v3-multigroup/config/samples/foo_v1_bar.yaml
new file mode 100644
index 00000000000..f43fb43849f
--- /dev/null
+++ b/testdata/project-v3-multigroup/config/samples/foo_v1_bar.yaml
@@ -0,0 +1,6 @@
+apiVersion: foo.testproject.org/v1
+kind: Bar
+metadata:
+  name: bar-sample
+spec:
+  # TODO(user): Add fields here
diff --git a/testdata/project-v3-multigroup/controllers/fiz/bar_controller.go b/testdata/project-v3-multigroup/controllers/fiz/bar_controller.go
new file mode 100644
index 00000000000..afd0693288c
--- /dev/null
+++ b/testdata/project-v3-multigroup/controllers/fiz/bar_controller.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 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 fiz
+
+import (
+	"context"
+
+	"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"
+
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/fiz/v1"
+)
+
+// BarReconciler reconciles a Bar object
+type BarReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=fiz.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=fiz.testproject.org,resources=bars/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=fiz.testproject.org,resources=bars/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 Bar 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.11.0/pkg/reconcile
+func (r *BarReconciler) 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 *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&fizv1.Bar{}).
+		Complete(r)
+}
diff --git a/testdata/project-v3-multigroup/controllers/fiz/suite_test.go b/testdata/project-v3-multigroup/controllers/fiz/suite_test.go
new file mode 100644
index 00000000000..68911da28c5
--- /dev/null
+++ b/testdata/project-v3-multigroup/controllers/fiz/suite_test.go
@@ -0,0 +1,80 @@
+/*
+Copyright 2022 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 fiz
+
+import (
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "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"
+	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/fiz/v1"
+	//+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
+
+func TestAPIs(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Controller Suite",
+		[]Reporter{printer.NewlineReporter{}})
+}
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
+	}
+
+	cfg, err := testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	err = fizv1.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())
+
+}, 60)
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/testdata/project-v3-multigroup/controllers/foo/bar_controller.go b/testdata/project-v3-multigroup/controllers/foo/bar_controller.go
new file mode 100644
index 00000000000..94518337989
--- /dev/null
+++ b/testdata/project-v3-multigroup/controllers/foo/bar_controller.go
@@ -0,0 +1,62 @@
+/*
+Copyright 2022 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 foo
+
+import (
+	"context"
+
+	"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"
+
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/foo/v1"
+)
+
+// BarReconciler reconciles a Bar object
+type BarReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=foo.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=foo.testproject.org,resources=bars/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=foo.testproject.org,resources=bars/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 Bar 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.11.0/pkg/reconcile
+func (r *BarReconciler) 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 *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&foov1.Bar{}).
+		Complete(r)
+}
diff --git a/testdata/project-v3-multigroup/controllers/foo/suite_test.go b/testdata/project-v3-multigroup/controllers/foo/suite_test.go
new file mode 100644
index 00000000000..e536c060229
--- /dev/null
+++ b/testdata/project-v3-multigroup/controllers/foo/suite_test.go
@@ -0,0 +1,80 @@
+/*
+Copyright 2022 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 foo
+
+import (
+	"path/filepath"
+	"testing"
+
+	. "github.com/onsi/ginkgo"
+	. "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"
+	"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/foo/v1"
+	//+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
+
+func TestAPIs(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecsWithDefaultAndCustomReporters(t,
+		"Controller Suite",
+		[]Reporter{printer.NewlineReporter{}})
+}
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
+	}
+
+	cfg, err := testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	err = foov1.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())
+
+}, 60)
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/testdata/project-v3-multigroup/main.go b/testdata/project-v3-multigroup/main.go
index 229a21f11a6..aa566237105 100644
--- a/testdata/project-v3-multigroup/main.go
+++ b/testdata/project-v3-multigroup/main.go
@@ -32,7 +32,9 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/crew/v1"
+	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/fiz/v1"
 	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/foo.policy/v1"
+	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/foo/v1"
 	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/sea-creatures/v1beta1"
 	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/sea-creatures/v1beta2"
 	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/apis/ship/v1"
@@ -42,6 +44,8 @@ import (
 	"sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers"
 	appscontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/apps"
 	crewcontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/crew"
+	fizcontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/fiz"
+	foocontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/foo"
 	foopolicycontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/foo.policy"
 	seacreaturescontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/sea-creatures"
 	shipcontrollers "sigs.k8s.io/kubebuilder/testdata/project-v3-multigroup/controllers/ship"
@@ -63,6 +67,8 @@ func init() {
 	utilruntime.Must(seacreaturesv1beta1.AddToScheme(scheme))
 	utilruntime.Must(seacreaturesv1beta2.AddToScheme(scheme))
 	utilruntime.Must(foopolicyv1.AddToScheme(scheme))
+	utilruntime.Must(foov1.AddToScheme(scheme))
+	utilruntime.Must(fizv1.AddToScheme(scheme))
 	utilruntime.Must(testprojectorgv1.AddToScheme(scheme))
 	//+kubebuilder:scaffold:scheme
 }
@@ -169,6 +175,20 @@ func main() {
 		setupLog.Error(err, "unable to create controller", "controller", "Deployment")
 		os.Exit(1)
 	}
+	if err = (&foocontrollers.BarReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Bar")
+		os.Exit(1)
+	}
+	if err = (&fizcontrollers.BarReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Bar")
+		os.Exit(1)
+	}
 	if err = (&controllers.LakersReconciler{
 		Client: mgr.GetClient(),
 		Scheme: mgr.GetScheme(),