feat: kubesphere 4.0 (#6115)

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

* feat: kubesphere 4.0

Signed-off-by: ci-bot <ci-bot@kubesphere.io>

---------

Signed-off-by: ci-bot <ci-bot@kubesphere.io>
Co-authored-by: ks-ci-bot <ks-ci-bot@example.com>
Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
KubeSphere CI Bot
2024-09-06 11:05:52 +08:00
committed by GitHub
parent b5015ec7b9
commit 447a51f08b
8557 changed files with 546695 additions and 1146174 deletions

View File

@@ -0,0 +1,228 @@
/*
* 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
}

View File

@@ -0,0 +1,181 @@
/*
* 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"
"fmt"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
corev1alpha1 "kubesphere.io/api/core/v1alpha1"
kscontroller "kubesphere.io/kubesphere/pkg/controller"
)
const (
controllerName = "ks-serviceaccount"
finalizer = "finalizers.kubesphere.io/serviceaccount"
messageCreateSecretSuccessfully = "Create token secret successfully"
reasonInvalidSecret = "InvalidSecret"
)
var _ kscontroller.Controller = &Reconciler{}
func (r *Reconciler) Name() string {
return controllerName
}
type Reconciler struct {
client.Client
Scheme *runtime.Scheme
Logger logr.Logger
EventRecorder record.EventRecorder
}
func (r *Reconciler) SetupWithManager(mgr *kscontroller.Manager) error {
r.Client = mgr.GetClient()
r.EventRecorder = mgr.GetEventRecorderFor(controllerName)
r.Logger = ctrl.Log.WithName("controllers").WithName(controllerName)
return builder.
ControllerManagedBy(mgr).
For(
&corev1alpha1.ServiceAccount{},
builder.WithPredicates(
predicate.ResourceVersionChangedPredicate{},
),
).
WithOptions(controller.Options{
MaxConcurrentReconciles: 2,
}).
Complete(r)
}
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (reconcile.Result, error) {
logger := r.Logger.WithValues(req.NamespacedName, "ServiceAccount")
sa := &corev1alpha1.ServiceAccount{}
if err := r.Get(ctx, req.NamespacedName, sa); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
if sa.ObjectMeta.DeletionTimestamp.IsZero() {
if !controllerutil.ContainsFinalizer(sa, finalizer) {
deepCopy := sa.DeepCopy()
deepCopy.Finalizers = append(deepCopy.Finalizers, finalizer)
if len(sa.Secrets) == 0 {
secretCreated, err := r.createTokenSecret(ctx, sa)
if err != nil {
logger.Error(err, "create secret failed")
return ctrl.Result{}, err
}
logger.V(4).WithName(secretCreated.Name).Info("secret created successfully")
deepCopy.Secrets = append(deepCopy.Secrets, v1.ObjectReference{
Namespace: secretCreated.Namespace,
Name: secretCreated.Name,
})
r.EventRecorder.Event(deepCopy, corev1.EventTypeNormal, kscontroller.Synced, messageCreateSecretSuccessfully)
}
if err := r.Update(ctx, deepCopy); err != nil {
logger.Error(err, "update serviceaccount failed")
return ctrl.Result{}, err
}
}
} else {
if controllerutil.ContainsFinalizer(sa, finalizer) {
if err := r.deleteSecretToken(ctx, sa, logger); err != nil {
logger.Error(err, "delete secret failed")
return ctrl.Result{}, err
}
_ = controllerutil.RemoveFinalizer(sa, finalizer)
if err := r.Update(ctx, sa); err != nil {
logger.Error(err, "update serviceaccount failed")
return ctrl.Result{}, err
}
}
}
if err := r.checkAllSecret(ctx, sa); err != nil {
logger.Error(err, "failed check secrets")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func (r *Reconciler) createTokenSecret(ctx context.Context, sa *corev1alpha1.ServiceAccount) (*v1.Secret, error) {
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-", sa.Name),
Namespace: sa.Namespace,
Annotations: map[string]string{corev1alpha1.ServiceAccountName: sa.Name},
},
Type: corev1alpha1.SecretTypeServiceAccountToken,
}
return secret, r.Client.Create(ctx, secret)
}
func (r *Reconciler) deleteSecretToken(ctx context.Context, sa *corev1alpha1.ServiceAccount, logger logr.Logger) error {
for _, secretName := range sa.Secrets {
secret := &v1.Secret{}
if err := r.Get(ctx, client.ObjectKey{Namespace: secretName.Namespace, Name: secretName.Name}, secret); err != nil {
if errors.IsNotFound(err) {
continue
} else {
return err
}
}
if err := r.checkSecretToken(secret, sa.Name); err == nil {
if err = r.Delete(ctx, secret); err != nil {
return err
}
logger.V(2).WithName(secretName.Name).Info("delete secret successfully")
}
}
return nil
}
func (r *Reconciler) checkAllSecret(ctx context.Context, sa *corev1alpha1.ServiceAccount) error {
for _, secretRef := range sa.Secrets {
secret := &v1.Secret{}
if err := r.Get(ctx, client.ObjectKey{Namespace: sa.Namespace, Name: secretRef.Name}, secret); err != nil {
if errors.IsNotFound(err) {
r.EventRecorder.Event(sa, corev1.EventTypeWarning, reasonInvalidSecret, err.Error())
continue
}
return err
}
if err := r.checkSecretToken(secret, sa.Name); err != nil {
r.EventRecorder.Event(sa, corev1.EventTypeWarning, reasonInvalidSecret, err.Error())
}
}
return nil
}
// checkSecretTokens Check if there has valid token, and the invalid token reference will be deleted
func (r *Reconciler) checkSecretToken(secret *v1.Secret, subjectName string) error {
if secret.Type != corev1alpha1.SecretTypeServiceAccountToken {
return fmt.Errorf("unsupported secret %s type: %s", secret.Name, secret.Type)
}
if saName := secret.Annotations[corev1alpha1.ServiceAccountName]; saName != subjectName {
return fmt.Errorf("incorrect subject name %s", saName)
}
return nil
}