Skip to content

Commit 5ca0816

Browse files
committed
(phase 2 plugins): add simple external plugin to testdata
Signed-off-by: Bryce Palmer <[email protected]>
1 parent 60d0c6e commit 5ca0816

File tree

2 files changed

+332
-0
lines changed

2 files changed

+332
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module simple-external-plugin
2+
3+
go 1.18
4+
5+
require (
6+
github.com/spf13/pflag v1.0.5
7+
sigs.k8s.io/kubebuilder/v3 v3.5.1-0.20220707134334-33ad938a65be
8+
)
9+
10+
require (
11+
github.com/gobuffalo/flect v0.2.5 // indirect
12+
github.com/spf13/afero v1.6.0 // indirect
13+
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
14+
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
15+
golang.org/x/text v0.3.7 // indirect
16+
golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717 // indirect
17+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
18+
)
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
10+
"github.com/spf13/pflag"
11+
"sigs.k8s.io/kubebuilder/v3/pkg/plugin"
12+
"sigs.k8s.io/kubebuilder/v3/pkg/plugin/external"
13+
)
14+
15+
// TODO(everettraven): clean this up a bit and add detailed comments discussing how this works with the Phase 2 Plugins
16+
17+
func main() {
18+
// In this sample we will implement all the plugin operations in the run function
19+
run()
20+
}
21+
22+
// run is the function that handles all the logic for running the plugin
23+
func run() error {
24+
// Phase 2 Plugins makes requests to an external plugin by
25+
// writing to the STDIN buffer. This means that an external plugin
26+
// call will NOT include any arguments other than the program name
27+
// itself. In order to get the request JSON from Kubebuilder
28+
// we will need to read the input from STDIN
29+
reader := bufio.NewReader(os.Stdin)
30+
31+
input, err := io.ReadAll(reader)
32+
if err != nil {
33+
return fmt.Errorf("encountered error reading STDIN: %w", err)
34+
}
35+
36+
// Parse the JSON input from STDIN to a PluginRequest object.
37+
// Since the Phase 2 Plugin implementation was written in Go
38+
// there is already a Go API in place to represent these values.
39+
// Phase 2 Plugins can be written in any language, but you may
40+
// need to create some classes/interfaces to parse the JSON used
41+
// in the Phase 2 Plugins communication. More information on the
42+
// Phase 2 Plugin JSON schema can be found in the Phase 2 Plugins docs
43+
pluginRequest := &external.PluginRequest{}
44+
45+
err = json.Unmarshal(input, pluginRequest)
46+
if err != nil {
47+
return fmt.Errorf("encountered error unmarshaling STDIN: %w", err)
48+
}
49+
50+
var response external.PluginResponse
51+
52+
// Run logic depending on the command that is requested by Kubebuilder
53+
switch pluginRequest.Command {
54+
// the `init` subcommand is often used when initializing a new project
55+
case "init":
56+
response = initCmd(pluginRequest)
57+
// the `create api` subcommand is often used after initializing a project
58+
// with the `init` subcommand to create a controller and CRDs for a
59+
// provided group, version, and kind
60+
case "create api":
61+
response = apiCmd(pluginRequest)
62+
// the `create webhook` subcommand is often used after initializing a project
63+
// with the `init` subcommand to create a webhook for a provided
64+
// group, version, and kind
65+
case "create webhook":
66+
response = webhookCmd(pluginRequest)
67+
// the `flags` subcommand is used to customize the flags that
68+
// the Kubebuilder cli will bind for use with this plugin
69+
case "flags":
70+
response = flagsCmd(pluginRequest)
71+
// the `metadata` subcommand is used to customize the
72+
// plugin metadata (help message and examples) that are
73+
// shown to Kubebuilder CLI users.
74+
case "metadata":
75+
response = metadataCmd(pluginRequest)
76+
// Any errors should still be returned as part of the plugin's
77+
// JSON response. There is an `error` boolean field to signal to
78+
// Kubebuilder that the external plugin encountered an error.
79+
// There is also an `errorMsgs` string array field to provide all
80+
// error messages to Kubebuilder.
81+
default:
82+
response = external.PluginResponse{
83+
Error: true,
84+
ErrorMsgs: []string{
85+
"unknown subcommand:" + pluginRequest.Command,
86+
},
87+
}
88+
}
89+
90+
// The Phase 2 Plugins implementation will read the response
91+
// from a Phase 2 Plugin via STDOUT. For Kubebuilder to properly
92+
// read our response we need to create a valid JSON string and
93+
// write it to the STDOUT buffer.
94+
output, err := json.Marshal(response)
95+
if err != nil {
96+
return fmt.Errorf("encountered error marshaling output: %w | OUTPUT: %s", err, output)
97+
}
98+
99+
fmt.Printf("%s", output)
100+
101+
return nil
102+
}
103+
104+
// initCmd handles all the logic for the `init` subcommand of this sample external plugin
105+
func initCmd(pr *external.PluginRequest) external.PluginResponse {
106+
pluginResponse := external.PluginResponse{
107+
APIVersion: "v1alpha1",
108+
Command: "init",
109+
Universe: pr.Universe,
110+
}
111+
112+
// Here is an example of parsing a flag from a Kubebuilder external plugin request
113+
flags := pflag.NewFlagSet("initFlags", pflag.ContinueOnError)
114+
flags.String("domain", "example.domain.com", "sets the domain added in the scaffolded initFile.txt")
115+
flags.Parse(pr.Args)
116+
domain, _ := flags.GetString("domain")
117+
118+
// Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin.
119+
// This universe is a key:value mapping of filename:contents. Here we are adding the file
120+
// "initFile.txt" to the universe with some content. When this is returned Kubebuilder will
121+
// take all values within the "universe" and write them to the user's filesystem.
122+
pluginResponse.Universe["initFile.txt"] = fmt.Sprintf("A simple text file created with the `init` subcommand\nDOMAIN: %s", domain)
123+
124+
return pluginResponse
125+
}
126+
127+
// apiCmd handles all the logic for the `create api` subcommand of this sample external plugin
128+
func apiCmd(pr *external.PluginRequest) external.PluginResponse {
129+
pluginResponse := external.PluginResponse{
130+
APIVersion: "v1alpha1",
131+
Command: "create api",
132+
Universe: pr.Universe,
133+
}
134+
135+
// Here is an example of parsing a flag from a Kubebuilder external plugin request
136+
flags := pflag.NewFlagSet("apiFlags", pflag.ContinueOnError)
137+
flags.Int("number", 1, "set a number to be added in the scaffolded apiFile.txt")
138+
flags.Parse(pr.Args)
139+
number, _ := flags.GetInt("number")
140+
141+
// Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin.
142+
// This universe is a key:value mapping of filename:contents. Here we are adding the file
143+
// "apiFile.txt" to the universe with some content. When this is returned Kubebuilder will
144+
// take all values within the "universe" and write them to the user's filesystem.
145+
pluginResponse.Universe["apiFile.txt"] = fmt.Sprintf("A simple text file created with the `create api` subcommand\nNUMBER: %d", number)
146+
147+
return pluginResponse
148+
}
149+
150+
// webhookCmd handles all the logic for the `create webhook` subcommand of this sample external plugin
151+
func webhookCmd(pr *external.PluginRequest) external.PluginResponse {
152+
pluginResponse := external.PluginResponse{
153+
APIVersion: "v1alpha1",
154+
Command: "create webhook",
155+
Universe: pr.Universe,
156+
}
157+
158+
// Here is an example of parsing a flag from a Kubebuilder external plugin request
159+
flags := pflag.NewFlagSet("apiFlags", pflag.ContinueOnError)
160+
flags.Bool("hooked", false, "add the word `hooked` to the end of the scaffolded webhookFile.txt")
161+
flags.Parse(pr.Args)
162+
hooked, _ := flags.GetBool("hooked")
163+
164+
msg := "A simple text file created with the `create webhook` subcommand"
165+
if hooked {
166+
msg += "\nHOOKED!"
167+
}
168+
169+
// Phase 2 Plugins uses the concept of a "universe" to represent the filesystem for a plugin.
170+
// This universe is a key:value mapping of filename:contents. Here we are adding the file
171+
// "webhookFile.txt" to the universe with some content. When this is returned Kubebuilder will
172+
// take all values within the "universe" and write them to the user's filesystem.
173+
pluginResponse.Universe["webhookFile.txt"] = msg
174+
175+
return pluginResponse
176+
}
177+
178+
// flagsCmd handles all the logic for the `flags` subcommand of the sample external plugin.
179+
// In Kubebuilder's Phase 2 Plugins the `flags` subcommand is an optional subcommand for
180+
// external plugins to support. The `flags` subcommand allows for an external plugin
181+
// to provide Kubebuilder with a list of flags that the `init`, `create api`, `create webhook`,
182+
// and `edit` subcommands allow. This allows Kubebuilder to give an external plugin the ability
183+
// to feel like a native Kubebuilder plugin to a Kubebuilder user by only binding the supported
184+
// flags and failing early if an unknown flag is provided.
185+
func flagsCmd(pr *external.PluginRequest) external.PluginResponse {
186+
pluginResponse := external.PluginResponse{
187+
APIVersion: "v1alpha1",
188+
Command: "flags",
189+
Universe: pr.Universe,
190+
Flags: []external.Flag{},
191+
}
192+
193+
// Here is an example of parsing multiple flags from a Kubebuilder external plugin request
194+
flagsToParse := pflag.NewFlagSet("flagsFlags", pflag.ContinueOnError)
195+
flagsToParse.Bool("init", false, "sets the init flag to true")
196+
flagsToParse.Bool("api", false, "sets the api flag to true")
197+
flagsToParse.Bool("webhook", false, "sets the webhook flag to true")
198+
199+
flagsToParse.Parse(pr.Args)
200+
201+
initFlag, _ := flagsToParse.GetBool("init")
202+
apiFlag, _ := flagsToParse.GetBool("api")
203+
webhookFlag, _ := flagsToParse.GetBool("webhook")
204+
205+
// The Phase 2 Plugins implementation will only ever pass a single boolean flag
206+
// argument in the JSON request `args` field. The flag will be `--init` if it is
207+
// attempting to get the flags for the `init` subcommand, `--api` for `create api`,
208+
// `--webhook` for `create webhook`, and `--edit` for `edit`
209+
if initFlag {
210+
// Add a flag to the JSON response `flags` field that Kubebuilder reads
211+
// to ensure it binds to the flags given in the response.
212+
pluginResponse.Flags = append(pluginResponse.Flags, external.Flag{
213+
Name: "domain",
214+
Type: "string",
215+
Default: "example.domain.com",
216+
Usage: "sets the domain added in the scaffolded initFile.txt",
217+
})
218+
} else if apiFlag {
219+
pluginResponse.Flags = append(pluginResponse.Flags, external.Flag{
220+
Name: "number",
221+
Type: "int",
222+
Default: "1",
223+
Usage: "set a number to be added in the scaffolded apiFile.txt",
224+
})
225+
} else if webhookFlag {
226+
pluginResponse.Flags = append(pluginResponse.Flags, external.Flag{
227+
Name: "hooked",
228+
Type: "bool",
229+
Default: "false",
230+
Usage: "add the word `hooked` to the end of the scaffolded webhookFile.txt",
231+
})
232+
} else {
233+
pluginResponse.Error = true
234+
pluginResponse.ErrorMsgs = []string{
235+
"unrecognized flag",
236+
}
237+
}
238+
239+
return pluginResponse
240+
}
241+
242+
// metadataCmd handles all the logic for the `metadata` subcommand of the sample external plugin.
243+
// In Kubebuilder's Phase 2 Plugins the `metadata` subcommand is an optional subcommand for
244+
// external plugins to support. The `metadata` subcommand allows for an external plugin
245+
// to provide Kubebuilder with a description of the plugin and examples for each of the
246+
// `init`, `create api`, `create webhook`, and `edit` subcommands. This allows Kubebuilder
247+
// to provide users a native Kubebuilder plugin look and feel for an external plugin.
248+
func metadataCmd(pr *external.PluginRequest) external.PluginResponse {
249+
pluginResponse := external.PluginResponse{
250+
APIVersion: "v1alpha1",
251+
Command: "flags",
252+
Universe: pr.Universe,
253+
}
254+
255+
// Here is an example of parsing multiple flags from a Kubebuilder external plugin request
256+
flagsToParse := pflag.NewFlagSet("flagsFlags", pflag.ContinueOnError)
257+
flagsToParse.Bool("init", false, "sets the init flag to true")
258+
flagsToParse.Bool("api", false, "sets the api flag to true")
259+
flagsToParse.Bool("webhook", false, "sets the webhook flag to true")
260+
261+
flagsToParse.Parse(pr.Args)
262+
263+
initFlag, _ := flagsToParse.GetBool("init")
264+
apiFlag, _ := flagsToParse.GetBool("api")
265+
webhookFlag, _ := flagsToParse.GetBool("webhook")
266+
267+
// The Phase 2 Plugins implementation will only ever pass a single boolean flag
268+
// argument in the JSON request `args` field. The flag will be `--init` if it is
269+
// attempting to get the flags for the `init` subcommand, `--api` for `create api`,
270+
// `--webhook` for `create webhook`, and `--edit` for `edit`
271+
if initFlag {
272+
// Populate the JSON response `metadata` field with a description
273+
// and examples for the `init` subcommand
274+
pluginResponse.Metadata = plugin.SubcommandMetadata{
275+
Description: "The `init` subcommand of the sampleexternalplugin is meant to initialize a project via Kubebuilder. It scaffolds a single file: `initFile.txt`",
276+
Examples: `
277+
Scaffold with the defaults:
278+
$ kubebuilder init --plugins sampleexternalplugin/v1
279+
280+
Scaffold with a specific domain:
281+
$ kubebuilder init --plugins sampleexternalplugin/v1 --domain sample.domain.com
282+
`,
283+
}
284+
} else if apiFlag {
285+
pluginResponse.Metadata = plugin.SubcommandMetadata{
286+
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`",
287+
Examples: `
288+
Scaffold with the defaults:
289+
$ kubebuilder create api --plugins sampleexternalplugin/v1
290+
291+
Scaffold with a specific number in the apiFile.txt file:
292+
$ kubebuilder create api --plugins sampleexternalplugin/v1 --number 2
293+
`,
294+
}
295+
} else if webhookFlag {
296+
pluginResponse.Metadata = plugin.SubcommandMetadata{
297+
Description: "The `create webhook` subcommand of the sampleexternalplugin is meant to create a webhook for a project via Kubebuilder. It scaffolds a single file: `webhookFile.txt`",
298+
Examples: `
299+
Scaffold with the defaults:
300+
$ kubebuilder create webhook --plugins sampleexternalplugin/v1
301+
302+
Scaffold with the text "HOOKED!" in the webhookFile.txt file:
303+
$ kubebuilder create webhook --plugins sampleexternalplugin/v1 --hooked
304+
`,
305+
}
306+
} else {
307+
pluginResponse.Error = true
308+
pluginResponse.ErrorMsgs = []string{
309+
"unrecognized flag",
310+
}
311+
}
312+
313+
return pluginResponse
314+
}

0 commit comments

Comments
 (0)