1
15
16 package plugin
17
18 import (
19 "fmt"
20 "os"
21 "path/filepath"
22 "regexp"
23 "runtime"
24 "strings"
25 "unicode"
26
27 "github.com/pkg/errors"
28 "sigs.k8s.io/yaml"
29
30 "helm.sh/helm/v3/pkg/cli"
31 )
32
33 const PluginFileName = "plugin.yaml"
34
35
36
37 type Downloaders struct {
38
39 Protocols []string `json:"protocols"`
40
41
42 Command string `json:"command"`
43 }
44
45
46 type PlatformCommand struct {
47 OperatingSystem string `json:"os"`
48 Architecture string `json:"arch"`
49 Command string `json:"command"`
50 }
51
52
53
54
55 type Metadata struct {
56
57 Name string `json:"name"`
58
59
60 Version string `json:"version"`
61
62
63 Usage string `json:"usage"`
64
65
66 Description string `json:"description"`
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 PlatformCommand []PlatformCommand `json:"platformCommand"`
84 Command string `json:"command"`
85
86
87
88
89
90
91 IgnoreFlags bool `json:"ignoreFlags"`
92
93
94 Hooks Hooks
95
96
97
98 Downloaders []Downloaders `json:"downloaders"`
99
100
101
102
103
104 UseTunnelDeprecated bool `json:"useTunnel,omitempty"`
105 }
106
107
108 type Plugin struct {
109
110 Metadata *Metadata
111
112 Dir string
113 }
114
115
116
117
118
119 func getPlatformCommand(cmds []PlatformCommand) []string {
120 var command []string
121 eq := strings.EqualFold
122 for _, c := range cmds {
123 if eq(c.OperatingSystem, runtime.GOOS) {
124 command = strings.Split(c.Command, " ")
125 }
126 if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) {
127 return strings.Split(c.Command, " ")
128 }
129 }
130 return command
131 }
132
133
134
135
136
137
138
139
140
141
142
143
144 func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) {
145 var parts []string
146 platCmdLen := len(p.Metadata.PlatformCommand)
147 if platCmdLen > 0 {
148 parts = getPlatformCommand(p.Metadata.PlatformCommand)
149 }
150 if platCmdLen == 0 || parts == nil {
151 parts = strings.Split(p.Metadata.Command, " ")
152 }
153 if len(parts) == 0 || parts[0] == "" {
154 return "", nil, fmt.Errorf("no plugin command is applicable")
155 }
156
157 main := os.ExpandEnv(parts[0])
158 baseArgs := []string{}
159 if len(parts) > 1 {
160 for _, cmdpart := range parts[1:] {
161 cmdexp := os.ExpandEnv(cmdpart)
162 baseArgs = append(baseArgs, cmdexp)
163 }
164 }
165 if !p.Metadata.IgnoreFlags {
166 baseArgs = append(baseArgs, extraArgs...)
167 }
168 return main, baseArgs, nil
169 }
170
171
172
173
174 var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")
175
176
177 func validatePluginData(plug *Plugin, filepath string) error {
178
179 if plug.Metadata == nil {
180 plug.Metadata = &Metadata{}
181 }
182 if !validPluginName.MatchString(plug.Metadata.Name) {
183 return fmt.Errorf("invalid plugin name at %q", filepath)
184 }
185 plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage)
186
187
188 return nil
189 }
190
191
192 func sanitizeString(str string) string {
193 return strings.Map(func(r rune) rune {
194 if unicode.IsSpace(r) {
195 return ' '
196 }
197 if unicode.IsPrint(r) {
198 return r
199 }
200 return -1
201 }, str)
202 }
203
204 func detectDuplicates(plugs []*Plugin) error {
205 names := map[string]string{}
206
207 for _, plug := range plugs {
208 if oldpath, ok := names[plug.Metadata.Name]; ok {
209 return fmt.Errorf(
210 "two plugins claim the name %q at %q and %q",
211 plug.Metadata.Name,
212 oldpath,
213 plug.Dir,
214 )
215 }
216 names[plug.Metadata.Name] = plug.Dir
217 }
218
219 return nil
220 }
221
222
223 func LoadDir(dirname string) (*Plugin, error) {
224 pluginfile := filepath.Join(dirname, PluginFileName)
225 data, err := os.ReadFile(pluginfile)
226 if err != nil {
227 return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile)
228 }
229
230 plug := &Plugin{Dir: dirname}
231 if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil {
232 return nil, errors.Wrapf(err, "failed to load plugin at %q", pluginfile)
233 }
234 return plug, validatePluginData(plug, pluginfile)
235 }
236
237
238
239
240 func LoadAll(basedir string) ([]*Plugin, error) {
241 plugins := []*Plugin{}
242
243 scanpath := filepath.Join(basedir, "*", PluginFileName)
244 matches, err := filepath.Glob(scanpath)
245 if err != nil {
246 return plugins, errors.Wrapf(err, "failed to find plugins in %q", scanpath)
247 }
248
249 if matches == nil {
250 return plugins, nil
251 }
252
253 for _, yaml := range matches {
254 dir := filepath.Dir(yaml)
255 p, err := LoadDir(dir)
256 if err != nil {
257 return plugins, err
258 }
259 plugins = append(plugins, p)
260 }
261 return plugins, detectDuplicates(plugins)
262 }
263
264
265 func FindPlugins(plugdirs string) ([]*Plugin, error) {
266 found := []*Plugin{}
267
268 for _, p := range filepath.SplitList(plugdirs) {
269 matches, err := LoadAll(p)
270 if err != nil {
271 return matches, err
272 }
273 found = append(found, matches...)
274 }
275 return found, nil
276 }
277
278
279
280
281 func SetupPluginEnv(settings *cli.EnvSettings, name, base string) {
282 env := settings.EnvVars()
283 env["HELM_PLUGIN_NAME"] = name
284 env["HELM_PLUGIN_DIR"] = base
285 for key, val := range env {
286 os.Setenv(key, val)
287 }
288 }
289
View as plain text