talos

Форк
0
/
serviceaccount.go 
376 строк · 8.8 Кб
1
// This Source Code Form is subject to the terms of the Mozilla Public
2
// License, v. 2.0. If a copy of the MPL was not distributed with this
3
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4

5
package inject
6

7
import (
8
	"bytes"
9
	"errors"
10
	"fmt"
11
	"io"
12
	"path/filepath"
13
	"strings"
14

15
	"gopkg.in/yaml.v3"
16
	appsv1 "k8s.io/api/apps/v1"
17
	batchv1 "k8s.io/api/batch/v1"
18
	corev1 "k8s.io/api/core/v1"
19
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
21
	"k8s.io/apimachinery/pkg/runtime"
22
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
23

24
	"github.com/siderolabs/talos/pkg/machinery/constants"
25
)
26

27
const (
28
	injectToEnv = false
29
	volumeName  = "talos-secrets"
30

31
	nameSuffix = "-talos-secrets"
32

33
	apiVersionField = "apiVersion"
34
	kindField       = "kind"
35
	metadataField   = "metadata"
36
	namespaceField  = "namespace"
37
	nameField       = "name"
38

39
	yamlSeparator = "---\n"
40
)
41

42
// ServiceAccount takes a YAML with Kubernetes manifests and requested Talos roles as input
43
// and injects Talos service accounts into them.
44
//
45
//nolint:gocyclo
46
func ServiceAccount(reader io.Reader, roles []string) ([]byte, error) {
47
	var err error
48

49
	objectSerializer := json.NewSerializerWithOptions(
50
		json.DefaultMetaFactory,
51
		nil,
52
		nil,
53
		json.SerializerOptions{
54
			Yaml:   true,
55
			Pretty: true,
56
			Strict: true,
57
		},
58
	)
59

60
	seenResourceIDs := make(map[string]struct{})
61

62
	var buf bytes.Buffer
63

64
	decoder := yaml.NewDecoder(reader)
65

66
	// loop over all documents in a possibly YAML with multiple documents separated by ---
67
	for {
68
		var raw map[string]any
69

70
		err = decoder.Decode(&raw)
71
		if errors.Is(err, io.EOF) {
72
			break
73
		}
74

75
		if err != nil {
76
			return nil, err
77
		}
78

79
		if raw == nil {
80
			continue
81
		}
82

83
		var injected metav1.Object
84

85
		injected, err = injectToObject(raw)
86
		if err != nil { // not a known resource with a PodSpec
87
			// if this is already a Talos ServiceAccount resource we have seen,
88
			// we keep it only if we have not seen it yet (means it belongs to the user, not injected by us)
89
			id := readResourceIDFromServiceAccount(raw)
90
			if id != "" {
91
				if _, ok := seenResourceIDs[id]; ok {
92
					continue
93
				}
94

95
				seenResourceIDs[id] = struct{}{}
96
			}
97

98
			err = yaml.NewEncoder(&buf).Encode(raw)
99
			if err != nil {
100
				return nil, err
101
			}
102

103
			buf.WriteString(yamlSeparator)
104

105
			continue
106
		}
107

108
		// injectable resource type which contains a PodSpec
109

110
		runtimeObject, ok := injected.(runtime.Object)
111
		if !ok {
112
			return nil, errors.New("injected object is not a runtime.Object")
113
		}
114

115
		err = objectSerializer.Encode(runtimeObject, &buf)
116
		if err != nil {
117
			return nil, err
118
		}
119

120
		buf.WriteString(yamlSeparator)
121

122
		id := readResourceIDFromObject(injected)
123

124
		// inject service account for the resource
125
		if _, ok = seenResourceIDs[id]; !ok {
126
			sa := buildServiceAccount(injected.GetNamespace(), fmt.Sprintf("%s%s", injected.GetName(), nameSuffix), roles)
127

128
			err = yaml.NewEncoder(&buf).Encode(sa)
129
			if err != nil {
130
				return nil, err
131
			}
132

133
			buf.WriteString(yamlSeparator)
134

135
			// mark resource as seen
136
			seenResourceIDs[id] = struct{}{}
137
		}
138
	}
139

140
	return buf.Bytes(), nil
141
}
142

143
func buildServiceAccount(namespace string, name string, roles []string) map[string]any {
144
	metadata := map[string]any{
145
		nameField: name,
146
	}
147

148
	if namespace != "" {
149
		metadata[namespaceField] = namespace
150
	}
151

152
	return map[string]any{
153
		apiVersionField: fmt.Sprintf(
154
			"%s/%s",
155
			constants.ServiceAccountResourceGroup,
156
			constants.ServiceAccountResourceVersion,
157
		),
158
		kindField:     constants.ServiceAccountResourceKind,
159
		metadataField: metadata,
160
		"spec": map[string]any{
161
			"roles": roles,
162
		},
163
	}
164
}
165

166
func isServiceAccount(raw map[string]any) bool {
167
	apiVersionKind, err := readResourceAPIVersionKind(raw)
168
	if err != nil {
169
		return false
170
	}
171

172
	return apiVersionKind == fmt.Sprintf(
173
		"%s/%s/%s",
174
		constants.ServiceAccountResourceGroup,
175
		constants.ServiceAccountResourceVersion,
176
		constants.ServiceAccountResourceKind,
177
	)
178
}
179

180
// injectToDocument takes a single YAML document and attempts to inject a ServiceAccount
181
// into it if it is a known Kubernetes resource type which contains a corev1.PodSpec.
182
func injectToObject(raw map[string]any) (metav1.Object, error) {
183
	var err error
184

185
	apiVersionKind, err := readResourceAPIVersionKind(raw)
186
	if err != nil {
187
		return nil, err
188
	}
189

190
	switch apiVersionKind {
191
	case "v1/Pod":
192
		return injectToPodSpecObject[corev1.Pod](raw, func(obj *corev1.Pod) *corev1.PodSpec {
193
			return &obj.Spec
194
		})
195

196
	case "apps/v1/Deployment":
197
		return injectToPodSpecObject[appsv1.Deployment](raw, func(obj *appsv1.Deployment) *corev1.PodSpec {
198
			return &obj.Spec.Template.Spec
199
		})
200

201
	case "apps/v1/StatefulSet":
202
		return injectToPodSpecObject[appsv1.StatefulSet](raw, func(obj *appsv1.StatefulSet) *corev1.PodSpec {
203
			return &obj.Spec.Template.Spec
204
		})
205

206
	case "apps/v1/DaemonSet":
207
		return injectToPodSpecObject[appsv1.DaemonSet](raw, func(obj *appsv1.DaemonSet) *corev1.PodSpec {
208
			return &obj.Spec.Template.Spec
209
		})
210

211
	case "batch/v1/Job":
212
		return injectToPodSpecObject[batchv1.Job](raw, func(obj *batchv1.Job) *corev1.PodSpec {
213
			return &obj.Spec.Template.Spec
214
		})
215

216
	case "batch/v1/CronJob":
217
		return injectToPodSpecObject[batchv1.CronJob](raw, func(obj *batchv1.CronJob) *corev1.PodSpec {
218
			return &obj.Spec.JobTemplate.Spec.Template.Spec
219
		})
220
	}
221

222
	return nil, fmt.Errorf("unsupported object type: %s", apiVersionKind)
223
}
224

225
func injectToPodSpecObject[T any](raw map[string]any, podSpecFunc func(*T) *corev1.PodSpec) (*T, error) {
226
	objectName, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
227
	if err != nil {
228
		return nil, err
229
	}
230

231
	if !nameFound {
232
		return nil, errors.New("object has no name")
233
	}
234

235
	var obj T
236

237
	err = runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(raw, &obj, false)
238
	if err != nil {
239
		return nil, err
240
	}
241

242
	injectToPodSpec(fmt.Sprintf("%s%s", objectName, nameSuffix), podSpecFunc(&obj))
243

244
	return &obj, nil
245
}
246

247
func readResourceAPIVersionKind(raw map[string]any) (string, error) {
248
	apiVersion, found, err := unstructured.NestedString(raw, apiVersionField)
249
	if err != nil {
250
		return "", err
251
	}
252

253
	if !found {
254
		return "", fmt.Errorf("%s not found", apiVersionField)
255
	}
256

257
	kind, found, err := unstructured.NestedString(raw, kindField)
258
	if err != nil {
259
		return "", err
260
	}
261

262
	if !found {
263
		return "", fmt.Errorf("%s not found", kindField)
264
	}
265

266
	return fmt.Sprintf("%s/%s", apiVersion, kind), nil
267
}
268

269
func readResourceIDFromObject(obj metav1.Object) string {
270
	if obj.GetNamespace() == "" {
271
		return obj.GetName()
272
	}
273

274
	return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
275
}
276

277
func readResourceIDFromServiceAccount(raw map[string]any) string {
278
	if !isServiceAccount(raw) {
279
		return ""
280
	}
281

282
	name, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
283
	if err != nil || !nameFound {
284
		return ""
285
	}
286

287
	nameTrimmed := strings.TrimSuffix(name, nameSuffix)
288

289
	ns, nsFound, err := unstructured.NestedString(raw, metadataField, namespaceField)
290
	if err != nil {
291
		return ""
292
	}
293

294
	if nsFound {
295
		return fmt.Sprintf("%s/%s", ns, nameTrimmed)
296
	}
297

298
	return nameTrimmed
299
}
300

301
func injectToPodSpec(secretName string, podSpec *corev1.PodSpec) {
302
	podSpec.Volumes = injectToVolumes(secretName, podSpec.Volumes)
303
	podSpec.InitContainers = injectToContainers(podSpec.InitContainers)
304
	podSpec.Containers = injectToContainers(podSpec.Containers)
305
}
306

307
func injectToVolumes(name string, volumes []corev1.Volume) []corev1.Volume {
308
	result := make([]corev1.Volume, 0, len(volumes))
309

310
	for _, volume := range volumes {
311
		if volume.Name != volumeName {
312
			result = append(result, volume)
313
		}
314
	}
315

316
	result = append(result, corev1.Volume{
317
		Name: volumeName,
318
		VolumeSource: corev1.VolumeSource{
319
			Secret: &corev1.SecretVolumeSource{
320
				SecretName: name,
321
			},
322
		},
323
	})
324

325
	return result
326
}
327

328
func injectToContainers(containers []corev1.Container) []corev1.Container {
329
	result := make([]corev1.Container, 0, len(containers))
330

331
	for _, container := range containers {
332
		injectToContainer(&container)
333

334
		result = append(result, container)
335
	}
336

337
	return result
338
}
339

340
func injectToContainer(container *corev1.Container) {
341
	volumeMounts := make([]corev1.VolumeMount, 0, len(container.VolumeMounts))
342

343
	for _, mount := range container.VolumeMounts {
344
		if mount.Name != volumeName {
345
			volumeMounts = append(volumeMounts, mount)
346
		}
347
	}
348

349
	volumeMounts = append(volumeMounts, corev1.VolumeMount{
350
		Name:      volumeName,
351
		MountPath: constants.ServiceAccountMountPath,
352
	})
353

354
	container.VolumeMounts = volumeMounts
355

356
	if injectToEnv {
357
		container.Env = injectToContainerEnv(container.Env)
358
	}
359
}
360

361
func injectToContainerEnv(env []corev1.EnvVar) []corev1.EnvVar {
362
	result := make([]corev1.EnvVar, 0, len(env))
363

364
	for _, envVar := range env {
365
		if envVar.Name != constants.TalosConfigEnvVar {
366
			result = append(result, envVar)
367
		}
368
	}
369

370
	result = append(result, corev1.EnvVar{
371
		Name:  constants.TalosConfigEnvVar,
372
		Value: filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename),
373
	})
374

375
	return result
376
}
377

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.