talos/pkg/kubernetes/inject/serviceaccount.go
Andrey Smirnov 96aa9638f7
chore: rename talos-systems/talos to siderolabs/talos
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>
2022-11-03 16:50:32 +04:00

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
}