File: loader.go

package info (click to toggle)
golang-k8s-sigs-kustomize-api 0.20.1%2Bds-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,768 kB
  • sloc: makefile: 206; sh: 67
file content (290 lines) | stat: -rw-r--r-- 9,178 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package loader

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kustomize/api/ifc"
	"sigs.k8s.io/kustomize/api/internal/plugins/builtinhelpers"
	"sigs.k8s.io/kustomize/api/internal/plugins/execplugin"
	"sigs.k8s.io/kustomize/api/internal/plugins/fnplugin"
	"sigs.k8s.io/kustomize/api/konfig"
	"sigs.k8s.io/kustomize/api/resmap"
	"sigs.k8s.io/kustomize/api/resource"
	"sigs.k8s.io/kustomize/api/types"
	"sigs.k8s.io/kustomize/kyaml/errors"
	"sigs.k8s.io/kustomize/kyaml/filesys"
	"sigs.k8s.io/kustomize/kyaml/resid"
)

// Loader loads plugins using a file loader (a different loader).
type Loader struct {
	pc *types.PluginConfig
	rf *resmap.Factory
	fs filesys.FileSystem

	// absolutePluginHome caches the location of a valid plugin root directory.
	// It should only be set once the directory's existence has been confirmed.
	absolutePluginHome string
}

func NewLoader(
	pc *types.PluginConfig, rf *resmap.Factory, fs filesys.FileSystem,
) *Loader {
	return &Loader{pc: pc, rf: rf, fs: fs}
}

// LoaderWithWorkingDir returns loader after setting its working directory.
// NOTE: This is not really a new loader since some of the Loader struct fields are pointers.
func (l *Loader) LoaderWithWorkingDir(wd string) *Loader {
	lpc := &types.PluginConfig{
		PluginRestrictions: l.pc.PluginRestrictions,
		BpLoadingOptions:   l.pc.BpLoadingOptions,
		FnpLoadingOptions:  l.pc.FnpLoadingOptions,
		HelmConfig:         l.pc.HelmConfig,
	}
	lpc.FnpLoadingOptions.WorkingDir = wd
	return &Loader{pc: lpc, rf: l.rf, fs: l.fs}
}

// Config provides the global (not plugin specific) PluginConfig data.
func (l *Loader) Config() *types.PluginConfig {
	return l.pc
}

func (l *Loader) LoadGenerators(
	ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) (
	result []*resmap.GeneratorWithProperties, err error,
) {
	for _, res := range rm.Resources() {
		g, err := l.LoadGenerator(ldr, v, res)
		if err != nil {
			return nil, fmt.Errorf("failed to load generator: %w", err)
		}
		generatorOrigin, err := resource.OriginFromCustomPlugin(res)
		if err != nil {
			return nil, fmt.Errorf("failed to get origin from CustomPlugin: %w", err)
		}
		result = append(result, &resmap.GeneratorWithProperties{Generator: g, Origin: generatorOrigin})
	}
	return result, nil
}

func (l *Loader) LoadGenerator(
	ldr ifc.Loader, v ifc.Validator, res *resource.Resource,
) (resmap.Generator, error) {
	c, err := l.loadAndConfigurePlugin(ldr, v, res)
	if err != nil {
		return nil, err
	}
	g, ok := c.(resmap.Generator)
	if !ok {
		return nil, fmt.Errorf("plugin %s not a generator", res.OrgId())
	}
	return g, nil
}

func (l *Loader) LoadTransformers(
	ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap,
) ([]*resmap.TransformerWithProperties, error) {
	var result []*resmap.TransformerWithProperties
	for _, res := range rm.Resources() {
		t, err := l.LoadTransformer(ldr, v, res)
		if err != nil {
			return nil, err
		}
		transformerOrigin, err := resource.OriginFromCustomPlugin(res)
		if err != nil {
			return nil, err
		}
		result = append(result, &resmap.TransformerWithProperties{Transformer: t, Origin: transformerOrigin})
	}
	return result, nil
}

func (l *Loader) LoadTransformer(
	ldr ifc.Loader, v ifc.Validator, res *resource.Resource,
) (*resmap.TransformerWithProperties, error) {
	c, err := l.loadAndConfigurePlugin(ldr, v, res)
	if err != nil {
		return nil, err
	}
	t, ok := c.(resmap.Transformer)
	if !ok {
		return nil, fmt.Errorf("plugin %s not a transformer", res.OrgId())
	}
	return &resmap.TransformerWithProperties{Transformer: t}, nil
}

func relativePluginPath(id resid.ResId) string {
	return filepath.Join(
		id.Group,
		id.Version,
		strings.ToLower(id.Kind))
}

func (l *Loader) AbsolutePluginPath(id resid.ResId) (string, error) {
	pluginHome, err := l.absPluginHome()
	if err != nil {
		return "", err
	}
	return filepath.Join(pluginHome, relativePluginPath(id), id.Kind), nil
}

// absPluginHome is the home of kustomize Exec and Go plugins.
// Kustomize plugin configuration files are k8s-style objects
// containing the fields 'apiVersion' and 'kind', e.g.
//
//	apiVersion: apps/v1
//	kind: Deployment
//
// kustomize reads plugin configuration data from a file path
// specified in the 'generators:' or 'transformers:' field of a
// kustomization file.  For Exec and Go plugins, kustomize
// uses this data to both locate the plugin and configure it.
// Each Exec or Go plugin (its code, its tests, its supporting data
// files, etc.) must be housed in its own directory at
//
//	${absPluginHome}/${pluginApiVersion}/LOWERCASE(${pluginKind})
//
// where
//   - ${absPluginHome} is an absolute path, defined below.
//   - ${pluginApiVersion} is taken from the plugin config file.
//   - ${pluginKind} is taken from the plugin config file.
func (l *Loader) absPluginHome() (string, error) {
	// External plugins are disabled--return the dummy plugin root.
	if l.pc.PluginRestrictions != types.PluginRestrictionsNone {
		return konfig.NoPluginHomeSentinal, nil
	}
	// We've already determined plugin home--use the cached value.
	if l.absolutePluginHome != "" {
		return l.absolutePluginHome, nil
	}

	// Check default locations for a valid plugin root, and cache it if found.
	dir, err := konfig.DefaultAbsPluginHome(l.fs)
	if err != nil {
		return "", err
	}
	l.absolutePluginHome = dir
	return l.absolutePluginHome, nil
}

func isBuiltinPlugin(res *resource.Resource) bool {
	// TODO: the special string should appear in Group, not Version.
	return res.GetGvk().Group == "" &&
		res.GetGvk().Version == konfig.BuiltinPluginApiVersion
}

func (l *Loader) loadAndConfigurePlugin(
	ldr ifc.Loader,
	v ifc.Validator,
	res *resource.Resource,
) (c resmap.Configurable, err error) {
	if isBuiltinPlugin(res) {
		switch l.pc.BpLoadingOptions {
		case types.BploLoadFromFileSys:
			c, err = l.loadPlugin(res)
		case types.BploUseStaticallyLinked:
			// Instead of looking for and loading a .so file,
			// instantiate the plugin from a generated factory
			// function (see "pluginator").  Being able to do this
			// is what makes a plugin "builtin".
			c, err = l.makeBuiltinPlugin(res.GetGvk())
		default:
			err = fmt.Errorf(
				"unknown plugin loader behavior specified: %s %v", res.GetGvk().String(),
				l.pc.BpLoadingOptions)
		}
	} else {
		switch l.pc.PluginRestrictions {
		case types.PluginRestrictionsNone:
			c, err = l.loadPlugin(res)
		case types.PluginRestrictionsBuiltinsOnly:
			err = types.NewErrOnlyBuiltinPluginsAllowed(res.OrgId().Kind)
		default:
			err = fmt.Errorf(
				"unknown plugin restriction specified: %v",
				l.pc.PluginRestrictions)
		}
	}
	if err != nil {
		return nil, err
	}
	yaml, err := res.AsYAML()
	if err != nil {
		return nil, errors.WrapPrefixf(err, "marshalling yaml from res %s", res.OrgId())
	}
	err = c.Config(resmap.NewPluginHelpers(ldr, v, l.rf, l.pc), yaml)
	if err != nil {
		return nil, errors.WrapPrefixf(
			err, "plugin %s fails configuration", res.OrgId())
	}
	return c, nil
}

func (l *Loader) makeBuiltinPlugin(r resid.Gvk) (resmap.Configurable, error) {
	bpt := builtinhelpers.GetBuiltinPluginType(r.Kind)
	if f, ok := builtinhelpers.GeneratorFactories[bpt]; ok {
		return f(), nil
	}
	if f, ok := builtinhelpers.TransformerFactories[bpt]; ok {
		return f(), nil
	}
	return nil, errors.Errorf("unable to load builtin %s", r)
}

func (l *Loader) loadPlugin(res *resource.Resource) (resmap.Configurable, error) {
	spec, err := fnplugin.GetFunctionSpec(res)
	if err != nil {
		return nil, fmt.Errorf("loader: %w", err)
	}
	if spec != nil {
		// validation check that function mounts are under the current kustomization directory
		for _, mount := range spec.Container.StorageMounts {
			if filepath.IsAbs(mount.Src) {
				return nil, errors.Errorf("plugin %s with mount path '%s' is not permitted; "+
					"mount paths must be relative to the current kustomization directory", res.OrgId(), mount.Src)
			}
			if strings.HasPrefix(filepath.Clean(mount.Src), "../") {
				return nil, errors.Errorf("plugin %s with mount path '%s' is not permitted; "+
					"mount paths must be under the current kustomization directory", res.OrgId(), mount.Src)
			}
		}
		return fnplugin.NewFnPlugin(&l.pc.FnpLoadingOptions), nil
	}
	return l.loadExecOrGoPlugin(res.OrgId())
}

func (l *Loader) loadExecOrGoPlugin(resId resid.ResId) (resmap.Configurable, error) {
	absPluginPath, err := l.AbsolutePluginPath(resId)
	if err != nil {
		return nil, err
	}
	// First try to load the plugin as an executable.
	p := execplugin.NewExecPlugin(absPluginPath)
	if err = p.ErrIfNotExecutable(); err == nil {
		return p, nil
	}
	if !os.IsNotExist(err) {
		// The file exists, but something else is wrong,
		// likely it's not executable.
		// Assume the user forgot to set the exec bit,
		// and return an error, rather than adding ".so"
		// to the name and attempting to load it as a Go
		// plugin, which will likely fail and result
		// in an obscure message.
		return nil, err
	}
	// Failing the above, try loading it as a Go plugin.
	c, err := l.loadGoPlugin(resId, absPluginPath+".so")
	if err != nil {
		return nil, err
	}
	return c, nil
}