Skip to content

Commit c4315d6

Browse files
[external-plugin] fix(ci): use Make targets for CLI install and expose plugin failures
Previously, CI used inlined shell commands that produced an invalid `kubebuilder` binary, causing cryptic errors like: `/home/runner/.../kubebuilder: line 1: '!<arch>'` This commit switches to official Makefile targets to correctly build and install the Kubebuilder CLI. It also improves local testability and ensures plugin command failures are no longer silently skipped. As a result, plugin tests now surface failures in `create api` and `create webhook`, which remain broken and will be addressed in follow-up work.
1 parent cd90bd8 commit c4315d6

File tree

14 files changed

+208
-106
lines changed

14 files changed

+208
-106
lines changed

.github/workflows/external-plugin.yml

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,57 +18,14 @@ jobs:
1818
runs-on: ubuntu-latest
1919
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
2020
steps:
21-
- name: Clone the code
21+
- name: Checkout repository
2222
uses: actions/checkout@v4
23-
with:
24-
fetch-depth: 1 # Minimal history to avoid .git permissions issues
2523

2624
- name: Setup Go
2725
uses: actions/setup-go@v5
2826
with:
29-
go-version-file: docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.mod
30-
31-
- name: Build Sample External Plugin
32-
working-directory: docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1
33-
run: |
34-
mkdir -p ./bin
35-
make build
36-
37-
- name: Move Plugin Binary to Plugin Path
38-
run: |
39-
# Define the plugin destination for Linux (XDG_CONFIG_HOME path)
40-
XDG_CONFIG_HOME="${HOME}/.config"
41-
PLUGIN_DEST="$XDG_CONFIG_HOME/kubebuilder/plugins/sampleexternalplugin/v1"
42-
43-
# Ensure destination exists and move the built binary
44-
mkdir -p "$PLUGIN_DEST"
45-
mv docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/bin/sampleexternalplugin "$PLUGIN_DEST/sampleexternalplugin"
46-
chmod +x "$PLUGIN_DEST/sampleexternalplugin" # Ensure the binary is executable
47-
48-
- name: Build Kubebuilder Binary and Setup Environment
49-
env:
50-
KUBEBUILDER_ASSETS: $GITHUB_WORKSPACE/bin
51-
run: |
52-
# Build Kubebuilder Binary
53-
export kb_root_dir=$(pwd)
54-
go build -o "${kb_root_dir}/bin/kubebuilder" ./cmd
55-
chmod +x "${kb_root_dir}/bin/kubebuilder" # Ensure kubebuilder binary is executable
56-
echo "${kb_root_dir}/bin" >> $GITHUB_PATH # Add to PATH
27+
go-version-file: go.mod
5728

58-
- name: Create Directory, Run Kubebuilder Commands, and Validate Results
59-
env:
60-
KUBEBUILDER_ASSETS: $GITHUB_WORKSPACE/bin
61-
run: |
62-
# Create a directory named testplugin for running kubebuilder commands
63-
mkdir testplugin
64-
cd testplugin
65-
66-
# Run Kubebuilder commands inside the testplugin directory
67-
kubebuilder init --plugins sampleexternalplugin/v1 --domain sample.domain.com
68-
kubebuilder create api --plugins sampleexternalplugin/v1 --number=2 --group=example --version=v1alpha1 --kind=ExampleKind
69-
kubebuilder create webhook --plugins sampleexternalplugin/v1 --hooked --group=example --version=v1alpha1 --kind=ExampleKind
29+
- name: Run tests
30+
run: make test-external-plugin
7031

71-
# Validate generated file contents
72-
grep "DOMAIN: sample.domain.com" ./initFile.txt || exit 1
73-
grep "NUMBER: 2" ./apiFile.txt || exit 1
74-
grep "HOOKED!" ./webhookFile.txt || exit 1

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,11 @@ test-book: ## Run the cronjob tutorial's unit tests to make sure we don't break
184184
test-license: ## Run the license check
185185
./test/check-license.sh
186186

187+
.PHONY: test-external-plugin
188+
test-external-plugin: install ## Run tests for external plugin
189+
make -C docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1 install
190+
make -C docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1 test-plugin
191+
187192
.PHONY: test-spaces
188193
test-spaces: ## Run the trailing spaces check
189194
./test/check_spaces.sh

docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/Makefile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ help: ## Display this help.
3535

3636
##@ Development
3737

38+
.PHONY: tidy
39+
tidy: ## Run go mod tidy against code.
40+
go mod tidy
41+
3842
.PHONY: fmt
3943
fmt: ## Run go fmt against code.
4044
go fmt ./...
@@ -46,5 +50,16 @@ vet: ## Run go vet against code.
4650
##@ Build
4751

4852
.PHONY: build
49-
build: fmt vet ## Build manager binary.
53+
build: tidy fmt vet ## Build manager binary.
5054
go build -o ./bin/sampleexternalplugin
55+
56+
.PHONY: install
57+
install: build ## Build and install the binary with the current source code. Use it to test your changes locally.
58+
rm -f $(GOBIN)/sampleexternalplugin
59+
cp ./bin/sampleexternalplugin $(GOBIN)/sampleexternalplugin
60+
# Make the binary discoverable for kubebuilder
61+
./install.sh
62+
63+
.PHONY: test-plugin
64+
test-plugin: ## Run the plugin test.
65+
./test/test.sh

docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/cmd/flags.go

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,8 @@ limitations under the License.
1616
package cmd
1717

1818
import (
19-
"fmt"
20-
2119
"v1/scaffolds"
2220

23-
"github.com/spf13/pflag"
2421
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
2522
)
2623

@@ -39,40 +36,17 @@ func flagsCmd(pr *external.PluginRequest) external.PluginResponse {
3936
Flags: []external.Flag{},
4037
}
4138

42-
// Here is an example of parsing multiple flags from a Kubebuilder external plugin request
43-
flagsToParse := pflag.NewFlagSet("flagsFlags", pflag.ContinueOnError)
44-
flagsToParse.Bool("init", false, "sets the init flag to true")
45-
flagsToParse.Bool("api", false, "sets the api flag to true")
46-
flagsToParse.Bool("webhook", false, "sets the webhook flag to true")
47-
48-
if err := flagsToParse.Parse(pr.Args); err != nil {
49-
pluginResponse.Error = true
50-
pluginResponse.ErrorMsgs = []string{
51-
fmt.Sprintf("failed to parse flags: %s", err.Error()),
52-
}
53-
return pluginResponse
54-
}
55-
56-
initFlag, _ := flagsToParse.GetBool("init")
57-
apiFlag, _ := flagsToParse.GetBool("api")
58-
webhookFlag, _ := flagsToParse.GetBool("webhook")
59-
60-
// The Phase 2 Plugins implementation will only ever pass a single boolean flag
61-
// argument in the JSON request `args` field. The flag will be `--init` if it is
62-
// attempting to get the flags for the `init` subcommand, `--api` for `create api`,
63-
// `--webhook` for `create webhook`, and `--edit` for `edit`
64-
if initFlag {
65-
// Add a flag to the JSON response `flags` field that Kubebuilder reads
66-
// to ensure it binds to the flags given in the response.
39+
switch pr.Command {
40+
case "init":
6741
pluginResponse.Flags = scaffolds.InitFlags
68-
} else if apiFlag {
42+
case "create api":
6943
pluginResponse.Flags = scaffolds.ApiFlags
70-
} else if webhookFlag {
44+
case "create webhook":
7145
pluginResponse.Flags = scaffolds.WebhookFlags
72-
} else {
46+
default:
7347
pluginResponse.Error = true
7448
pluginResponse.ErrorMsgs = []string{
75-
"unrecognized flag",
49+
"unrecognized command: " + pr.Command,
7650
}
7751
}
7852

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module v1
22

3-
go 1.23.0
3+
go 1.24.0
44

55
require (
66
github.com/spf13/pflag v1.0.6
@@ -11,7 +11,7 @@ require (
1111
github.com/gobuffalo/flect v1.0.3 // indirect
1212
github.com/spf13/afero v1.14.0 // indirect
1313
golang.org/x/mod v0.24.0 // indirect
14-
golang.org/x/sync v0.12.0 // indirect
15-
golang.org/x/text v0.23.0 // indirect
16-
golang.org/x/tools v0.31.0 // indirect
14+
golang.org/x/sync v0.14.0 // indirect
15+
golang.org/x/text v0.25.0 // indirect
16+
golang.org/x/tools v0.33.0 // indirect
1717
)

docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
3333
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
3434
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
3535
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
36-
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
37-
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
38-
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
39-
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
40-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
41-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
42-
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
43-
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
44-
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
45-
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
36+
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
37+
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
38+
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
39+
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
40+
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
41+
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
42+
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
43+
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
44+
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
45+
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
4646
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4747
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
4848
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/bin/bash
2+
3+
# Copyright 2021 The Kubernetes Authors.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
PLUGIN_NAME="sampleexternalplugin"
18+
PLUGIN_VERSION="v1"
19+
PLUGIN_BINARY="./bin/${PLUGIN_NAME}"
20+
21+
if [[ ! -f "${PLUGIN_BINARY}" ]]; then
22+
echo "Plugin binary not found at ${PLUGIN_BINARY}"
23+
echo "Make sure you run: make build"
24+
exit 1
25+
fi
26+
27+
# Detect OS and set plugin destination path
28+
if [[ "$OSTYPE" == "darwin"* ]]; then
29+
PLUGIN_DEST="$HOME/Library/Application Support/kubebuilder/plugins/${PLUGIN_NAME}/${PLUGIN_VERSION}"
30+
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
31+
PLUGIN_DEST="$HOME/.config/kubebuilder/plugins/${PLUGIN_NAME}/${PLUGIN_VERSION}"
32+
else
33+
echo "Unsupported OS: $OSTYPE"
34+
exit 1
35+
fi
36+
37+
mkdir -p "${PLUGIN_DEST}"
38+
39+
cp "${PLUGIN_BINARY}" "${PLUGIN_DEST}/${PLUGIN_NAME}"
40+
chmod +x "${PLUGIN_DEST}/${PLUGIN_NAME}"
41+
42+
echo "Plugin installed at:"
43+
echo "${PLUGIN_DEST}/${PLUGIN_NAME}"

docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ limitations under the License.
1616

1717
package main
1818

19-
import "v1/cmd"
19+
import (
20+
"v1/cmd"
21+
)
2022

2123
func main() {
2224
cmd.Run()

docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/scaffolds/api.go

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,34 @@ var ApiFlags = []external.Flag{
3232
Type: "int",
3333
Usage: "set a number to be added to the scaffolded apiFile.txt",
3434
},
35+
{
36+
Name: "group",
37+
Default: "",
38+
Type: "string",
39+
Usage: "API group name (e.g., 'example')",
40+
},
41+
{
42+
Name: "version",
43+
Default: "",
44+
Type: "string",
45+
Usage: "API version (e.g., 'v1alpha1')",
46+
},
47+
{
48+
Name: "kind",
49+
Default: "",
50+
Type: "string",
51+
Usage: "API kind (e.g., 'ExampleKind')",
52+
},
3553
}
3654

3755
var ApiMeta = plugin.SubcommandMetadata{
3856
Description: "The `create api` subcommand of the sampleexternalplugin is meant to create an api for a project via Kubebuilder. It scaffolds a single file: `apiFile.txt`",
3957
Examples: `
4058
Scaffold with the defaults:
41-
$ kubebuilder create api --plugins sampleexternalplugin/v1
59+
$ kubebuilder create api --plugins sampleexternalplugin/v1 --group samplegroup --version v1 --kind SampleKind
4260
4361
Scaffold with a specific number in the apiFile.txt file:
44-
$ kubebuilder create api --plugins sampleexternalplugin/v1 --number 2
62+
$ kubebuilder create api --plugins sampleexternalplugin/v1 --number 2 --group samplegroup --version v1 --kind SampleKind
4563
`,
4664
}
4765

@@ -53,25 +71,42 @@ func ApiCmd(pr *external.PluginRequest) external.PluginResponse {
5371
Universe: pr.Universe,
5472
}
5573

56-
// Here is an example of parsing a flag from a Kubebuilder external plugin request
5774
flags := pflag.NewFlagSet("apiFlags", pflag.ContinueOnError)
5875
flags.Int("number", 1, "set a number to be added in the scaffolded apiFile.txt")
76+
flags.String("group", "", "API group name")
77+
flags.String("version", "", "API version")
78+
flags.String("kind", "", "API kind")
79+
5980
if err := flags.Parse(pr.Args); err != nil {
6081
pluginResponse.Error = true
6182
pluginResponse.ErrorMsgs = []string{
6283
fmt.Sprintf("failed to parse flags: %s", err.Error()),
6384
}
6485
return pluginResponse
6586
}
87+
6688
number, _ := flags.GetInt("number")
89+
group, _ := flags.GetString("group")
90+
version, _ := flags.GetString("version")
91+
kind, _ := flags.GetString("kind")
6792

68-
apiFile := api.NewApiFile(api.WithNumber(number))
93+
// Validate GVK inputs
94+
if group == "" || version == "" || kind == "" {
95+
pluginResponse.Error = true
96+
pluginResponse.ErrorMsgs = []string{
97+
"--group, --version, and --kind are required flags",
98+
}
99+
return pluginResponse
100+
}
69101

70-
// Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin.
71-
// This universe is a key:value mapping of filename:contents. Here we are adding the file
72-
// "apiFile.txt" to the universe with some content. When this is returned Kubebuilder will
73-
// take all values within the "universe" and write them to the user's filesystem.
74-
pluginResponse.Universe[apiFile.Name] = apiFile.Contents
102+
// Scaffold API file using all values
103+
apiFile := api.NewApiFile(
104+
api.WithNumber(number),
105+
api.WithGroup(group),
106+
api.WithVersion(version),
107+
api.WithKind(kind),
108+
)
75109

110+
pluginResponse.Universe[apiFile.Name] = apiFile.Contents
76111
return pluginResponse
77112
}

0 commit comments

Comments
 (0)