Skip to content

Commit bdcf67b

Browse files
authored
Merge pull request #8957 from alexander-demicev/clusterctlplugin
✨ Introduce possibility to create clusterctl plugins
2 parents 03f0752 + de26976 commit bdcf67b

File tree

5 files changed

+160
-0
lines changed

5 files changed

+160
-0
lines changed

cmd/clusterctl/cmd/root.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/adrg/xdg"
2929
"github.com/pkg/errors"
3030
"github.com/spf13/cobra"
31+
kubectlcmd "k8s.io/kubectl/pkg/cmd"
3132

3233
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
3334
logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
@@ -141,6 +142,8 @@ func init() {
141142
RootCmd.SetCompletionCommandGroupID(groupOther)
142143

143144
cobra.OnInitialize(initConfig, registerCompletionFuncForCommonFlags)
145+
146+
handlePlugins()
144147
}
145148

146149
func initConfig() {
@@ -181,6 +184,39 @@ func registerCompletionFuncForCommonFlags() {
181184
})
182185
}
183186

187+
func handlePlugins() {
188+
args := os.Args
189+
pluginHandler := kubectlcmd.NewDefaultPluginHandler([]string{"clusterctl"})
190+
if len(args) > 1 {
191+
cmdPathPieces := args[1:]
192+
193+
// only look for suitable extension executables if
194+
// the specified command does not already exist
195+
if _, _, err := RootCmd.Find(cmdPathPieces); err != nil {
196+
// Also check the commands that will be added by Cobra.
197+
// These commands are only added once rootCmd.Execute() is called, so we
198+
// need to check them explicitly here.
199+
var cmdName string // first "non-flag" arguments
200+
for _, arg := range cmdPathPieces {
201+
if !strings.HasPrefix(arg, "-") {
202+
cmdName = arg
203+
break
204+
}
205+
}
206+
207+
switch cmdName {
208+
case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
209+
// Don't search for a plugin
210+
default:
211+
if err := kubectlcmd.HandlePluginCommand(pluginHandler, cmdPathPieces, false); err != nil {
212+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
213+
os.Exit(1)
214+
}
215+
}
216+
}
217+
}
218+
}
219+
184220
const indentation = ` `
185221

186222
// LongDesc normalizes a command's long description to follow the conventions.

docs/book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
- [clusterctl Configuration](clusterctl/configuration.md)
6060
- [clusterctl Provider Contract](clusterctl/provider-contract.md)
6161
- [clusterctl for Developers](clusterctl/developers.md)
62+
- [clusterctl Extensions with Plugins](clusterctl/plugins.md)
6263
- [Developer Guide](./developer/guide.md)
6364
- [Repository Layout](./developer/repository-layout.md)
6465
- [Rapid iterative development with Tilt](./developer/tilt.md)

docs/book/src/clusterctl/plugins.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# clusterctl Extensions with Plugins
2+
3+
You can extend `clusterctl` with plugins, similar to `kubectl`. Please refer to the [kubectl plugin documentation](https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/) for more information,
4+
as `clusterctl` plugins are implemented in the same way, with the exception of plugin distribution.
5+
6+
## Installing clusterctl plugins
7+
8+
To install a clusterctl plugin, place the plugin's executable file in any location on your `PATH`.
9+
10+
## Writing clusterctl plugins
11+
12+
No plugin installation or pre-loading is required. Plugin executables inherit the environment from the `clusterctl` binary. A plugin determines the command it implements based on its name.
13+
For example, a plugin named `clusterctl-foo` provides the `clusterctl` foo command. The plugin executable should be installed in your `PATH`.
14+
15+
Example plugin
16+
17+
```bash
18+
#!/bin/bash
19+
20+
# optional argument handling
21+
if [[ "$1" == "version" ]]
22+
then
23+
echo "1.0.0"
24+
exit 0
25+
fi
26+
27+
# optional argument handling
28+
if [[ "$1" == "example-env-var" ]]
29+
then
30+
echo "$EXAMPLE_ENV_VAR"
31+
exit 0
32+
fi
33+
34+
echo "I am a plugin named clusterctl-foo"
35+
```
36+
37+
### Using a plugin
38+
To use a plugin, make the plugin executable:
39+
40+
```bash
41+
sudo chmod +x ./clusterctl-foo
42+
```
43+
44+
and place it anywhere in your `PATH`:
45+
46+
```bash
47+
sudo mv ./clusterctl-foo /usr/local/bin
48+
```
49+
50+
You may now invoke your plugin as a `clusterctl` command:
51+
52+
```bash
53+
clusterctl foo
54+
```
55+
56+
```
57+
I am a plugin named clusterctl-foo
58+
```
59+
60+
All args and flags are passed as-is to the executable:
61+
```bash
62+
clusterctl foo version
63+
```
64+
65+
```
66+
1.0.0
67+
```
68+
69+
All environment variables are also passed as-is to the executable:
70+
71+
```bash
72+
export EXAMPLE_ENV_VAR=example-value
73+
clusterctl foo example-env-var
74+
```
75+
76+
```
77+
example-value
78+
```
79+
80+
```bash
81+
EXAMPLE_ENV_VAR=another-example-value clusterctl foo example-env-var
82+
```
83+
84+
```
85+
another-example-value
86+
```
87+
88+
Additionally, the first argument that is passed to a plugin will always be the full path to the location where it was invoked ($0 would equal /usr/local/bin/clusterctl-foo in the example above).
89+
90+
## Naming a plugin
91+
92+
A plugin determines the command path it implements based on its filename. Each sub-command in the path is separated by a dash (-). For example, a plugin for the command `clusterctl foo bar baz` would have the filename `clusterctl-foo-bar-baz`.

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,21 @@ require (
145145

146146
require (
147147
github.com/blang/semver/v4 v4.0.0 // indirect
148+
github.com/daviddengcn/go-colortext v1.0.0 // indirect
148149
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
150+
github.com/fatih/camelcase v1.0.0 // indirect
151+
github.com/fvbommel/sortorder v1.0.1 // indirect
149152
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
150153
github.com/google/gnostic v0.6.9 // indirect
151154
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
155+
github.com/jonboulle/clockwork v0.2.2 // indirect
156+
github.com/lithammer/dedent v1.1.0 // indirect
152157
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
158+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
153159
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
154160
github.com/russross/blackfriday/v2 v2.1.0 // indirect
155161
golang.org/x/tools v0.9.3 // indirect
162+
k8s.io/component-helpers v0.27.2 // indirect
163+
k8s.io/metrics v0.27.2 // indirect
164+
sigs.k8s.io/kustomize/kustomize/v5 v5.0.1 // indirect
156165
)

go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
120120
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
121121
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
122122
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
123+
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
124+
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
123125
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
124126
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
125127
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
@@ -142,6 +144,8 @@ github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJ
142144
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
143145
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
144146
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
147+
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
148+
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
145149
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
146150
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
147151
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
@@ -155,6 +159,8 @@ github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0X
155159
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
156160
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
157161
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
162+
github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE=
163+
github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
158164
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
159165
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
160166
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
@@ -219,6 +225,11 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
219225
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
220226
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
221227
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
228+
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
229+
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
230+
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
231+
github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ=
232+
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
222233
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
223234
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
224235
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
@@ -313,6 +324,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
313324
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
314325
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
315326
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
327+
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
328+
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
316329
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
317330
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
318331
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -338,6 +351,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
338351
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
339352
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
340353
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
354+
github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
341355
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
342356
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
343357
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -391,6 +405,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
391405
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
392406
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
393407
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
408+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
409+
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
394410
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
395411
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
396412
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
@@ -957,12 +973,16 @@ k8s.io/cluster-bootstrap v0.27.2 h1:OL3onrOwrUD7NQxBUqQwTl1Uu2GQKCkw9BMHpc4PbiA=
957973
k8s.io/cluster-bootstrap v0.27.2/go.mod h1:b++PF0mjUOiTKdPQFlDw7p4V2VquANZ8SfhAwzxZJFM=
958974
k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo=
959975
k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo=
976+
k8s.io/component-helpers v0.27.2 h1:i9TgWJ6TH8lQ9x4ExHOwhVitrRpBOr7Wn8aZLbBWxkc=
977+
k8s.io/component-helpers v0.27.2/go.mod h1:NwcpSKo1xzXtUtrUjj5NTSVWex84UPua/z0PYDcCzNo=
960978
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
961979
k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
962980
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg=
963981
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg=
964982
k8s.io/kubectl v0.27.2 h1:sSBM2j94MHBFRWfHIWtEXWCicViQzZsb177rNsKBhZg=
965983
k8s.io/kubectl v0.27.2/go.mod h1:GCOODtxPcrjh+EC611MqREkU8RjYBh10ldQCQ6zpFKw=
984+
k8s.io/metrics v0.27.2 h1:TD6z3dhhN9bgg5YkbTh72bPiC1BsxipBLPBWyC3VQAU=
985+
k8s.io/metrics v0.27.2/go.mod h1:v3OT7U0DBvoAzWVzGZWQhdV4qsRJWchzs/LeVN8bhW4=
966986
k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY=
967987
k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
968988
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
@@ -974,6 +994,8 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm
974994
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
975995
sigs.k8s.io/kustomize/api v0.13.2 h1:kejWfLeJhUsTGioDoFNJET5LQe/ajzXhJGYoU+pJsiA=
976996
sigs.k8s.io/kustomize/api v0.13.2/go.mod h1:DUp325VVMFVcQSq+ZxyDisA8wtldwHxLZbr1g94UHsw=
997+
sigs.k8s.io/kustomize/kustomize/v5 v5.0.1 h1:HWXbyKDNwGqol+s/sMNr/vnfNME/EoMdEraP4ZkUQek=
998+
sigs.k8s.io/kustomize/kustomize/v5 v5.0.1/go.mod h1:Q8o+soB41Pn1y26eXzG9cniuECDpTJe2eKOA1fENCU8=
977999
sigs.k8s.io/kustomize/kyaml v0.14.1 h1:c8iibius7l24G2wVAGZn/Va2wNys03GXLjYVIcFVxKA=
9781000
sigs.k8s.io/kustomize/kyaml v0.14.1/go.mod h1:AN1/IpawKilWD7V+YvQwRGUvuUOOWpjsHu6uHwonSF4=
9791001
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE=

0 commit comments

Comments
 (0)