Skip to content

Commit 912d117

Browse files
[external-plugin] fix(ci): enable plugin tests and expose broken API/webhook logic
Previously, CI scripts skipped failures in plugin command tests, giving the false impression of successful execution. This change ensures the test script exits on failure, validating plugin behavior as expected. As a result, we discovered that `create api` and `create webhook` currently fail due to incomplete or incorrect handling in the plugin implementation. These failures are now visible and will need to be addressed in follow-up
1 parent cd90bd8 commit 912d117

File tree

14 files changed

+175
-147
lines changed

14 files changed

+175
-147
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ 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 the license check
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+
192+
193+
187194
.PHONY: test-spaces
188195
test-spaces: ## Run the trailing spaces check
189196
./test/check_spaces.sh

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,14 @@ vet: ## Run go vet against code.
4848
.PHONY: build
4949
build: fmt vet ## Build manager binary.
5050
go build -o ./bin/sampleexternalplugin
51+
52+
.PHONY: install
53+
install: build ## Build and install the binary with the current source code. Use it to test your changes locally.
54+
rm -f $(GOBIN)/sampleexternalplugin
55+
cp ./bin/sampleexternalplugin $(GOBIN)/sampleexternalplugin
56+
# Make the binary discoverable for kubebuilder
57+
./install.sh
58+
59+
.PHONY: test-plugin
60+
test-plugin: ## Run the plugin test.
61+
./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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
PLUGIN_NAME="sampleexternalplugin"
5+
PLUGIN_VERSION="v1"
6+
PLUGIN_BINARY="./bin/${PLUGIN_NAME}"
7+
8+
if [[ ! -f "${PLUGIN_BINARY}" ]]; then
9+
echo "Plugin binary not found at ${PLUGIN_BINARY}"
10+
echo "Make sure you run: make build"
11+
exit 1
12+
fi
13+
14+
# Detect OS and set plugin destination path
15+
if [[ "$OSTYPE" == "darwin"* ]]; then
16+
PLUGIN_DEST="$HOME/Library/Application Support/kubebuilder/plugins/${PLUGIN_NAME}/${PLUGIN_VERSION}"
17+
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
18+
PLUGIN_DEST="$HOME/.config/kubebuilder/plugins/${PLUGIN_NAME}/${PLUGIN_VERSION}"
19+
else
20+
echo "Unsupported OS: $OSTYPE"
21+
exit 1
22+
fi
23+
24+
mkdir -p "${PLUGIN_DEST}"
25+
26+
cp "${PLUGIN_BINARY}" "${PLUGIN_DEST}/${PLUGIN_NAME}"
27+
chmod +x "${PLUGIN_DEST}/${PLUGIN_NAME}"
28+
29+
echo "Plugin installed at:"
30+
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()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
echo "[+] Setting up test environment..."
5+
6+
# Create a directory named testplugin for running kubebuilder commands
7+
mkdir -p testdata/testplugin
8+
cd testdata/testplugin
9+
rm -rf *
10+
11+
# Run Kubebuilder commands inside the testplugin directory
12+
kubebuilder init --plugins sampleexternalplugin/v1 --domain sample.domain.com
13+
# FIXME: The following commands are commented since they are not working well
14+
# as should be.
15+
#kubebuilder create api --plugins sampleexternalplugin/v1 --number 2
16+
#kubebuilder create webhook --plugins sampleexternalplugin/v1 --hooked
17+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Code generated by tool. DO NOT EDIT.
2+
# This file is used to track the info used to scaffold your project
3+
# and allow the plugins properly work.
4+
# More info: https://book.kubebuilder.io/reference/project-config.html
5+
cliVersion: 4.6.0
6+
layout:
7+
- sampleexternalplugin/v1
8+
version: "3"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
A simple text file created with the `init` subcommand
2+
DOMAIN: sample.domain.com

pkg/plugins/external/external_test.go

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,7 @@ func (m *mockValidFlagOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, e
8484
Universe: nil,
8585
Flags: getFlags(),
8686
}
87-
marshaledResponse, err := json.Marshal(response)
88-
if err != nil {
89-
return nil, fmt.Errorf("error marshalling response: %w", err)
90-
}
91-
92-
return marshaledResponse, nil
87+
return json.Marshal(response)
9388
}
9489

9590
type mockValidMEOutputGetter struct{}
@@ -102,12 +97,7 @@ func (m *mockValidMEOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, err
10297
Metadata: getMetadata(),
10398
}
10499

105-
marshaledResponse, err := json.Marshal(response)
106-
if err != nil {
107-
return nil, fmt.Errorf("error marshalling response: %w", err)
108-
}
109-
110-
return marshaledResponse, nil
100+
return json.Marshal(response)
111101
}
112102

113103
const (
@@ -297,11 +287,10 @@ var _ = Describe("Run external plugin using Scaffold", func() {
297287
flagset *pflag.FlagSet
298288

299289
// Make an array of flags to represent the ones that should be returned in these tests
300-
flags []external.Flag
290+
flags = getFlags()
301291

302292
checkFlagset func()
303293
)
304-
305294
BeforeEach(func() {
306295
outputGetter = &mockValidFlagOutputGetter{}
307296
currentDirGetter = &mockValidOsWdGetter{}
@@ -310,8 +299,6 @@ var _ = Describe("Run external plugin using Scaffold", func() {
310299
args = []string{"--captain", "black-beard", "--sail"}
311300
flagset = pflag.NewFlagSet("test", pflag.ContinueOnError)
312301

313-
flags = getFlags()
314-
315302
checkFlagset = func() {
316303
Expect(flagset.HasFlags()).To(BeTrue())
317304

@@ -382,7 +369,6 @@ var _ = Describe("Run external plugin using Scaffold", func() {
382369
usage string
383370
checkFlagset func()
384371
)
385-
386372
BeforeEach(func() {
387373
outputGetter = &mockInValidOutputGetter{}
388374
currentDirGetter = &mockValidOsWdGetter{}
@@ -477,15 +463,7 @@ var _ = Describe("Run external plugin using Scaffold", func() {
477463

478464
Context("Flag Parsing Helper Functions", func() {
479465
var (
480-
fs *pflag.FlagSet
481-
args []string
482-
forbidden []string
483-
flags []external.Flag
484-
argFilters []argFilterFunc
485-
externalFlagFilters []externalFlagFilterFunc
486-
)
487-
488-
BeforeEach(func() {
466+
fs *pflag.FlagSet
489467
args = []string{
490468
"--domain", "something.com",
491469
"--boolean",
@@ -498,7 +476,12 @@ var _ = Describe("Run external plugin using Scaffold", func() {
498476
forbidden = []string{
499477
"help", "group", "kind", "version",
500478
}
479+
flags []external.Flag
480+
argFilters []argFilterFunc
481+
externalFlagFilters []externalFlagFilterFunc
482+
)
501483

484+
BeforeEach(func() {
502485
fs = pflag.NewFlagSet("test", pflag.ContinueOnError)
503486

504487
flagsToAppend := getFlags()
@@ -576,7 +559,6 @@ var _ = Describe("Run external plugin using Scaffold", func() {
576559
metadata *plugin.SubcommandMetadata
577560
checkMetadata func()
578561
)
579-
580562
BeforeEach(func() {
581563
outputGetter = &mockValidMEOutputGetter{}
582564
currentDirGetter = &mockValidOsWdGetter{}
@@ -641,7 +623,6 @@ var _ = Describe("Run external plugin using Scaffold", func() {
641623
metadata *plugin.SubcommandMetadata
642624
checkMetadata func()
643625
)
644-
645626
BeforeEach(func() {
646627
outputGetter = &mockInValidOutputGetter{}
647628
currentDirGetter = &mockValidOsWdGetter{}

0 commit comments

Comments
 (0)