Skip to content

Commit f7e03e2

Browse files
(CLI): Allow users define project-path
1 parent 7681e75 commit f7e03e2

9 files changed

Lines changed: 757 additions & 56 deletions

File tree

docs/book/src/faq.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ Then the result resource group will be `mygroup.example.com`.
2424

2525
> If domain field not set, the default value is `my.domain`.
2626
27+
## How can I run Kubebuilder commands on a project in a different directory?
28+
29+
Use the global `--project-path` flag to work on projects without changing directories. This is useful for monorepos or automation.
30+
31+
```shell
32+
# Initialize project in specific directory
33+
kubebuilder --project-path=./services/api init --domain example.com
34+
35+
# Create API in different project
36+
kubebuilder --project-path=./services/api create api --group apps --version v1 --kind MyApp
37+
```
38+
39+
> The flag works with all project commands but not with `version`, `completion`, or `help`.
40+
2741
## I'd like to customize my project to use [klog][klog] instead of the [zap][zap] provided by controller-runtime. How to use `klog` or other loggers as the project logger?
2842

2943
In the `main.go` you can replace:

docs/book/src/plugins/available/helm-v2-alpha.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -481,9 +481,9 @@ The Makefile targets use sensible defaults extracted from your project configura
481481

482482
| Flag | Description |
483483
|---------------------|-----------------------------------------------------------------------------|
484-
| **--manifests** | Path to YAML file containing Kubernetes manifests (default: `dist/install.yaml`) |
485-
| **--output-dir** string | Output directory for chart (default: `dist`) |
486-
| **--force** | Regenerates preserved files except `Chart.yaml` (`values.yaml`, `NOTES.txt`, `_helpers.tpl`, `.helmignore`, `test-chart.yml`) |
484+
| **--manifests** | Path to your Kubernetes manifests YAML file. Default: `dist/install.yaml`. |
485+
| **--output-dir** | Where to create the Helm chart. Default: `dist`. |
486+
| **--force** | Recreates all files except `Chart.yaml`. This includes `values.yaml`, `NOTES.txt`, `_helpers.tpl`, `.helmignore`, and `test-chart.yml`. |
487487

488488
<aside class="note" role="note">
489489
<p class="note-title"> Examples </p>

internal/cli/alpha/generate.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func NewScaffoldCommand() *cobra.Command {
4343
scaffoldCmd := &cobra.Command{
4444
Use: "generate",
4545
Short: "Re-scaffold a Kubebuilder project from its PROJECT file",
46-
Long: `The 'generate' command re-creates a Kubebuilder project scaffold based on the configuration
46+
Long: `The 'generate' command re-creates a Kubebuilder project scaffold based on the configuration
4747
defined in the PROJECT file, using the latest installed Kubebuilder version and plugins.
4848
4949
This is helpful for migrating projects to a newer Kubebuilder layout or plugin version (e.g., v3 to v4)
@@ -52,13 +52,22 @@ as update your project from any previous version to the current one.
5252
If no output directory is provided, the current working directory will be cleaned (except .git and PROJECT).`,
5353
Example: `
5454
# **WARNING**(will delete all files to allow the re-scaffold except .git and PROJECT)
55-
# Re-scaffold the project in-place
56-
kubebuilder alpha generate
55+
# Re-scaffold the project in-place (using global --project-path)
56+
kubebuilder --project-path="./path/to/project" alpha generate
5757
58-
# Re-scaffold the project from ./test into ./my-output
59-
kubebuilder alpha generate --input-dir="./path/to/project" --output-dir="./my-output"
58+
# Re-scaffold into a different output directory
59+
kubebuilder --project-path="./path/to/project" alpha generate --output-dir="./my-output"
60+
61+
# Legacy: using --input-dir (DEPRECATED - use global --project-path instead)
62+
kubebuilder alpha generate --input-dir="./path/to/project"
6063
`,
6164
PreRunE: func(_ *cobra.Command, _ []string) error {
65+
// Handle deprecation warning
66+
if opts.InputDir != "" {
67+
slog.Warn("--input-dir is DEPRECATED and will be removed in a future release")
68+
slog.Warn("Use the global --project-path flag instead:")
69+
slog.Warn(" Example: kubebuilder --project-path=\"./myproject\" alpha generate")
70+
}
6271
return opts.Validate()
6372
},
6473
Run: func(_ *cobra.Command, _ []string) {
@@ -70,8 +79,10 @@ If no output directory is provided, the current working directory will be cleane
7079
}
7180

7281
scaffoldCmd.Flags().StringVar(&opts.InputDir, "input-dir", "",
73-
"Path to the directory containing the PROJECT file. "+
74-
"Defaults to the current working directory. WARNING: delete existing files (except .git and PROJECT).")
82+
"[DEPRECATED: use global --project-path instead] "+
83+
"Path to the directory containing the PROJECT file.")
84+
// Mark the flag as deprecated
85+
_ = scaffoldCmd.Flags().MarkDeprecated("input-dir", "use global --project-path flag instead (e.g., kubebuilder --project-path=./path alpha generate)")
7586

7687
scaffoldCmd.Flags().StringVar(&opts.OutputDir, "output-dir", "",
7788
"Directory where the new project scaffold will be written. "+

internal/cli/alpha/internal/generate.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ var getExecutablePathFunc = getExecutablePath
4949

5050
// Generate handles the migration and scaffolding process.
5151
func (opts *Generate) Generate() error {
52+
// If InputDir is not set, use current working directory.
53+
// This handles both:
54+
// 1. Legacy --input-dir flag (explicitly sets opts.InputDir)
55+
// 2. Global --project-path flag (changes CWD before this runs, opts.InputDir is empty)
56+
if opts.InputDir == "" {
57+
cwd, err := os.Getwd()
58+
if err != nil {
59+
return fmt.Errorf("failed to get working directory: %w", err)
60+
}
61+
opts.InputDir = cwd
62+
slog.Info("Using current working directory as input directory", "path", opts.InputDir)
63+
}
64+
5265
projectConfig, err := common.LoadProjectConfig(opts.InputDir)
5366
if err != nil {
5467
return fmt.Errorf("error loading project config: %v", err)

internal/cli/alpha/internal/generate_test.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,4 +1103,243 @@ var _ = Describe("Generate", func() {
11031103
Expect(g.Generate()).To(Succeed())
11041104
})
11051105
})
1106+
1107+
Context("with global --project-path flag behavior", func() {
1108+
var projectDir string
1109+
1110+
BeforeEach(func() {
1111+
// Create a separate project directory
1112+
var err error
1113+
projectDir, err = os.MkdirTemp("", "alpha-project-path-test-*")
1114+
Expect(err).NotTo(HaveOccurred())
1115+
1116+
// Create PROJECT file in project directory
1117+
projectFilePath := filepath.Join(projectDir, "PROJECT")
1118+
projectFileContent := []byte(`domain: example.com
1119+
layout:
1120+
- go.kubebuilder.io/v4
1121+
projectName: test-project
1122+
repo: github.com/example/test-project
1123+
version: "3"
1124+
`)
1125+
Expect(os.WriteFile(projectFilePath, projectFileContent, 0o644)).To(Succeed())
1126+
})
1127+
1128+
AfterEach(func() {
1129+
if projectDir != "" {
1130+
_ = os.RemoveAll(projectDir)
1131+
}
1132+
})
1133+
1134+
It("should work when CWD is changed to project directory (simulating --project-path)", func() {
1135+
// Save current directory
1136+
originalWd, err := os.Getwd()
1137+
Expect(err).NotTo(HaveOccurred())
1138+
defer func() {
1139+
_ = os.Chdir(originalWd)
1140+
}()
1141+
1142+
// Change to project directory (this is what --project-path does)
1143+
Expect(os.Chdir(projectDir)).To(Succeed())
1144+
1145+
// InputDir should be empty when using global --project-path
1146+
// The Generate function should default to CWD
1147+
g := &Generate{
1148+
InputDir: "", // Empty - will use CWD
1149+
OutputDir: filepath.Join(projectDir, "regenerated"),
1150+
SkipGoVersionCheck: true,
1151+
}
1152+
1153+
// Validate should succeed
1154+
err = g.Validate()
1155+
Expect(err).NotTo(HaveOccurred())
1156+
1157+
// Verify InputDir gets set to CWD (handle symlink resolution on macOS)
1158+
expectedDir, err := filepath.EvalSymlinks(projectDir)
1159+
Expect(err).NotTo(HaveOccurred())
1160+
1161+
actualDir, err := filepath.EvalSymlinks(g.InputDir)
1162+
Expect(err).NotTo(HaveOccurred())
1163+
1164+
Expect(actualDir).To(Equal(expectedDir))
1165+
})
1166+
1167+
It("should use CWD when InputDir is empty", func() {
1168+
// Save current directory
1169+
originalWd, err := os.Getwd()
1170+
Expect(err).NotTo(HaveOccurred())
1171+
defer func() {
1172+
_ = os.Chdir(originalWd)
1173+
}()
1174+
1175+
// Change to project directory
1176+
Expect(os.Chdir(projectDir)).To(Succeed())
1177+
1178+
// Create Generate with empty InputDir
1179+
g := &Generate{
1180+
InputDir: "",
1181+
SkipGoVersionCheck: true,
1182+
}
1183+
1184+
// Call Generate - it should use CWD as InputDir
1185+
// Note: This will fail during kubebuilder execution but that's OK,
1186+
// we're testing that it picks up the right directory
1187+
_ = g.Generate()
1188+
1189+
// Verify that InputDir was set to the current working directory
1190+
cwd, err := os.Getwd()
1191+
Expect(err).NotTo(HaveOccurred())
1192+
Expect(g.InputDir).To(Equal(cwd))
1193+
})
1194+
1195+
It("should load PROJECT file from CWD when InputDir is empty", func() {
1196+
// Save current directory
1197+
originalWd, err := os.Getwd()
1198+
Expect(err).NotTo(HaveOccurred())
1199+
defer func() {
1200+
_ = os.Chdir(originalWd)
1201+
}()
1202+
1203+
// Change to project directory
1204+
Expect(os.Chdir(projectDir)).To(Succeed())
1205+
1206+
// Verify PROJECT file can be loaded from CWD
1207+
_, err = os.Stat("PROJECT")
1208+
Expect(err).NotTo(HaveOccurred())
1209+
1210+
// Verify content
1211+
content, err := os.ReadFile("PROJECT")
1212+
Expect(err).NotTo(HaveOccurred())
1213+
Expect(string(content)).To(ContainSubstring("domain: example.com"))
1214+
Expect(string(content)).To(ContainSubstring("test-project"))
1215+
})
1216+
1217+
It("should resolve output directory relative to project directory", func() {
1218+
// Save current directory
1219+
originalWd, err := os.Getwd()
1220+
Expect(err).NotTo(HaveOccurred())
1221+
defer func() {
1222+
_ = os.Chdir(originalWd)
1223+
}()
1224+
1225+
// Change to project directory
1226+
Expect(os.Chdir(projectDir)).To(Succeed())
1227+
1228+
// Create Generate with relative output path
1229+
g := &Generate{
1230+
InputDir: "", // Will use CWD
1231+
OutputDir: "regenerated",
1232+
SkipGoVersionCheck: true,
1233+
}
1234+
1235+
// Validate should succeed
1236+
err = g.Validate()
1237+
Expect(err).NotTo(HaveOccurred())
1238+
1239+
// Verify paths are resolved correctly
1240+
expectedInputDir, err := filepath.EvalSymlinks(projectDir)
1241+
Expect(err).NotTo(HaveOccurred())
1242+
1243+
actualInputDir, err := filepath.EvalSymlinks(g.InputDir)
1244+
Expect(err).NotTo(HaveOccurred())
1245+
1246+
Expect(actualInputDir).To(Equal(expectedInputDir))
1247+
})
1248+
1249+
It("should handle absolute output directory path", func() {
1250+
// Save current directory
1251+
originalWd, err := os.Getwd()
1252+
Expect(err).NotTo(HaveOccurred())
1253+
defer func() {
1254+
_ = os.Chdir(originalWd)
1255+
}()
1256+
1257+
// Create separate output directory
1258+
outputDir, err := os.MkdirTemp("", "alpha-output-*")
1259+
Expect(err).NotTo(HaveOccurred())
1260+
defer func() {
1261+
_ = os.RemoveAll(outputDir)
1262+
}()
1263+
1264+
// Change to project directory
1265+
Expect(os.Chdir(projectDir)).To(Succeed())
1266+
1267+
// Create Generate with absolute output path
1268+
g := &Generate{
1269+
InputDir: "", // Will use CWD (project directory)
1270+
OutputDir: outputDir,
1271+
SkipGoVersionCheck: true,
1272+
}
1273+
1274+
// Validate should succeed
1275+
err = g.Validate()
1276+
Expect(err).NotTo(HaveOccurred())
1277+
1278+
// Verify input is CWD and output is absolute
1279+
expectedInputDir, err := filepath.EvalSymlinks(projectDir)
1280+
Expect(err).NotTo(HaveOccurred())
1281+
1282+
actualInputDir, err := filepath.EvalSymlinks(g.InputDir)
1283+
Expect(err).NotTo(HaveOccurred())
1284+
1285+
Expect(actualInputDir).To(Equal(expectedInputDir))
1286+
})
1287+
1288+
It("should work with PROJECT file containing multiple resources", func() {
1289+
// Create PROJECT file with resources
1290+
projectFileContent := []byte(`domain: example.com
1291+
layout:
1292+
- go.kubebuilder.io/v4
1293+
projectName: test-project
1294+
repo: github.com/example/test-project
1295+
resources:
1296+
- api:
1297+
crdVersion: v1
1298+
namespaced: true
1299+
controller: true
1300+
domain: example.com
1301+
group: apps
1302+
kind: WebApp
1303+
path: github.com/example/test-project/api/v1
1304+
version: v1
1305+
- api:
1306+
crdVersion: v1
1307+
namespaced: true
1308+
controller: true
1309+
domain: example.com
1310+
group: batch
1311+
kind: Job
1312+
path: github.com/example/test-project/api/v1
1313+
version: v1
1314+
version: "3"
1315+
`)
1316+
Expect(os.WriteFile(filepath.Join(projectDir, "PROJECT"), projectFileContent, 0o644)).To(Succeed())
1317+
1318+
// Save current directory
1319+
originalWd, err := os.Getwd()
1320+
Expect(err).NotTo(HaveOccurred())
1321+
defer func() {
1322+
_ = os.Chdir(originalWd)
1323+
}()
1324+
1325+
// Change to project directory
1326+
Expect(os.Chdir(projectDir)).To(Succeed())
1327+
1328+
// Verify PROJECT file can be loaded
1329+
content, err := os.ReadFile("PROJECT")
1330+
Expect(err).NotTo(HaveOccurred())
1331+
Expect(string(content)).To(ContainSubstring("WebApp"))
1332+
Expect(string(content)).To(ContainSubstring("Job"))
1333+
1334+
// Create Generate
1335+
g := &Generate{
1336+
InputDir: "",
1337+
SkipGoVersionCheck: true,
1338+
}
1339+
1340+
// Validate should succeed
1341+
err = g.Validate()
1342+
Expect(err).NotTo(HaveOccurred())
1343+
})
1344+
})
11061345
})

0 commit comments

Comments
 (0)