mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-09 06:31:25 +02:00
There's a cyclic dependency on siderolink library which imports talos machinery back. We will fix that after we get talos pushed under a new name. Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
379 lines
8.8 KiB
Go
379 lines
8.8 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package inject
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
|
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
)
|
|
|
|
const (
|
|
injectToEnv = false
|
|
volumeName = "talos-secrets"
|
|
|
|
nameSuffix = "-talos-secrets"
|
|
|
|
apiVersionField = "apiVersion"
|
|
kindField = "kind"
|
|
metadataField = "metadata"
|
|
namespaceField = "namespace"
|
|
nameField = "name"
|
|
|
|
yamlSeparator = "---\n"
|
|
)
|
|
|
|
// ServiceAccount takes a YAML with Kubernetes manifests and requested Talos roles as input
|
|
// and injects Talos service accounts into them.
|
|
//
|
|
//nolint:gocyclo
|
|
func ServiceAccount(reader io.Reader, roles []string) ([]byte, error) {
|
|
var err error
|
|
|
|
objectSerializer := json.NewSerializerWithOptions(
|
|
json.DefaultMetaFactory,
|
|
nil,
|
|
nil,
|
|
json.SerializerOptions{
|
|
Yaml: true,
|
|
Pretty: true,
|
|
Strict: true,
|
|
},
|
|
)
|
|
|
|
seenResourceIDs := make(map[string]struct{})
|
|
|
|
var buf bytes.Buffer
|
|
|
|
decoder := yaml.NewDecoder(reader)
|
|
|
|
// loop over all documents in a possibly YAML with multiple documents separated by ---
|
|
for {
|
|
var raw map[string]any
|
|
|
|
err = decoder.Decode(&raw)
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if raw == nil {
|
|
continue
|
|
}
|
|
|
|
var injected metav1.Object
|
|
|
|
injected, err = injectToObject(raw)
|
|
if err != nil { // not a known resource with a PodSpec
|
|
// if this is already a Talos ServiceAccount resource we have seen,
|
|
// we keep it only if we have not seen it yet (means it belongs to the user, not injected by us)
|
|
id := readResourceIDFromServiceAccount(raw)
|
|
if id != "" {
|
|
if _, ok := seenResourceIDs[id]; ok {
|
|
continue
|
|
}
|
|
|
|
seenResourceIDs[id] = struct{}{}
|
|
}
|
|
|
|
err = yaml.NewEncoder(&buf).Encode(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf.WriteString(yamlSeparator)
|
|
|
|
continue
|
|
}
|
|
|
|
// injectable resource type which contains a PodSpec
|
|
|
|
runtimeObject, ok := injected.(runtime.Object)
|
|
if !ok {
|
|
return nil, errors.New("injected object is not a runtime.Object")
|
|
}
|
|
|
|
err = objectSerializer.Encode(runtimeObject, &buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf.WriteString(yamlSeparator)
|
|
|
|
id := readResourceIDFromObject(injected)
|
|
|
|
// inject service account for the resource
|
|
if _, ok = seenResourceIDs[id]; !ok {
|
|
sa := buildServiceAccount(injected.GetNamespace(), fmt.Sprintf("%s%s", injected.GetName(), nameSuffix), roles)
|
|
|
|
err = yaml.NewEncoder(&buf).Encode(sa)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf.WriteString(yamlSeparator)
|
|
|
|
// mark resource as seen
|
|
seenResourceIDs[id] = struct{}{}
|
|
}
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func buildServiceAccount(namespace string, name string, roles []string) map[string]any {
|
|
metadata := map[string]any{
|
|
nameField: name,
|
|
}
|
|
|
|
if namespace != "" {
|
|
metadata[namespaceField] = namespace
|
|
}
|
|
|
|
return map[string]any{
|
|
apiVersionField: fmt.Sprintf(
|
|
"%s/%s",
|
|
constants.ServiceAccountResourceGroup,
|
|
constants.ServiceAccountResourceVersion,
|
|
),
|
|
kindField: constants.ServiceAccountResourceKind,
|
|
metadataField: metadata,
|
|
"spec": map[string]any{
|
|
"roles": roles,
|
|
},
|
|
}
|
|
}
|
|
|
|
func isServiceAccount(raw map[string]any) bool {
|
|
apiVersionKind, err := readResourceAPIVersionKind(raw)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return apiVersionKind == fmt.Sprintf(
|
|
"%s/%s/%s",
|
|
constants.ServiceAccountResourceGroup,
|
|
constants.ServiceAccountResourceVersion,
|
|
constants.ServiceAccountResourceKind,
|
|
)
|
|
}
|
|
|
|
// injectToDocument takes a single YAML document and attempts to inject a ServiceAccount
|
|
// into it if it is a known Kubernetes resource type which contains a corev1.PodSpec.
|
|
//
|
|
//nolint:gocyclo
|
|
func injectToObject(raw map[string]any) (metav1.Object, error) {
|
|
var err error
|
|
|
|
apiVersionKind, err := readResourceAPIVersionKind(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch apiVersionKind {
|
|
case "v1/Pod":
|
|
return injectToPodSpecObject[corev1.Pod](raw, func(obj *corev1.Pod) *corev1.PodSpec {
|
|
return &obj.Spec
|
|
})
|
|
|
|
case "apps/v1/Deployment":
|
|
return injectToPodSpecObject[appsv1.Deployment](raw, func(obj *appsv1.Deployment) *corev1.PodSpec {
|
|
return &obj.Spec.Template.Spec
|
|
})
|
|
|
|
case "apps/v1/StatefulSet":
|
|
return injectToPodSpecObject[appsv1.StatefulSet](raw, func(obj *appsv1.StatefulSet) *corev1.PodSpec {
|
|
return &obj.Spec.Template.Spec
|
|
})
|
|
|
|
case "apps/v1/DaemonSet":
|
|
return injectToPodSpecObject[appsv1.DaemonSet](raw, func(obj *appsv1.DaemonSet) *corev1.PodSpec {
|
|
return &obj.Spec.Template.Spec
|
|
})
|
|
|
|
case "batch/v1/Job":
|
|
return injectToPodSpecObject[batchv1.Job](raw, func(obj *batchv1.Job) *corev1.PodSpec {
|
|
return &obj.Spec.Template.Spec
|
|
})
|
|
|
|
case "batch/v1/CronJob":
|
|
return injectToPodSpecObject[batchv1.CronJob](raw, func(obj *batchv1.CronJob) *corev1.PodSpec {
|
|
return &obj.Spec.JobTemplate.Spec.Template.Spec
|
|
})
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported object type: %s", apiVersionKind)
|
|
}
|
|
|
|
func injectToPodSpecObject[T any](raw map[string]any, podSpecFunc func(*T) *corev1.PodSpec) (*T, error) {
|
|
objectName, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !nameFound {
|
|
return nil, errors.New("object has no name")
|
|
}
|
|
|
|
var obj T
|
|
|
|
err = runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(raw, &obj, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
injectToPodSpec(fmt.Sprintf("%s%s", objectName, nameSuffix), podSpecFunc(&obj))
|
|
|
|
return &obj, nil
|
|
}
|
|
|
|
func readResourceAPIVersionKind(raw map[string]any) (string, error) {
|
|
apiVersion, found, err := unstructured.NestedString(raw, apiVersionField)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !found {
|
|
return "", fmt.Errorf("%s not found", apiVersionField)
|
|
}
|
|
|
|
kind, found, err := unstructured.NestedString(raw, kindField)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !found {
|
|
return "", fmt.Errorf("%s not found", kindField)
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", apiVersion, kind), nil
|
|
}
|
|
|
|
func readResourceIDFromObject(obj metav1.Object) string {
|
|
if obj.GetNamespace() == "" {
|
|
return obj.GetName()
|
|
}
|
|
|
|
return fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName())
|
|
}
|
|
|
|
func readResourceIDFromServiceAccount(raw map[string]any) string {
|
|
if !isServiceAccount(raw) {
|
|
return ""
|
|
}
|
|
|
|
name, nameFound, err := unstructured.NestedString(raw, metadataField, nameField)
|
|
if err != nil || !nameFound {
|
|
return ""
|
|
}
|
|
|
|
nameTrimmed := strings.TrimSuffix(name, nameSuffix)
|
|
|
|
ns, nsFound, err := unstructured.NestedString(raw, metadataField, namespaceField)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
if nsFound {
|
|
return fmt.Sprintf("%s/%s", ns, nameTrimmed)
|
|
}
|
|
|
|
return nameTrimmed
|
|
}
|
|
|
|
func injectToPodSpec(secretName string, podSpec *corev1.PodSpec) {
|
|
podSpec.Volumes = injectToVolumes(secretName, podSpec.Volumes)
|
|
podSpec.InitContainers = injectToContainers(podSpec.InitContainers)
|
|
podSpec.Containers = injectToContainers(podSpec.Containers)
|
|
}
|
|
|
|
func injectToVolumes(name string, volumes []corev1.Volume) []corev1.Volume {
|
|
result := make([]corev1.Volume, 0, len(volumes))
|
|
|
|
for _, volume := range volumes {
|
|
if volume.Name != volumeName {
|
|
result = append(result, volume)
|
|
}
|
|
}
|
|
|
|
result = append(result, corev1.Volume{
|
|
Name: volumeName,
|
|
VolumeSource: corev1.VolumeSource{
|
|
Secret: &corev1.SecretVolumeSource{
|
|
SecretName: name,
|
|
},
|
|
},
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
func injectToContainers(containers []corev1.Container) []corev1.Container {
|
|
result := make([]corev1.Container, 0, len(containers))
|
|
|
|
for _, container := range containers {
|
|
injectToContainer(&container)
|
|
|
|
result = append(result, container)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func injectToContainer(container *corev1.Container) {
|
|
volumeMounts := make([]corev1.VolumeMount, 0, len(container.VolumeMounts))
|
|
|
|
for _, mount := range container.VolumeMounts {
|
|
if mount.Name != volumeName {
|
|
volumeMounts = append(volumeMounts, mount)
|
|
}
|
|
}
|
|
|
|
volumeMounts = append(volumeMounts, corev1.VolumeMount{
|
|
Name: volumeName,
|
|
MountPath: constants.ServiceAccountMountPath,
|
|
})
|
|
|
|
container.VolumeMounts = volumeMounts
|
|
|
|
if injectToEnv {
|
|
container.Env = injectToContainerEnv(container.Env)
|
|
}
|
|
}
|
|
|
|
func injectToContainerEnv(env []corev1.EnvVar) []corev1.EnvVar {
|
|
result := make([]corev1.EnvVar, 0, len(env))
|
|
|
|
for _, envVar := range env {
|
|
if envVar.Name != constants.TalosConfigEnvVar {
|
|
result = append(result, envVar)
|
|
}
|
|
}
|
|
|
|
result = append(result, corev1.EnvVar{
|
|
Name: constants.TalosConfigEnvVar,
|
|
Value: filepath.Join(constants.ServiceAccountMountPath, constants.TalosconfigFilename),
|
|
})
|
|
|
|
return result
|
|
}
|