230 lines
6.1 KiB
Go
230 lines
6.1 KiB
Go
/*
|
|
* Copyright 2024 the KubeSphere Authors.
|
|
* Please refer to the LICENSE file in the root directory of the project.
|
|
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
|
*/
|
|
|
|
package ksserviceaccount
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/go-logr/logr"
|
|
v1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
|
|
|
|
"kubesphere.io/kubesphere/pkg/constants"
|
|
kscontroller "kubesphere.io/kubesphere/pkg/controller"
|
|
)
|
|
|
|
const (
|
|
AnnotationServiceAccountName = "kubesphere.io/serviceaccount-name"
|
|
ServiceAccountVolumeName = "kubesphere-service-account"
|
|
VolumeMountPath = "/var/run/secrets/kubesphere.io/serviceaccount"
|
|
caCertName = "kubesphere-root-ca.crt"
|
|
caCertKey = "ca.crt"
|
|
)
|
|
|
|
const webhookName = "service-account-injector"
|
|
|
|
func (w *Webhook) Name() string {
|
|
return webhookName
|
|
}
|
|
|
|
var _ kscontroller.Controller = &Webhook{}
|
|
|
|
type Webhook struct {
|
|
}
|
|
|
|
func (w *Webhook) SetupWithManager(mgr *kscontroller.Manager) error {
|
|
serviceAccountPodInjector := &PodInjector{
|
|
Log: mgr.GetLogger(),
|
|
Decoder: admission.NewDecoder(mgr.GetScheme()),
|
|
Client: mgr.GetClient(),
|
|
tls: mgr.Options.KubeSphereOptions.TLS,
|
|
}
|
|
mgr.GetWebhookServer().Register("/serviceaccount-pod-injector", &webhook.Admission{Handler: serviceAccountPodInjector})
|
|
return nil
|
|
}
|
|
|
|
// PodInjector injects a token (issue by KubeSphere) by mounting a secret to the
|
|
// pod when the pod specifies a KubeSphere service account.
|
|
type PodInjector struct {
|
|
client.Client
|
|
|
|
Log logr.Logger
|
|
Decoder admission.Decoder
|
|
tls bool
|
|
}
|
|
|
|
func (i *PodInjector) Handle(ctx context.Context, req admission.Request) admission.Response {
|
|
log := i.Log.WithValues("namespace", req.Namespace).WithValues("pod", req.Name)
|
|
var saName string
|
|
pod := &v1.Pod{}
|
|
err := i.Decoder.Decode(req, pod)
|
|
if err != nil {
|
|
log.Error(err, "decode pod failed")
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
if pod.Annotations != nil {
|
|
saName = pod.Annotations[AnnotationServiceAccountName]
|
|
}
|
|
|
|
if saName == "" {
|
|
log.V(6).Info("pod does not specify kubesphere service account, skip it")
|
|
return admission.Allowed("")
|
|
}
|
|
|
|
sa := &corev1alpha1.ServiceAccount{}
|
|
err = i.Client.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: saName}, sa)
|
|
if err != nil {
|
|
if errors.IsNotFound(err) {
|
|
return admission.Errored(http.StatusNotFound, err)
|
|
}
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
err = i.injectServiceAccountTokenAndCACert(ctx, pod, sa)
|
|
if err != nil {
|
|
log.WithValues("serviceaccount", sa.Name).
|
|
Error(err, "inject service account token to pod failed")
|
|
if errors.IsNotFound(err) {
|
|
return admission.Errored(http.StatusNotFound, err)
|
|
}
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
log.V(6).WithValues("serviceaccount", sa.Name).
|
|
Info("successfully injected service account token into the pod")
|
|
|
|
marshal, err := json.Marshal(pod)
|
|
if err != nil {
|
|
log.Error(err, "marshal pod to json failed")
|
|
return admission.Errored(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
return admission.PatchResponseFromRaw(req.Object.Raw, marshal)
|
|
}
|
|
|
|
func (i *PodInjector) injectServiceAccountTokenAndCACert(ctx context.Context, pod *v1.Pod, sa *corev1alpha1.ServiceAccount) error {
|
|
var tokenName string
|
|
if len(sa.Secrets) == 0 {
|
|
return fmt.Errorf("can not find any token in service account: %s", sa.Name)
|
|
}
|
|
|
|
// just select the first token, cause usually sa has only one token
|
|
tokenName = sa.Secrets[0].Name
|
|
|
|
secret := &v1.Secret{}
|
|
err := i.Client.Get(ctx, types.NamespacedName{Namespace: sa.Namespace, Name: tokenName}, secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
volume := v1.Volume{
|
|
Name: ServiceAccountVolumeName,
|
|
VolumeSource: v1.VolumeSource{
|
|
Projected: &v1.ProjectedVolumeSource{
|
|
Sources: []v1.VolumeProjection{
|
|
{
|
|
Secret: &v1.SecretProjection{
|
|
LocalObjectReference: v1.LocalObjectReference{
|
|
Name: tokenName,
|
|
},
|
|
Items: []v1.KeyToPath{
|
|
{
|
|
Key: corev1alpha1.ServiceAccountToken,
|
|
Path: corev1alpha1.ServiceAccountToken,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if i.tls {
|
|
if err = i.createCertConfigMap(ctx, pod.Namespace); err != nil {
|
|
return err
|
|
}
|
|
volume.Projected.Sources = append(volume.Projected.Sources, v1.VolumeProjection{
|
|
ConfigMap: &v1.ConfigMapProjection{
|
|
LocalObjectReference: v1.LocalObjectReference{
|
|
Name: caCertName,
|
|
},
|
|
Items: []v1.KeyToPath{
|
|
{
|
|
Key: caCertKey,
|
|
Path: caCertKey,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
volumeMount := v1.VolumeMount{
|
|
Name: ServiceAccountVolumeName,
|
|
ReadOnly: true,
|
|
MountPath: VolumeMountPath,
|
|
}
|
|
|
|
if pod.Spec.Volumes == nil {
|
|
volumes := make([]v1.Volume, 0)
|
|
pod.Spec.Volumes = volumes
|
|
}
|
|
pod.Spec.Volumes = append(pod.Spec.Volumes, volume)
|
|
|
|
// mount to every container
|
|
for i, c := range pod.Spec.Containers {
|
|
volumeMounts := append(c.VolumeMounts, volumeMount)
|
|
pod.Spec.Containers[i].VolumeMounts = volumeMounts
|
|
}
|
|
|
|
// mount to init container
|
|
for i, c := range pod.Spec.InitContainers {
|
|
volumeMounts := append(c.VolumeMounts, volumeMount)
|
|
pod.Spec.InitContainers[i].VolumeMounts = volumeMounts
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *PodInjector) createCertConfigMap(ctx context.Context, namespace string) error {
|
|
cm := &v1.ConfigMap{}
|
|
if err := i.Client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: caCertName}, cm); err != nil {
|
|
if errors.IsNotFound(err) {
|
|
secret := &v1.Secret{}
|
|
if err = i.Client.Get(ctx, types.NamespacedName{
|
|
Namespace: constants.KubeSphereNamespace,
|
|
Name: "ks-apiserver-tls-certs",
|
|
}, secret); err != nil {
|
|
return err
|
|
}
|
|
|
|
cm = &v1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Namespace: namespace,
|
|
Name: caCertName,
|
|
},
|
|
BinaryData: map[string][]byte{
|
|
caCertKey: secret.Data[caCertKey],
|
|
},
|
|
}
|
|
return i.Client.Create(ctx, cm)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|