talos
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
5package inject
6
7import (
8"bytes"
9"errors"
10"fmt"
11"io"
12"path/filepath"
13"strings"
14
15"gopkg.in/yaml.v3"
16appsv1 "k8s.io/api/apps/v1"
17batchv1 "k8s.io/api/batch/v1"
18corev1 "k8s.io/api/core/v1"
19metav1 "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
27const (
28injectToEnv = false
29volumeName = "talos-secrets"
30
31nameSuffix = "-talos-secrets"
32
33apiVersionField = "apiVersion"
34kindField = "kind"
35metadataField = "metadata"
36namespaceField = "namespace"
37nameField = "name"
38
39yamlSeparator = "---\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
46func ServiceAccount(reader io.Reader, roles []string) ([]byte, error) {
47var err error
48
49objectSerializer := json.NewSerializerWithOptions(
50json.DefaultMetaFactory,
51nil,
52nil,
53json.SerializerOptions{
54Yaml: true,
55Pretty: true,
56Strict: true,
57},
58)
59
60seenResourceIDs := make(map[string]struct{})
61
62var buf bytes.Buffer
63
64decoder := yaml.NewDecoder(reader)
65
66// loop over all documents in a possibly YAML with multiple documents separated by ---
67for {
68var raw map[string]any
69
70err = decoder.Decode(&raw)
71if errors.Is(err, io.EOF) {
72break
73}
74
75if err != nil {
76return nil, err
77}
78
79if raw == nil {
80continue
81}
82
83var injected metav1.Object
84
85injected, err = injectToObject(raw)
86if 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)
89id := readResourceIDFromServiceAccount(raw)
90if id != "" {
91if _, ok := seenResourceIDs[id]; ok {
92continue
93}
94
95seenResourceIDs[id] = struct{}{}
96}
97
98err = yaml.NewEncoder(&buf).Encode(raw)
99if err != nil {
100return nil, err
101}
102
103buf.WriteString(yamlSeparator)
104
105continue
106}
107
108// injectable resource type which contains a PodSpec
109
110runtimeObject, ok := injected.(runtime.Object)
111if !ok {
112return nil, errors.New("injected object is not a runtime.Object")
113}
114
115err = objectSerializer.Encode(runtimeObject, &buf)
116if err != nil {
117return nil, err
118}
119
120buf.WriteString(yamlSeparator)
121
122id := readResourceIDFromObject(injected)
123
124// inject service account for the resource
125if _, ok = seenResourceIDs[id]; !ok {
126sa := buildServiceAccount(injected.GetNamespace(), fmt.Sprintf("%s%s", injected.GetName(), nameSuffix), roles)
127
128err = yaml.NewEncoder(&buf).Encode(sa)
129if err != nil {
130return nil, err
131}
132
133buf.WriteString(yamlSeparator)
134
135// mark resource as seen
136seenResourceIDs[id] = struct{}{}
137}
138}
139
140return buf.Bytes(), nil
141}
142
143func buildServiceAccount(namespace string, name string, roles []string) map[string]any {
144metadata := map[string]any{
145nameField: name,
146}
147
148if namespace != "" {
149metadata[namespaceField] = namespace
150}
151
152return map[string]any{
153apiVersionField: fmt.Sprintf(
154"%s/%s",
155constants.ServiceAccountResourceGroup,
156constants.ServiceAccountResourceVersion,
157),
158kindField: constants.ServiceAccountResourceKind,
159metadataField: metadata,
160"spec": map[string]any{
161"roles": roles,
162},
163}
164}
165
166func isServiceAccount(raw map[string]any) bool {
167apiVersionKind, err := readResourceAPIVersionKind(raw)
168if err != nil {
169return false
170}
171
172return apiVersionKind == fmt.Sprintf(
173"%s/%s/%s",
174constants.ServiceAccountResourceGroup,
175constants.ServiceAccountResourceVersion,
176constants.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.
182func injectToObject(raw map[string]any) (metav1.Object, error) {
183var err error
184
185apiVersionKind, err := readResourceAPIVersionKind(raw)
186if err != nil {
187return nil, err
188}
189
190switch apiVersionKind {
191case "v1/Pod":
192return injectToPodSpecObject[corev1.Pod](raw, func(obj *corev1.Pod) *corev1.PodSpec {
193return &obj.Spec
194})
195
196case "apps/v1/Deployment":
197return injectToPodSpecObject[appsv1.Deployment](raw, func(obj *appsv1.Deployment) *corev1.PodSpec {
198return &obj.Spec.Template.Spec
199})
200
201case "apps/v1/StatefulSet":
202return injectToPodSpecObject[appsv1.StatefulSet](raw, func(obj *appsv1.StatefulSet) *corev1.PodSpec {
203return &obj.Spec.Template.Spec
204})
205
206case "apps/v1/DaemonSet":
207return injectToPodSpecObject[appsv1.DaemonSet](raw, func(obj *appsv1.DaemonSet) *corev1.PodSpec {
208return &obj.Spec.Template.Spec
209})
210
211case "batch/v1/Job":
212return injectToPodSpecObject[batchv1.Job](raw, func(obj *batchv1.Job) *corev1.PodSpec {
213return &obj.Spec.Template.Spec
214})
215
216case "batch/v1/CronJob":
217return injectToPodSpecObject[batchv1.CronJob](raw, func(obj *batchv1.CronJob) *corev1.PodSpec {
218return &obj.Spec.JobTemplate.Spec.Template.Spec
219})
220}
221
222return nil, fmt.Errorf("unsupported object type: %s", apiVersionKind)
223}
224
225func injectToPodSpecObject[T any](raw map[string]any, podSpecFunc func(*T) *corev1.PodSpec) (*T, error) {
226objectName, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
227if err != nil {
228return nil, err
229}
230
231if !nameFound {
232return nil, errors.New("object has no name")
233}
234
235var obj T
236
237err = runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(raw, &obj, false)
238if err != nil {
239return nil, err
240}
241
242injectToPodSpec(fmt.Sprintf("%s%s", objectName, nameSuffix), podSpecFunc(&obj))
243
244return &obj, nil
245}
246
247func readResourceAPIVersionKind(raw map[string]any) (string, error) {
248apiVersion, found, err := unstructured.NestedString(raw, apiVersionField)
249if err != nil {
250return "", err
251}
252
253if !found {
254return "", fmt.Errorf("%s not found", apiVersionField)
255}
256
257kind, found, err := unstructured.NestedString(raw, kindField)
258if err != nil {
259return "", err
260}
261
262if !found {
263return "", fmt.Errorf("%s not found", kindField)
264}
265
266return fmt.Sprintf("%s/%s", apiVersion, kind), nil
267}
268
269func readResourceIDFromObject(obj metav1.Object) string {
270if obj.GetNamespace() == "" {
271return obj.GetName()
272}
273
274return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
275}
276
277func readResourceIDFromServiceAccount(raw map[string]any) string {
278if !isServiceAccount(raw) {
279return ""
280}
281
282name, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
283if err != nil || !nameFound {
284return ""
285}
286
287nameTrimmed := strings.TrimSuffix(name, nameSuffix)
288
289ns, nsFound, err := unstructured.NestedString(raw, metadataField, namespaceField)
290if err != nil {
291return ""
292}
293
294if nsFound {
295return fmt.Sprintf("%s/%s", ns, nameTrimmed)
296}
297
298return nameTrimmed
299}
300
301func injectToPodSpec(secretName string, podSpec *corev1.PodSpec) {
302podSpec.Volumes = injectToVolumes(secretName, podSpec.Volumes)
303podSpec.InitContainers = injectToContainers(podSpec.InitContainers)
304podSpec.Containers = injectToContainers(podSpec.Containers)
305}
306
307func injectToVolumes(name string, volumes []corev1.Volume) []corev1.Volume {
308result := make([]corev1.Volume, 0, len(volumes))
309
310for _, volume := range volumes {
311if volume.Name != volumeName {
312result = append(result, volume)
313}
314}
315
316result = append(result, corev1.Volume{
317Name: volumeName,
318VolumeSource: corev1.VolumeSource{
319Secret: &corev1.SecretVolumeSource{
320SecretName: name,
321},
322},
323})
324
325return result
326}
327
328func injectToContainers(containers []corev1.Container) []corev1.Container {
329result := make([]corev1.Container, 0, len(containers))
330
331for _, container := range containers {
332injectToContainer(&container)
333
334result = append(result, container)
335}
336
337return result
338}
339
340func injectToContainer(container *corev1.Container) {
341volumeMounts := make([]corev1.VolumeMount, 0, len(container.VolumeMounts))
342
343for _, mount := range container.VolumeMounts {
344if mount.Name != volumeName {
345volumeMounts = append(volumeMounts, mount)
346}
347}
348
349volumeMounts = append(volumeMounts, corev1.VolumeMount{
350Name: volumeName,
351MountPath: constants.ServiceAccountMountPath,
352})
353
354container.VolumeMounts = volumeMounts
355
356if injectToEnv {
357container.Env = injectToContainerEnv(container.Env)
358}
359}
360
361func injectToContainerEnv(env []corev1.EnvVar) []corev1.EnvVar {
362result := make([]corev1.EnvVar, 0, len(env))
363
364for _, envVar := range env {
365if envVar.Name != constants.TalosConfigEnvVar {
366result = append(result, envVar)
367}
368}
369
370result = append(result, corev1.EnvVar{
371Name: constants.TalosConfigEnvVar,
372Value: filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename),
373})
374
375return result
376}
377