Skip to content

Commit cd97195

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.
1 parent cd90bd8 commit cd97195

File tree

14 files changed

+207
-102
lines changed

14 files changed

+207
-102
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 & 5 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,46 @@ 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+
}
101+
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+
)
69109

70110
// Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin.
71111
// This universe is a key:value mapping of filename:contents. Here we are adding the file
72112
// "apiFile.txt" to the universe with some content. When this is returned Kubebuilder will
73113
// take all values within the "universe" and write them to the user's filesystem.
74114
pluginResponse.Universe[apiFile.Name] = apiFile.Contents
75-
76115
return pluginResponse
77116
}

0 commit comments

Comments
 (0)