docs/book/src/migration/manual-process.md
Please ensure you have followed the installation guide
to install the required components and have the desired version of the
Kubebuilder CLI available in your PATH.
This guide outlines the manual steps to migrate your existing Kubebuilder project to a newer version of the Kubebuilder framework. This process involves re-scaffolding your project and manually porting over your custom code and configurations.
From Kubebuilder v3.0.0 onwards, all inputs used by Kubebuilder are tracked in the PROJECT file.
Ensure that you check this file in your current project to verify the recorded configuration and metadata.
Review the PROJECT file documentation for a better understanding.
Also, before starting, it is recommended to check What's in a basic project? to better understand the project layouts and structure.
<aside class="warning"> <h1>About Manual Migration</h1>Manual migration is more complex than automated methods but gives you complete control. Use manual migration when:
Two-phase approach (recommended for legacy layouts):
This keeps your project working at each step and simplifies porting. AI migration helpers are provided to automate repetitive tasks. See AI Migration Helpers for AI instructions that automate both phases.
For future updates: Once migrated, use the AutoUpdate plugin or alpha update command to automatically update scaffolds with 3-way merge while preserving customizations.
</aside>Only needed if ANY of these are true:
internal/controller/internal/webhook/cmd/Skip this phase if your project already uses internal/controller/, internal/webhook/, and cmd/main.go.
git checkout -b reorganize
If you used Step 1: Reorganize to New Layout AI migration helper, your project is already reorganized. Skip to Phase 2.
</aside>Move files to new layout:
# If you have controllers/ directory
mkdir -p internal/controller
mv controllers/* internal/controller/
rmdir controllers
# OR if you have pkg/controllers/ directory
mkdir -p internal/controller
mv pkg/controllers/* internal/controller/
# If you have webhooks in api/v1/ or apis/v1/
mkdir -p internal/webhook/v1
mv api/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || mv apis/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || echo "No webhook files found to move (this is expected if your project has no webhooks)"
# If main.go is in root
mkdir -p cmd
mv main.go cmd/
After moving files, update package declarations:
Controllers: Change package controllers → package controller in all *_controller.go and *_controller_test.go files.
Webhooks: Keep version as package name (e.g., package v1 stays package v1 in internal/webhook/v1/).
Find and update all imports:
grep -r "pkg/controllers\|/controllers\"" --include="*.go"
In each file found, update:
<module>/controllers or <module>/pkg/controllers → <module>/internal/controllercontrollers.TypeName → controller.TypeNameIf your Dockerfile has explicit COPY statements for moved paths, update them to reflect the new structure, or simplify to COPY . . and use .dockerignore to exclude unnecessary files.
Build and test the reorganized project:
make generate manifests
make build && make test
If successful, commit the layout changes. Your project now uses the new layout. Proceed to Phase 2.
Create a branch from your current codebase:
git checkout -b migration
mkdir ../migration-backup
cp -r . ../migration-backup/
Remove all files except .git:
find . -not -path './.git*' -not -name '.' -not -name '..' -delete
About the PROJECT file: From v3.0.0+, the PROJECT file tracks all scaffolding metadata. If you have one and used CLI for all resources, try kubebuilder alpha generate first. Otherwise, follow the manual steps below to identify and re-scaffold all resources.
Identify the information you'll need for initialization from your backup.
<aside class="note"> <h1>Skip if Using AI Migration Helper</h1>If you used Step 2: Discovery CLI Commands AI migration helper, you already have a complete script with all commands. Execute it and skip to Step 4: Port Your Custom Code.
</aside>Module path - Check your backup's go.mod file:
cat ../migration-backup/go.mod
Look for the module line:
module tutorial.kubebuilder.io/migration-project
Domain - Check your backup's PROJECT file:
cat ../migration-backup/PROJECT
Look for the domain line:
domain: tutorial.kubebuilder.io
If you don't have a PROJECT file (versions < v3.0.0),
check your CRD files under config/crd/bases/ or examine the API group names.
The domain is the part after the group name in your API groups.
Initialize a new Go module using the same module path from your original project:
go mod init tutorial.kubebuilder.io/migration-project
Replace tutorial.kubebuilder.io/migration-project with your actual module path.
Initialize the project with Kubebuilder:
kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/migration-project
Replace with your actual domain and repository (module path).
<aside class="note"> <h1>Understanding init options</h1>--domain: The domain for your API groups (e.g., tutorial.kubebuilder.io). Your full API groups will be <group>.<domain>.--repo: Your Go module path (same as in go.mod)Multi-group projects organize APIs into different groups, with each group in its own directory. This is useful when you have APIs for different purposes or domains.
Check if your project uses multi-group layout by examining your backup's directory structure:
Single-group layout: All APIs in one group
api/v1/cronjob_types.goapi/v1/job_types.goapi/v2/cronjob_types.goMulti-group layout: APIs organized by group
api/batch/v1/cronjob_types.goapi/crew/v1/captain_types.goapi/sea/v1/ship_types.goYou can also check your backup's PROJECT file for multigroup: true.
If your project uses multi-group layout, enable it before creating APIs:
kubebuilder edit --multigroup=true
This must be done before creating any APIs to ensure they're scaffolded in the multi-group structure.
</aside>When following this guide, you'll get the new layout automatically since you're creating a fresh project with the latest version and porting your code into it.
For each API resource in your original project, re-scaffold them in the new project.
Review your backup project (../migration-backup/) to identify all APIs. It's recommended to check the backup directory
regardless of whether you have a PROJECT file, as not all resources may have been created using the CLI.
Check the directory structure in your backup to ensure you don't miss any manually created resources:
Look in the api/ directory (or apis/ for projects generated with older Kubebuilder versions) for *_types.go files:
api/v1/cronjob_types.go - extract: version v1, kind CronJob, group from importsapi/batch/v1/cronjob_types.go - extract: group batch, version v1, kind CronJobCheck for controllers in these locations:
internal/controller/cronjob_controller.go or internal/controller/<group>/cronjob_controller.gocontrollers/cronjob_controller.go or pkg/controllers/cronjob_controller.goIf you used the CLI to create all APIs from Kubebuilder v3.0.0+ you should have them in the PROJECT file under the resources section, such as:
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
group: batch
kind: CronJob
version: v1
Make a list of all APIs with their group, version, kind, and whether they have a controller. This will help you systematically re-scaffold everything.
</aside>For each API identified in step 3.1, re-scaffold it:
kubebuilder create api --group batch --version v1 --kind CronJob
When prompted:
After creating each API, update the generated manifests and code:
make manifests # Generate CRD, RBAC, and other config files
make generate # Generate code (e.g., DeepCopy methods)
Then verify everything compiles:
make build
These steps ensure the newly scaffolded API is properly integrated. See the Quick Start guide for a detailed walkthrough of the API creation workflow.
Repeat this process for ALL APIs in your project.
<aside class="note"> <h1>Using External Types (controllers for types not defined in your project)</h1>If your project has controllers for Kubernetes built-in types (like Deployment, Pod) or types from other projects:
kubebuilder create api --group apps --version v1 --kind Deployment --resource=false --controller=true
Or for CRDs from other projects, i.e. cert-manager's Certificate type:
kubebuilder create api --group "cert-manager" --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io --external-api-module=github.com/cert-manager/[email protected]
Use --resource=false to skip creating the API definition and only scaffold the controller.
Ensure that you check Using External Types for more details.
</aside>After creating all resources, regenerate manifests:
make manifests
make generate
If your original project has webhooks, you need to re-scaffold them.
Identify webhooks in your backup project:
From directory structure, look for webhook files:
api/v1/<kind>_webhook.go or api/<group>/<version>/<kind>_webhook.gointernal/webhook/<version>/<kind>_webhook.gointernal/webhook/<group>/<version>/<kind>_webhook.goFrom PROJECT file (if available), check each resource's webhooks section:
resources:
- api:
...
webhooks:
defaulting: true
validation: true
webhookVersion: v1
Re-scaffold webhooks:
For each resource with webhooks, run:
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
Webhook options:
--defaulting - creates a defaulting webhook (sets default values)--programmatic-validation - creates a validation webhook (validates create/update/delete operations)--conversion - creates a conversion webhook (for multi-version APIs, see next section)If your project has multi-version APIs with conversion webhooks, you need to set up the hub-spoke conversion pattern.
<aside class="note"> <h1>Understanding Hub-Spoke Conversion</h1>In Kubernetes multi-version APIs, the hub is the version that all other versions (spokes) convert to and from:
The hub implements Hub() marker interface, while spokes implement ConvertTo() and ConvertFrom() methods to convert to/from the hub.
Setting up conversion webhooks:
Create the conversion webhook for the hub version, with spoke versions specified using the --spoke flag.
Note: In the examples below, we use v1 as the hub for illustration. Choose the version in your project that should be the central conversion point—typically your most feature-complete and stable storage version, not necessarily the oldest or newest.
kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2
This command:
v1 as the hub versionv2 as a spoke that converts to/from the hub v1*_conversion.go files with conversion method stubsFor multiple spokes, specify them as a comma-separated list:
kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2,v1alpha1
This sets up v1 as the hub with both v2 and v1alpha1 as spokes.
What you need to implement:
The command generates method stubs that you'll fill in during Step 4:
Hub() method (usually just a marker)ConvertTo(hub) and ConvertFrom(hub) methods with your conversion logicSee the Multi-Version Tutorial for comprehensive guidance on implementing the conversion logic.
<aside class="note"> <H1> Forget a type of webhook ? </h1>If you forget a webhook type, use --force to re-run the command:
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --force
For external types, you can also create webhooks:
kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation
More info: Webhook Overview, Admission Webhook, and Creating Webhooks for External Types.
</aside>After scaffolding all webhooks, verify everything compiles:
make manifests && make build
If you used Step 3: Port Custom Code AI migration helper, your code is already ported.
You may skip to Step 5. However, it's still recommended to at least review the following steps and do manual validation to ensure all code was properly ported.
</aside>Manually port your custom business logic and configurations from the backup to the new project.
<aside class="note"> <h1>Use diff tools</h1>Use IDE diff tools or commands like diff -r ../migration-backup/ . to compare directories and identify all customizations you need to port.
Most modern IDEs support directory-level comparison which makes this process much easier.
Compare and merge your custom API fields and markers from your backup project.
Files to compare:
api/v1/<kind>_types.goapi/<group>/<version>/<kind>_types.goWhat to port:
+kubebuilder:validation:Minimum=0, +kubebuilder:validation:Pattern=...+kubebuilder:printcolumn, +kubebuilder:resource:scope=Cluster+kubebuilder:subresource:status, +kubebuilder:subresource:scaleSee CRD Generation, CRD Validation, and Markers for all available markers.
If your APIs reference a parent package (e.g., scheduling.GroupName), port it:
mkdir -p api/<group>/
cp ../migration-backup/apis/<group>/groupversion_info.go api/<group>/
After porting API definitions, regenerate and verify:
make manifests # Generate CRD manifests from your types
make generate # Generate DeepCopy methods
This ensures your API types and CRD manifests are properly generated before moving forward.
Files to compare:
internal/controller/<kind>_controller.gointernal/controller/<group>/<kind>_controller.goWhat to port:
+kubebuilder:rbac:groups=...,resources=...,verbs=...SetupWithManagerSee RBAC Markers for details on permission markers.
After porting controller logic, regenerate manifests and verify compilation:
make generate
make manifests
make build
Webhooks have changed location between Kubebuilder versions. Be aware of the path differences:
Legacy webhook location (Kubebuilder v3 and earlier):
api/v1/<kind>_webhook.goapi/<group>/<version>/<kind>_webhook.goCurrent webhook location:
internal/webhook/<version>/<kind>_webhook.gointernal/webhook/<group>/<version>/<kind>_webhook.goWhat to port:
Default() method implementationValidateCreate(), ValidateUpdate(), ValidateDelete() methodsConvertTo() and ConvertFrom() methods (for multi-version APIs)Even if the webhook file is in a different location, the implementation logic remains largely the same. Copy the method implementations, not just the entire file.
</aside>See Webhook Overview, Admission Webhook, and the Multi-Version Tutorial for details.
For conversion webhooks:
If you have conversion webhooks, ensure you used the create webhook --conversion --spoke <version> command in Step 3.4. This sets up the hub-spoke infrastructure automatically. You only need to fill in the conversion logic in the ConvertTo() and ConvertFrom() methods in your spoke versions, and the Hub() method in your hub version.
The command creates all the necessary boilerplate - you just implement the business logic for converting fields between versions.
After porting webhooks, regenerate and verify:
make generate
make manifests
make build
File: cmd/main.go
Most projects don't need to customize main.go as Kubebuilder handles all the standard setup automatically
(registering APIs, setting up controllers and webhooks, manager initialization, metrics, etc.).
Only port customizations that are not part of the standard scaffold. Compare your backup main.go with the
new scaffolded one to identify any custom logic you added.
The config/ directory contains Kustomize manifests for deploying your operator. Compare with your backup to ensure all configurations are properly set up.
Review and update these directories:
config/default/kustomization.yaml - Main kustomization file
config/manager/ - Controller manager deployment
config/rbac/ - RBAC configurations
config/webhook/ - Webhook configurations (if applicable)
config/samples/ - Sample CR manifests
After configuring Kustomize, verify the manifests build correctly:
make all
make build-installer
Port any additional packages, dependencies, and customizations from your backup:
Additional packages (e.g., pkg/util):
cp -r ../migration-backup/pkg/<package-name> pkg/
# Update import paths (works on both macOS and Linux)
find pkg/ -name "*.go" -exec sed -i.bak 's|<module>/apis/|<module>/api/|g' {} \;
find pkg/ -name "*.go.bak" -delete
For dependencies, run go mod tidy or copy go.mod/go.sum from backup for complex projects.
Check for additional customizations (Makefile, Dockerfile, test files). Use diff tools to compare with backup and identify missed files.
<aside class="note"> <h1>Using diff tools</h1>Use your IDE's diff tools to compare the current directory with your backup (../migration-backup/) or use git to compare
your current branch with your main branch. This helps identify any files you may have missed.
After porting all customizations, verify everything builds:
make all
Compare against the backup to ensure all customizations were correctly ported, such as:
diff -r --brief ../migration-backup/ . | grep "Only in ../migration-backup"
Run tests and verify functionality:
make test && make lint-fix
Deploy to a test cluster (e.g. kind) and verify the changes (i.e. validate expected behavior, run regression checks, confirm the full CI pipeline still passes, and execute the e2e tests).
<aside class="note"> <h1>If You Have a Helm Chart</h1>If you had a Helm chart to distribute your project, you may want to regenerate it with the helm/v2-alpha plugin, then apply your customizations.
kubebuilder edit --plugins=helm/v2-alpha
Compare your backup's chart/values.yaml and custom templates with the newly generated chart, and apply your customizations and ensure that all is still working
as before.