Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/book/src/migration/multi-group.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ You can tell which layout you're using by checking your `PROJECT` file for `mult

The following steps migrate the [CronJob example][cronjob-tutorial] from single-group to multi-group layout.

<aside class="note">
<h1>Starting new projects with multigroup</h1>

If you're starting a **new project** and already know you want multigroup layout, you can use the `--multigroup` flag during initialization:

```bash
kubebuilder init --domain example.org --multigroup
```

This guide is for **existing projects** that need to be migrated from single-group to multi-group layout.

</aside>

### Step 1: Enable multi-group mode

First, tell Kubebuilder you want to use multi-group layout:
Expand Down
4 changes: 2 additions & 2 deletions docs/book/src/reference/project-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ Now let's check its layout fields definition:
| `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. |
| `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. |
| `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. |
| `multigroup` | **(Optional)** When set to `true`, enables multi-group project layout. APIs are organized into group-specific directories (`api/<group>/<version>/`). Can be toggled via `kubebuilder edit --multigroup`. Default is `false` (omitted from PROJECT file). |
| `namespaced` | **(Optional)** When set to `true`, configures the project for namespace-scoped deployment. The operator will only watch and manage resources within its deployment namespace, using namespace-scoped RBAC (`Role`/`RoleBinding` instead of `ClusterRole`/`ClusterRoleBinding`). Can be toggled via `kubebuilder edit --namespaced`. Default is `false` (cluster-scoped, omitted from PROJECT file). |
| `multigroup` | **(Optional)** When set to `true`, enables multi-group project layout. APIs are organized into group-specific directories (`api/<group>/<version>/`). Can be set during initialization via `kubebuilder init --multigroup` or enabled/disabled later via `kubebuilder edit --multigroup`. Default is `false` (omitted from PROJECT file). |
| `namespaced` | **(Optional)** When set to `true`, configures the project for namespace-scoped deployment. The operator will only watch and manage resources within its deployment namespace, using namespace-scoped RBAC (`Role`/`RoleBinding` instead of `ClusterRole`/`ClusterRoleBinding`). Can be enabled/disabled via `kubebuilder edit --namespaced`. Default is `false` (cluster-scoped, omitted from PROJECT file). |
| `resources` | An array of all resources which were scaffolded in the project. |
| `resources.api` | The API scaffolded in the project via the sub-command `create api`. |
| `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. |
Expand Down
27 changes: 3 additions & 24 deletions pkg/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,6 @@ func (opts *Generate) Generate() error {
return fmt.Errorf("error initializing project config: %w", err)
}

if err = kubebuilderEdit(projectConfig); err != nil {
return fmt.Errorf("error editing project config: %w", err)
}

if err = kubebuilderCreate(projectConfig); err != nil {
return fmt.Errorf("error creating project config: %w", err)
}
Expand Down Expand Up @@ -203,26 +199,6 @@ func kubebuilderInit(s store.Store) error {
return nil
}

// Edits the project to enable or disable multigroup layout and namespace-scoped deployment.
func kubebuilderEdit(s store.Store) error {
var args []string
needsEdit := false

if s.Config().IsMultiGroup() {
args = append(args, "--multigroup")
needsEdit = true
}

if needsEdit {
editArgs := append([]string{"edit"}, args...)
if err := util.RunCmd("kubebuilder edit", "kubebuilder", editArgs...); err != nil {
return fmt.Errorf("failed to run kubebuilder edit command: %w", err)
}
}

return nil
}

// Creates APIs and Webhooks for the project.
func kubebuilderCreate(s store.Store) error {
resources, err := s.Config().GetResources()
Expand Down Expand Up @@ -447,6 +423,9 @@ func getInitArgs(s store.Store) []string {
if projectName := s.Config().GetProjectName(); projectName != "" {
args = append(args, "--project-name", projectName)
}
if s.Config().IsMultiGroup() {
args = append(args, "--multigroup")
}
if s.Config().IsNamespaced() {
args = append(args, "--namespaced")
}
Expand Down
41 changes: 16 additions & 25 deletions pkg/cli/alpha/internal/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,21 @@ var _ = Describe("generate: get-args-helpers", func() {
})
})

When("multigroup flag is enabled", func() {
It("includes --multigroup in init args", func() {
cfg := &fakeConfig{
pluginChain: []string{"go.kubebuilder.io/v4"},
domain: "foo.com",
repo: "bar",
multigroup: true,
}
store := &fakeStore{cfg: cfg}
args := getInitArgs(store)
Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"),
"--domain", "foo.com", "--repo", "bar", "--multigroup"))
})
})

When("namespaced flag is enabled", func() {
It("includes --namespaced in init args", func() {
cfg := &fakeConfig{
Expand Down Expand Up @@ -425,8 +440,7 @@ var _ = Describe("generate: get-args-helpers", func() {
store := &fakeStore{cfg: cfg}
args := getInitArgs(store)
Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"),
"--domain", "foo.com", "--repo", "bar", "--namespaced"))
// Note: multigroup is handled by kubebuilderEdit, not init
"--domain", "foo.com", "--repo", "bar", "--multigroup", "--namespaced"))
})
})
})
Expand Down Expand Up @@ -770,29 +784,6 @@ var _ = Describe("generate: kubebuilder", func() {
})
})

Context("kubebuilderEdit", func() {
It("runs kubebuilder edit successfully for multigroup layout", func() {
cfg := &fakeConfig{multigroup: true}
store := &fakeStore{cfg: cfg}
// Run kubebuilderEdit and verify no errors
Expect(kubebuilderEdit(store)).To(Succeed())
})

It("runs kubebuilder edit successfully for namespaced layout", func() {
cfg := &fakeConfig{namespaced: true}
store := &fakeStore{cfg: cfg}
// Run kubebuilderEdit and verify no errors
Expect(kubebuilderEdit(store)).To(Succeed())
})

It("runs kubebuilder edit successfully for both multigroup and namespaced", func() {
cfg := &fakeConfig{multigroup: true, namespaced: true}
store := &fakeStore{cfg: cfg}
// Run kubebuilderEdit and verify no errors
Expect(kubebuilderEdit(store)).To(Succeed())
})
})

Context("kubebuilderGrafanaEdit", func() {
It("runs kubebuilder edit successfully for Grafana plugin", func() {
// Run kubebuilderGrafanaEdit and verify no errors
Expand Down
57 changes: 32 additions & 25 deletions pkg/plugins/golang/v4/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,44 +38,51 @@ type editSubcommand struct {
}

func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
subcmdMeta.Description = `Edit the project configuration.

Features:
- Toggle multigroup layout (organize APIs by group).
- Toggle namespaced layout (namespace-scoped vs cluster-scoped).

Namespaced layout (--namespaced):
Changes namespace-scoped (watches specific namespaces) vs cluster-scoped (watches all namespaces).
What changes automatically:
- Updates PROJECT file (namespaced: true)
- Scaffolds Role/RoleBinding instead of ClusterRole/ClusterRoleBinding
- With --force: Regenerates config/manager/manager.yaml with WATCH_NAMESPACE env var
What you must update manually:
- Add namespace= to RBAC markers in existing controllers (new controllers get this automatically)
- Update cmd/main.go to use namespace-scoped cache
- Run: make manifests
subcmdMeta.Description = `Edit project configuration to enable or disable layout settings.

Multigroup (--multigroup):
Enable or disable multi-group layout.
Changes API structure: api/<version>/ becomes api/<group>/<version>/
Automatic: Updates PROJECT file, future APIs use new structure
Manual: Move existing API files, update import paths in controllers
Migration guide: https://book.kubebuilder.io/migration/multi-group.html

Namespaced (--namespaced):
Enable or disable namespace-scoped deployment.
Manager watches one or more specific namespaces vs all namespaces.
Namespaces to watch are configured via WATCH_NAMESPACE environment variable.
Automatic: Updates PROJECT file, scaffolds Role/RoleBinding, uses --force to regenerate manager.yaml
Manual: Add namespace= to RBAC markers in existing controllers, update cmd/main.go, run 'make manifests'

Force (--force):
Overwrite existing scaffolded files to apply configuration changes.
Example: With --namespaced, regenerates config/manager/manager.yaml to add WATCH_NAMESPACE env var.
Warning: This overwrites default scaffold files; manual changes in those files may be lost.

Note: To add optional plugins after initialization, use 'kubebuilder edit --plugins <plugin-name>'.
Run 'kubebuilder edit --plugins --help' to see available plugins.
`
subcmdMeta.Examples = fmt.Sprintf(` # Enable multigroup layout
%[1]s edit --multigroup

# Disable multigroup layout
%[1]s edit --multigroup=false
# Enable namespace-scoped permissions
%[1]s edit --namespaced

# Enable namespaced layout (--force regenerates config/manager/manager.yaml with WATCH_NAMESPACE)
# Enable with automatic file regeneration
%[1]s edit --namespaced --force

# Enable namespaced layout without force (manually update config/manager/manager.yaml)
%[1]s edit --namespaced
# Disable multigroup layout
%[1]s edit --multigroup=false

# Disable namespaced layout (--force regenerates config/manager/manager.yaml without WATCH_NAMESPACE)
%[1]s edit --namespaced=false --force
# Enable/disable multiple settings
%[1]s edit --multigroup --namespaced --force
`, cliMeta.CommandName)
}

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout")
fs.BoolVar(&p.namespaced, "namespaced", false, "enable or disable namespaced layout")
fs.BoolVar(&p.force, "force", false, "overwrite existing files (regenerates manager.yaml with WATCH_NAMESPACE)")
fs.BoolVar(&p.namespaced, "namespaced", false, "enable or disable namespace-scoped deployment")
fs.BoolVar(&p.force, "force", false, "overwrite scaffolded files to apply changes (manual edits may be lost)")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
Expand Down
73 changes: 56 additions & 17 deletions pkg/plugins/golang/v4/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type initSubcommand struct {
// flags
fetchDeps bool
skipGoVersionCheck bool
multigroup bool
namespaced bool
}

Expand All @@ -70,40 +71,72 @@ func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *
- several YAML files for project deployment under the "config" directory
- a "cmd/main.go" file that creates the manager that will run the project controllers

Namespaced layout (--namespaced):
Scaffolds the project for namespace-scoped deployment instead of cluster-scoped.
- Creates Role/RoleBinding instead of ClusterRole/ClusterRoleBinding
- Adds WATCH_NAMESPACE environment variable to manager deployment
- New controllers will include namespace= in RBAC markers automatically
Required flags:
--domain: Domain for your APIs (e.g., example.org creates crew.example.org for API groups)

Configuration flags:
--repo: Go module path (e.g., github.com/user/repo); auto-detected if not provided
--owner: Owner name for copyright license headers
--license: License to use (apache2 or none, default: apache2)

Plugin flags:
--plugins: Comma-separated list of plugins to use (default: go/v4)
Plugins scaffold files during init and are saved to the PROJECT layout
Future operations (i.e. create api, create webhook) call all plugins in the chain
Run 'kubebuilder init --plugins --help' to see available plugins

Layout flags:
--multigroup: Enable multigroup layout to organize APIs by group
Scaffolds APIs in api/<group>/<version>/ instead of api/<version>/
Useful when managing multiple API groups (e.g., batch, apps, crew)
--namespaced: Enable namespace-scoped deployment instead of cluster-scoped
Manager watches one or more specific namespaces instead of all namespaces
Namespaces to watch are configured via WATCH_NAMESPACE environment variable
Uses Role/RoleBinding instead of ClusterRole/ClusterRoleBinding
Suitable for multi-tenant environments or limited scope deployments

Note: Layout settings can be changed later with 'kubebuilder edit'.
`
subcmdMeta.Examples = fmt.Sprintf(` # Initialize a new project
%[1]s init --plugins go/v4 --domain example.org --owner "Your name"
%[1]s init --domain example.org

# Initialize with namespaced layout (namespace-scoped)
%[1]s init --plugins go/v4 --domain example.org --namespaced
# Initialize with multigroup layout
%[1]s init --domain example.org --multigroup

# Initialize with specific project version
%[1]s init --plugins go/v4 --project-version 3
# Initialize with namespace-scoped deployment
%[1]s init --domain example.org --namespaced

# Initialize with optional plugins
%[1]s init --plugins go/v4,autoupdate/v1-alpha --domain example.org
%[1]s init --plugins go/v4,helm/v2-alpha --domain example.org

# Initialize with custom settings
%[1]s init --domain example.org --owner "Your Name" --license apache2

# Initialize with all options combined
%[1]s init --plugins go/v4,autoupdate/v1-alpha --domain example.org --multigroup --namespaced
`, cliMeta.CommandName)
}

func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
false, "if specified, skip checking the Go version")
false, "skip Go version check")

// dependency args
fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "ensure dependencies are downloaded")
fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "download dependencies after scaffolding")

// boilerplate args
fs.StringVar(&p.license, "license", "apache2",
"license to use to boilerplate, may be one of 'apache2', 'none'")
fs.StringVar(&p.owner, "owner", "", "owner to add to the copyright")
"license header to use (apache2 or none)")
fs.StringVar(&p.owner, "owner", "", "copyright owner for license headers")

// project args
fs.StringVar(&p.repo, "repo", "", "name to use for go module (e.g., github.com/user/repo), "+
"defaults to the go package of the current working directory.")
fs.StringVar(&p.repo, "repo", "", "Go module name (e.g., github.com/user/repo); "+
"auto-detected from current directory if not provided")
fs.BoolVar(&p.multigroup, "multigroup", false,
"enable multigroup layout (organize APIs by group)")
fs.BoolVar(&p.namespaced, "namespaced", false,
"if specified, scaffold the project with namespaced layout (default: cluster-scoped)")
"enable namespace-scoped deployment (default: cluster-scoped)")
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
Expand All @@ -122,6 +155,12 @@ func (p *initSubcommand) InjectConfig(c config.Config) error {
return fmt.Errorf("error setting repository: %w", err)
}

if p.multigroup {
if err := p.config.SetMultiGroup(); err != nil {
return fmt.Errorf("error setting multigroup: %w", err)
}
}

if p.namespaced {
if err := p.config.SetNamespaced(); err != nil {
return fmt.Errorf("error setting namespaced: %w", err)
Expand Down
44 changes: 42 additions & 2 deletions pkg/plugins/golang/v4/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
)

const testRepo = "github.com/example/test"

var _ = Describe("initSubcommand", func() {
var (
subCmd *initSubcommand
Expand All @@ -40,11 +42,11 @@ var _ = Describe("initSubcommand", func() {

Context("InjectConfig", func() {
It("should set repository when provided", func() {
subCmd.repo = "github.com/example/test"
subCmd.repo = testRepo
err := subCmd.InjectConfig(cfg)

Expect(err).NotTo(HaveOccurred())
Expect(cfg.GetRepository()).To(Equal("github.com/example/test"))
Expect(cfg.GetRepository()).To(Equal(testRepo))
})

It("should fail when repository cannot be detected", func() {
Expand All @@ -64,6 +66,44 @@ var _ = Describe("initSubcommand", func() {

Expect(err).To(HaveOccurred())
})

It("should set multigroup when flag is enabled", func() {
subCmd.repo = testRepo
subCmd.multigroup = true
err := subCmd.InjectConfig(cfg)

Expect(err).NotTo(HaveOccurred())
Expect(cfg.IsMultiGroup()).To(BeTrue())
})

It("should not set multigroup when flag is disabled", func() {
subCmd.repo = testRepo
subCmd.multigroup = false
err := subCmd.InjectConfig(cfg)

Expect(err).NotTo(HaveOccurred())
Expect(cfg.IsMultiGroup()).To(BeFalse())
})

It("should set namespaced when flag is enabled", func() {
subCmd.repo = testRepo
subCmd.namespaced = true
err := subCmd.InjectConfig(cfg)

Expect(err).NotTo(HaveOccurred())
Expect(cfg.IsNamespaced()).To(BeTrue())
})

It("should set both multigroup and namespaced when both flags are enabled", func() {
subCmd.repo = testRepo
subCmd.multigroup = true
subCmd.namespaced = true
err := subCmd.InjectConfig(cfg)

Expect(err).NotTo(HaveOccurred())
Expect(cfg.IsMultiGroup()).To(BeTrue())
Expect(cfg.IsNamespaced()).To(BeTrue())
})
})

Context("checkDir validation", func() {
Expand Down
Loading