mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-02 03:08:15 +00:00
implement LegacyServiceAccountTokenCleanUp alpha
This commit is contained in:
@@ -98,6 +98,8 @@ type KubeControllerManagerConfiguration struct {
|
||||
// CronJobControllerConfiguration holds configuration for CronJobController
|
||||
// related features.
|
||||
CronJobController cronjobconfig.CronJobControllerConfiguration
|
||||
// LegacySATokenCleanerConfiguration holds configuration for LegacySATokenCleaner related features.
|
||||
LegacySATokenCleaner serviceaccountconfig.LegacySATokenCleanerConfiguration
|
||||
// NamespaceControllerConfiguration holds configuration for NamespaceController
|
||||
// related features.
|
||||
NamespaceController namespaceconfig.NamespaceControllerConfiguration
|
||||
|
||||
@@ -104,6 +104,8 @@ func SetDefaults_KubeControllerManagerConfiguration(obj *kubectrlmgrconfigv1alph
|
||||
resourcequotaconfigv1alpha1.RecommendedDefaultResourceQuotaControllerConfiguration(&obj.ResourceQuotaController)
|
||||
// Use the default RecommendedDefaultGenericControllerManagerConfiguration options
|
||||
serviceconfigv1alpha1.RecommendedDefaultServiceControllerConfiguration(&obj.ServiceController)
|
||||
// Use the default RecommendedDefaultLegacySATokenCleanerConfiguration options
|
||||
serviceaccountconfigv1alpha1.RecommendedDefaultLegacySATokenCleanerConfiguration(&obj.LegacySATokenCleaner)
|
||||
// Use the default RecommendedDefaultSAControllerConfiguration options
|
||||
serviceaccountconfigv1alpha1.RecommendedDefaultSAControllerConfiguration(&obj.SAController)
|
||||
// Use the default RecommendedDefaultTTLAfterFinishedControllerConfiguration options
|
||||
|
||||
@@ -184,6 +184,9 @@ func autoConvert_v1alpha1_KubeControllerManagerConfiguration_To_config_KubeContr
|
||||
if err := cronjobconfigv1alpha1.Convert_v1alpha1_CronJobControllerConfiguration_To_config_CronJobControllerConfiguration(&in.CronJobController, &out.CronJobController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := serviceaccountconfigv1alpha1.Convert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration(&in.LegacySATokenCleaner, &out.LegacySATokenCleaner, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := namespaceconfigv1alpha1.Convert_v1alpha1_NamespaceControllerConfiguration_To_config_NamespaceControllerConfiguration(&in.NamespaceController, &out.NamespaceController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -274,6 +277,9 @@ func autoConvert_config_KubeControllerManagerConfiguration_To_v1alpha1_KubeContr
|
||||
if err := cronjobconfigv1alpha1.Convert_config_CronJobControllerConfiguration_To_v1alpha1_CronJobControllerConfiguration(&in.CronJobController, &out.CronJobController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := serviceaccountconfigv1alpha1.Convert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration(&in.LegacySATokenCleaner, &out.LegacySATokenCleaner, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := namespaceconfigv1alpha1.Convert_config_NamespaceControllerConfiguration_To_v1alpha1_NamespaceControllerConfiguration(&in.NamespaceController, &out.NamespaceController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
|
||||
out.HPAController = in.HPAController
|
||||
out.JobController = in.JobController
|
||||
out.CronJobController = in.CronJobController
|
||||
out.LegacySATokenCleaner = in.LegacySATokenCleaner
|
||||
out.NamespaceController = in.NamespaceController
|
||||
out.NodeIPAMController = in.NodeIPAMController
|
||||
out.NodeLifecycleController = in.NodeLifecycleController
|
||||
|
||||
@@ -16,6 +16,10 @@ limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// SAControllerConfiguration contains elements describing ServiceAccountController.
|
||||
type SAControllerConfiguration struct {
|
||||
// serviceAccountKeyFile is the filename containing a PEM-encoded private RSA key
|
||||
@@ -28,3 +32,9 @@ type SAControllerConfiguration struct {
|
||||
// account's token secret. This must be a valid PEM-encoded CA bundle.
|
||||
RootCAFile string
|
||||
}
|
||||
|
||||
type LegacySATokenCleanerConfiguration struct {
|
||||
// CleanUpPeriod is the period of time since the last usage of an
|
||||
// auto-generated service account token before it can be deleted.
|
||||
CleanUpPeriod metav1.Duration
|
||||
}
|
||||
|
||||
@@ -38,3 +38,13 @@ func Convert_v1alpha1_SAControllerConfiguration_To_config_SAControllerConfigurat
|
||||
func Convert_config_SAControllerConfiguration_To_v1alpha1_SAControllerConfiguration(in *config.SAControllerConfiguration, out *v1alpha1.SAControllerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_config_SAControllerConfiguration_To_v1alpha1_SAControllerConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration(in *v1alpha1.LegacySATokenCleanerConfiguration, out *config.LegacySATokenCleanerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
// Convert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration is an autogenerated conversion function.
|
||||
func Convert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration(in *config.LegacySATokenCleanerConfiguration, out *v1alpha1.LegacySATokenCleanerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
)
|
||||
|
||||
@@ -34,3 +37,10 @@ func RecommendedDefaultSAControllerConfiguration(obj *kubectrlmgrconfigv1alpha1.
|
||||
obj.ConcurrentSATokenSyncs = 5
|
||||
}
|
||||
}
|
||||
|
||||
func RecommendedDefaultLegacySATokenCleanerConfiguration(obj *kubectrlmgrconfigv1alpha1.LegacySATokenCleanerConfiguration) {
|
||||
zero := metav1.Duration{}
|
||||
if obj.CleanUpPeriod == zero {
|
||||
obj.CleanUpPeriod = metav1.Duration{Duration: 365 * 24 * time.Hour}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,21 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.LegacySATokenCleanerConfiguration)(nil), (*v1alpha1.LegacySATokenCleanerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration(a.(*config.LegacySATokenCleanerConfiguration), b.(*v1alpha1.LegacySATokenCleanerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.SAControllerConfiguration)(nil), (*v1alpha1.SAControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_SAControllerConfiguration_To_v1alpha1_SAControllerConfiguration(a.(*config.SAControllerConfiguration), b.(*v1alpha1.SAControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*v1alpha1.LegacySATokenCleanerConfiguration)(nil), (*config.LegacySATokenCleanerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration(a.(*v1alpha1.LegacySATokenCleanerConfiguration), b.(*config.LegacySATokenCleanerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*v1alpha1.SAControllerConfiguration)(nil), (*config.SAControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_SAControllerConfiguration_To_config_SAControllerConfiguration(a.(*v1alpha1.SAControllerConfiguration), b.(*config.SAControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
@@ -81,6 +91,16 @@ func Convert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, ou
|
||||
return autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_LegacySATokenCleanerConfiguration_To_config_LegacySATokenCleanerConfiguration(in *v1alpha1.LegacySATokenCleanerConfiguration, out *config.LegacySATokenCleanerConfiguration, s conversion.Scope) error {
|
||||
out.CleanUpPeriod = in.CleanUpPeriod
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_config_LegacySATokenCleanerConfiguration_To_v1alpha1_LegacySATokenCleanerConfiguration(in *config.LegacySATokenCleanerConfiguration, out *v1alpha1.LegacySATokenCleanerConfiguration, s conversion.Scope) error {
|
||||
out.CleanUpPeriod = in.CleanUpPeriod
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_SAControllerConfiguration_To_config_SAControllerConfiguration(in *v1alpha1.SAControllerConfiguration, out *config.SAControllerConfiguration, s conversion.Scope) error {
|
||||
out.ServiceAccountKeyFile = in.ServiceAccountKeyFile
|
||||
out.ConcurrentSATokenSyncs = in.ConcurrentSATokenSyncs
|
||||
|
||||
@@ -21,6 +21,23 @@ limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *LegacySATokenCleanerConfiguration) DeepCopyInto(out *LegacySATokenCleanerConfiguration) {
|
||||
*out = *in
|
||||
out.CleanUpPeriod = in.CleanUpPeriod
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LegacySATokenCleanerConfiguration.
|
||||
func (in *LegacySATokenCleanerConfiguration) DeepCopy() *LegacySATokenCleanerConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(LegacySATokenCleanerConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SAControllerConfiguration) DeepCopyInto(out *SAControllerConfiguration) {
|
||||
*out = *in
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
"k8s.io/utils/clock"
|
||||
)
|
||||
|
||||
const (
|
||||
dateFormat = "2006-01-02"
|
||||
DefaultCleanerSyncInterval = 24 * time.Hour
|
||||
)
|
||||
|
||||
// TokenCleanerOptions contains options for the LegacySATokenCleaner
|
||||
type LegacySATokenCleanerOptions struct {
|
||||
// CleanUpPeriod is the period of time since the last usage of an legacy token before it can be deleted.
|
||||
CleanUpPeriod time.Duration
|
||||
SyncInterval time.Duration
|
||||
}
|
||||
|
||||
// LegacySATokenCleaner is a controller that deletes legacy serviceaccount tokens that are not in use for a specified period of time.
|
||||
type LegacySATokenCleaner struct {
|
||||
client clientset.Interface
|
||||
clock clock.Clock
|
||||
saLister listersv1.ServiceAccountLister
|
||||
saInformerSynced cache.InformerSynced
|
||||
|
||||
secretLister listersv1.SecretLister
|
||||
secretInformerSynced cache.InformerSynced
|
||||
|
||||
podLister listersv1.PodLister
|
||||
podInformerSynced cache.InformerSynced
|
||||
|
||||
syncInterval time.Duration
|
||||
minimumSinceLastUsed time.Duration
|
||||
}
|
||||
|
||||
// NewLegacySATokenCleaner returns a new *NewLegacySATokenCleaner.
|
||||
func NewLegacySATokenCleaner(saInformer coreinformers.ServiceAccountInformer, secretInformer coreinformers.SecretInformer, podInformer coreinformers.PodInformer, client clientset.Interface, cl clock.Clock, options LegacySATokenCleanerOptions) (*LegacySATokenCleaner, error) {
|
||||
if !(options.CleanUpPeriod > 0) {
|
||||
return nil, fmt.Errorf("invalid CleanUpPeriod: %v", options.CleanUpPeriod)
|
||||
}
|
||||
if !(options.SyncInterval > 0) {
|
||||
return nil, fmt.Errorf("invalid SyncInterval: %v", options.SyncInterval)
|
||||
}
|
||||
|
||||
tc := &LegacySATokenCleaner{
|
||||
client: client,
|
||||
clock: cl,
|
||||
saLister: saInformer.Lister(),
|
||||
saInformerSynced: saInformer.Informer().HasSynced,
|
||||
secretLister: secretInformer.Lister(),
|
||||
secretInformerSynced: secretInformer.Informer().HasSynced,
|
||||
podLister: podInformer.Lister(),
|
||||
podInformerSynced: podInformer.Informer().HasSynced,
|
||||
minimumSinceLastUsed: options.CleanUpPeriod,
|
||||
syncInterval: options.SyncInterval,
|
||||
}
|
||||
|
||||
return tc, nil
|
||||
}
|
||||
|
||||
func (tc *LegacySATokenCleaner) Run(ctx context.Context) {
|
||||
defer utilruntime.HandleCrash()
|
||||
|
||||
logger := klog.FromContext(ctx)
|
||||
logger.Info("Starting legacy service account token cleaner controller")
|
||||
defer logger.Info("Shutting down legacy service account token cleaner controller")
|
||||
|
||||
if !cache.WaitForNamedCacheSync("legacy-service-account-token-cleaner", ctx.Done(), tc.saInformerSynced, tc.secretInformerSynced, tc.podInformerSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
go wait.UntilWithContext(ctx, tc.evaluateSATokens, tc.syncInterval)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (tc *LegacySATokenCleaner) evaluateSATokens(ctx context.Context) {
|
||||
logger := klog.FromContext(ctx)
|
||||
|
||||
now := tc.clock.Now().UTC()
|
||||
trackedSince, err := tc.latestPossibleTrackedSinceTime(ctx)
|
||||
if err != nil {
|
||||
logger.Error(err, "Getting lastest possible tracked_since time")
|
||||
return
|
||||
}
|
||||
|
||||
if now.Before(trackedSince.Add(tc.minimumSinceLastUsed)) {
|
||||
// we haven't been tracking long enough
|
||||
return
|
||||
}
|
||||
|
||||
preserveCreatedOnOrAfter := now.Add(-tc.minimumSinceLastUsed)
|
||||
preserveUsedOnOrAfter := now.Add(-tc.minimumSinceLastUsed).Format(dateFormat)
|
||||
|
||||
secretList, err := tc.secretLister.Secrets(metav1.NamespaceAll).List(labels.Everything())
|
||||
if err != nil {
|
||||
logger.Error(err, "Getting cached secret list")
|
||||
return
|
||||
}
|
||||
|
||||
namespaceToUsedSecretNames := make(map[string]sets.String)
|
||||
for _, secret := range secretList {
|
||||
if secret.Type != v1.SecretTypeServiceAccountToken {
|
||||
continue
|
||||
}
|
||||
if !secret.CreationTimestamp.Time.Before(preserveCreatedOnOrAfter) {
|
||||
continue
|
||||
}
|
||||
|
||||
if secret.DeletionTimestamp != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if LastUsedLabelKey does not exist, we think the secret has not been used
|
||||
// since the legacy token starts to track.
|
||||
lastUsed, ok := secret.Labels[serviceaccount.LastUsedLabelKey]
|
||||
if ok {
|
||||
_, err := time.Parse(dateFormat, lastUsed)
|
||||
if err != nil {
|
||||
// the lastUsed value is not well-formed thus we cannot determine it
|
||||
logger.Error(err, "Parsing lastUsed time", "secret", klog.KRef(secret.Namespace, secret.Name))
|
||||
continue
|
||||
}
|
||||
if lastUsed >= preserveUsedOnOrAfter {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sa, saErr := tc.getServiceAccount(secret)
|
||||
|
||||
if saErr != nil {
|
||||
logger.Error(saErr, "Getting service account", "secret", klog.KRef(secret.Namespace, secret.Name))
|
||||
continue
|
||||
}
|
||||
if sa == nil || !hasSecretReference(sa, secret.Name) {
|
||||
// can't determine if this is an auto-generated token
|
||||
continue
|
||||
}
|
||||
|
||||
mountedSecretNames, err := tc.getMountedSecretNames(secret.Namespace, namespaceToUsedSecretNames)
|
||||
if err != nil {
|
||||
logger.Error(err, "Resolving mounted secrets", "secret", klog.KRef(secret.Namespace, secret.Name))
|
||||
continue
|
||||
}
|
||||
if mountedSecretNames.Has(secret.Name) {
|
||||
// still used by pods
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("Delete auto-generated service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "creationTime", secret.CreationTimestamp, "lastUsed", lastUsed)
|
||||
if err := tc.client.CoreV1().Secrets(secret.Namespace).Delete(ctx, secret.Name, metav1.DeleteOptions{Preconditions: &metav1.Preconditions{ResourceVersion: &secret.ResourceVersion}}); err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Deleting legacy service account token", "secret", klog.KRef(secret.Namespace, secret.Name), "serviceaccount", sa.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *LegacySATokenCleaner) getMountedSecretNames(secretNamespace string, namespaceToUsedSecretNames map[string]sets.String) (sets.String, error) {
|
||||
if secrets, ok := namespaceToUsedSecretNames[secretNamespace]; ok {
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
podList, err := tc.podLister.Pods(secretNamespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pod list from pod cache: %v", err)
|
||||
}
|
||||
|
||||
var secrets sets.String
|
||||
for _, pod := range podList {
|
||||
podutil.VisitPodSecretNames(pod, func(secretName string) bool {
|
||||
if secrets == nil {
|
||||
secrets = sets.NewString()
|
||||
}
|
||||
secrets.Insert(secretName)
|
||||
return true
|
||||
})
|
||||
}
|
||||
if secrets != nil {
|
||||
namespaceToUsedSecretNames[secretNamespace] = secrets
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func (tc *LegacySATokenCleaner) getServiceAccount(secret *v1.Secret) (*v1.ServiceAccount, error) {
|
||||
saName := secret.Annotations[v1.ServiceAccountNameKey]
|
||||
if len(saName) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
saUID := types.UID(secret.Annotations[v1.ServiceAccountUIDKey])
|
||||
sa, err := tc.saLister.ServiceAccounts(secret.Namespace).Get(saName)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure UID matches if given
|
||||
if len(saUID) == 0 || saUID == sa.UID {
|
||||
return sa, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// get the latest possible TrackedSince time information from the configMap label.
|
||||
func (tc *LegacySATokenCleaner) latestPossibleTrackedSinceTime(ctx context.Context) (time.Time, error) {
|
||||
configMap, err := tc.client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, legacytokentracking.ConfigMapName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
trackedSince, exist := configMap.Data[legacytokentracking.ConfigMapDataKey]
|
||||
if !exist {
|
||||
return time.Time{}, fmt.Errorf("configMap does not have since label")
|
||||
}
|
||||
trackedSinceTime, err := time.Parse(dateFormat, trackedSince)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("error parsing trackedSince time: %v", err)
|
||||
}
|
||||
// make sure the time to be 00:00 on the day just after the date starts to track
|
||||
return trackedSinceTime.AddDate(0, 0, 1), nil
|
||||
}
|
||||
|
||||
func hasSecretReference(serviceAccount *v1.ServiceAccount, secretName string) bool {
|
||||
for _, secret := range serviceAccount.Secrets {
|
||||
if secret.Name == secretName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package serviceaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
"k8s.io/kubernetes/pkg/controlplane/controller/legacytokentracking"
|
||||
"k8s.io/kubernetes/pkg/serviceaccount"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
func configuredConfigMap(label string) *v1.ConfigMap {
|
||||
if label == "" {
|
||||
return &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: legacytokentracking.ConfigMapName},
|
||||
}
|
||||
}
|
||||
return &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: metav1.NamespaceSystem, Name: legacytokentracking.ConfigMapName},
|
||||
Data: map[string]string{legacytokentracking.ConfigMapDataKey: label},
|
||||
}
|
||||
}
|
||||
|
||||
func configuredServiceAccountTokenSecret(label, creationTimeString, serviceAccountName, serviceAccountUID, deletionTimeString string) *v1.Secret {
|
||||
var deletionTime *metav1.Time
|
||||
if deletionTimeString == "" {
|
||||
deletionTime = nil
|
||||
} else {
|
||||
deletionTime = &metav1.Time{Time: time.Now().UTC()}
|
||||
}
|
||||
creationTime, _ := time.Parse(dateFormat, creationTimeString)
|
||||
labels := map[string]string{}
|
||||
if label != "" {
|
||||
labels[serviceaccount.LastUsedLabelKey] = label
|
||||
}
|
||||
return &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "token-secret-1",
|
||||
Namespace: "default",
|
||||
UID: "23456",
|
||||
ResourceVersion: "1",
|
||||
Labels: labels,
|
||||
CreationTimestamp: metav1.NewTime(creationTime),
|
||||
DeletionTimestamp: deletionTime,
|
||||
Annotations: map[string]string{
|
||||
v1.ServiceAccountNameKey: serviceAccountName,
|
||||
v1.ServiceAccountUIDKey: serviceAccountUID,
|
||||
},
|
||||
},
|
||||
Type: v1.SecretTypeServiceAccountToken,
|
||||
Data: map[string][]byte{
|
||||
"token": []byte("ABC"),
|
||||
"ca.crt": []byte("CA Data"),
|
||||
"namespace": []byte("default"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func configuredLegacyTokenCleanUpPeriod(start string) time.Duration {
|
||||
current := time.Now().UTC()
|
||||
startTime, _ := time.Parse(dateFormat, start)
|
||||
return current.Sub(startTime)
|
||||
}
|
||||
|
||||
func configuredPod(withSecretMount bool) *v1.Pod {
|
||||
if !withSecretMount {
|
||||
return &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
}
|
||||
}
|
||||
return &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod-1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Volumes: []v1.Volume{{Name: "foo", VolumeSource: v1.VolumeSource{Secret: &v1.SecretVolumeSource{SecretName: "token-secret-1"}}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyServiceAccountTokenCleanUp(t *testing.T) {
|
||||
testcases := map[string]struct {
|
||||
LegacyTokenCleanUpPeriod time.Duration
|
||||
|
||||
ExistingServiceAccount *v1.ServiceAccount
|
||||
ExistingSecret *v1.Secret
|
||||
ExistingPod *v1.Pod
|
||||
ClientObjects []runtime.Object
|
||||
|
||||
ExpectedActions []core.Action
|
||||
}{
|
||||
"configmap does not exist": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-28"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"configmap exists, but the configmap does not have tracked-since label": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-28"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"configmap exists, the time period since 'tracked-since' is smaller than the CleanUpPeriod": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-29")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"configmap exists, the 'tracked-since' cannot be parsed": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27-1")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"secret is not SecretTypeServiceAccountToken type": {
|
||||
ExistingSecret: opaqueSecret(),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-28")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"secret is not referenced by serviceaccount": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(emptySecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret has a late creation time": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-30", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret has a deletion time": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", "deleted"),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-30"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret has a late last-used time": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-30", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret has a last-used label, but it can not be parsed": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27-1", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"secret-referenced service account does not exist": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"secret-referenced service account uid does not match": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "123456", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"secret-referenced service account name is empty": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-27")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret does not have 'last-used' label": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-28")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-30"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
core.NewDeleteActionWithOptions(
|
||||
schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
|
||||
metav1.NamespaceDefault, "token-secret-1",
|
||||
metav1.DeleteOptions{
|
||||
Preconditions: &metav1.Preconditions{ResourceVersion: &configuredServiceAccountTokenSecret("", "2022-12-27", "default", "12345", "").ResourceVersion},
|
||||
}),
|
||||
},
|
||||
},
|
||||
"auto-generated secret is mounted by the pod": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(true),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-28")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-29"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
},
|
||||
},
|
||||
"auto-generated secret has 'last-used' label, the time period since last-used is larger than CleanUpPeriod": {
|
||||
ExistingSecret: configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", ""),
|
||||
ExistingServiceAccount: serviceAccount(tokenSecretReferences()),
|
||||
ExistingPod: configuredPod(false),
|
||||
ClientObjects: []runtime.Object{configuredConfigMap("2022-12-28")},
|
||||
LegacyTokenCleanUpPeriod: configuredLegacyTokenCleanUpPeriod("2022-12-30"),
|
||||
ExpectedActions: []core.Action{
|
||||
core.NewGetAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, metav1.NamespaceSystem, legacytokentracking.ConfigMapName),
|
||||
core.NewDeleteActionWithOptions(
|
||||
schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
|
||||
metav1.NamespaceDefault, "token-secret-1",
|
||||
metav1.DeleteOptions{Preconditions: &metav1.Preconditions{
|
||||
ResourceVersion: &configuredServiceAccountTokenSecret("2022-12-27", "2022-12-27", "default", "12345", "").ResourceVersion,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range testcases {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
tc.ClientObjects = append(tc.ClientObjects, tc.ExistingSecret)
|
||||
client := fake.NewSimpleClientset(tc.ClientObjects...)
|
||||
|
||||
informers := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
|
||||
secretInformer := informers.Core().V1().Secrets()
|
||||
saInformer := informers.Core().V1().ServiceAccounts()
|
||||
podInformer := informers.Core().V1().Pods()
|
||||
secrets := secretInformer.Informer().GetStore()
|
||||
serviceAccounts := saInformer.Informer().GetStore()
|
||||
pods := podInformer.Informer().GetStore()
|
||||
options := LegacySATokenCleanerOptions{
|
||||
SyncInterval: 30 * time.Second,
|
||||
CleanUpPeriod: tc.LegacyTokenCleanUpPeriod,
|
||||
}
|
||||
cleaner, _ := NewLegacySATokenCleaner(saInformer, secretInformer, podInformer, client, testingclock.NewFakeClock(time.Now().UTC()), options)
|
||||
|
||||
if tc.ExistingServiceAccount != nil {
|
||||
serviceAccounts.Add(tc.ExistingServiceAccount)
|
||||
}
|
||||
if tc.ExistingPod != nil {
|
||||
pods.Add(tc.ExistingPod)
|
||||
}
|
||||
secrets.Add(tc.ExistingSecret)
|
||||
|
||||
ctx := context.TODO()
|
||||
cleaner.evaluateSATokens(ctx)
|
||||
|
||||
actions := client.Actions()
|
||||
if len(actions) != len(tc.ExpectedActions) {
|
||||
t.Fatalf("got %d actions, wanted %d actions", len(actions), len(tc.ExpectedActions))
|
||||
}
|
||||
for i, action := range actions {
|
||||
if len(tc.ExpectedActions) < i+1 {
|
||||
t.Errorf("%s: %d unexpected actions: %+v", k, len(actions)-len(tc.ExpectedActions), actions[i:])
|
||||
break
|
||||
}
|
||||
expectedAction := tc.ExpectedActions[i]
|
||||
if !reflect.DeepEqual(expectedAction, action) {
|
||||
t.Errorf("got action %#v, wanted %v", action, expectedAction)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user