167 lines
5.9 KiB
Go
167 lines
5.9 KiB
Go
package serviceaccounttoken
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
corecontrollers "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
|
|
"github.com/sirupsen/logrus"
|
|
v1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
"k8s.io/client-go/kubernetes"
|
|
clientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
|
)
|
|
|
|
const (
|
|
// ServiceAccountSecretLabel is the label used to search for the secret belonging to a service account.
|
|
ServiceAccountSecretLabel = "cattle.io/service-account.name"
|
|
|
|
serviceAccountSecretAnnotation = "kubernetes.io/service-account.name"
|
|
)
|
|
|
|
// secretLister is an abstraction over any kind of secret lister.
|
|
// The caller can use any cache or client it has available, whether that is from norman, wrangler, or client-go,
|
|
// as long as it can wrap it in a simplified lambda with this signature.
|
|
type secretLister func(namespace string, selector labels.Selector) ([]*v1.Secret, error)
|
|
|
|
// EnsureSecretForServiceAccount gets or creates a service account token Secret for the provided Service Account.
|
|
// For k8s <1.24, the secret is automatically generated for the service account. For >=1.24, we need to generate it explicitly.
|
|
func EnsureSecretForServiceAccount(ctx context.Context, secretsCache corecontrollers.SecretCache, clientSet kubernetes.Interface, sa *v1.ServiceAccount) (*v1.Secret, error) {
|
|
if sa == nil {
|
|
return nil, fmt.Errorf("could not ensure secret for invalid service account")
|
|
}
|
|
secretClient := clientSet.CoreV1().Secrets(sa.Namespace)
|
|
var secretLister secretLister
|
|
if secretsCache != nil {
|
|
secretLister = secretsCache.List
|
|
} else {
|
|
secretLister = func(_ string, selector labels.Selector) ([]*v1.Secret, error) {
|
|
secretList, err := secretClient.List(ctx, metav1.ListOptions{
|
|
LabelSelector: selector.String(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*v1.Secret, len(secretList.Items))
|
|
for i := range secretList.Items {
|
|
result[i] = &secretList.Items[i]
|
|
}
|
|
return result, nil
|
|
}
|
|
}
|
|
secret, err := ServiceAccountSecret(ctx, sa, secretLister, secretClient)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error looking up secret for service account [%s:%s]: %w", sa.Namespace, sa.Name, err)
|
|
}
|
|
if secret == nil {
|
|
sc := SecretTemplate(sa)
|
|
secret, err = secretClient.Create(ctx, sc, metav1.CreateOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error ensuring secret for service account [%s:%s]: %w", sa.Namespace, sa.Name, err)
|
|
}
|
|
}
|
|
if len(secret.Data[v1.ServiceAccountTokenKey]) > 0 {
|
|
return secret, nil
|
|
}
|
|
logrus.Infof("EnsureSecretForServiceAccount: waiting for secret [%s] to be populated with token", secret.Name)
|
|
backoff := wait.Backoff{
|
|
Duration: 2 * time.Millisecond,
|
|
Cap: 100 * time.Millisecond,
|
|
Steps: 50,
|
|
}
|
|
err = wait.ExponentialBackoff(backoff, func() (bool, error) {
|
|
var err error
|
|
// use the secret client, rather than the secret getter, to circumvent the cache
|
|
secret, err = secretClient.Get(ctx, secret.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return false, fmt.Errorf("error ensuring secret for service account [%s:%s]: %w", sa.Namespace, sa.Name, err)
|
|
}
|
|
if len(secret.Data[v1.ServiceAccountTokenKey]) > 0 {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error ensuring secret for service account [%s:%s]: %w", sa.Namespace, sa.Name, err)
|
|
}
|
|
return secret, nil
|
|
}
|
|
|
|
// SecretTemplate generate a template of service-account-token Secret for the provided Service Account.
|
|
func SecretTemplate(sa *v1.ServiceAccount) *v1.Secret {
|
|
return &v1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
GenerateName: serviceAccountSecretPrefix(sa),
|
|
Namespace: sa.Namespace,
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
{
|
|
APIVersion: "v1",
|
|
Kind: "ServiceAccount",
|
|
Name: sa.Name,
|
|
UID: sa.UID,
|
|
},
|
|
},
|
|
Annotations: map[string]string{
|
|
serviceAccountSecretAnnotation: sa.Name,
|
|
},
|
|
Labels: map[string]string{
|
|
ServiceAccountSecretLabel: sa.Name,
|
|
},
|
|
},
|
|
Type: v1.SecretTypeServiceAccountToken,
|
|
}
|
|
|
|
}
|
|
|
|
// serviceAccountSecretPrefix returns the prefix that will be used to generate the secret for the given service account.
|
|
func serviceAccountSecretPrefix(sa *v1.ServiceAccount) string {
|
|
return fmt.Sprintf("%s-token-", sa.Name)
|
|
}
|
|
|
|
// ServiceAccountSecret returns the secret for the given Service Account.
|
|
// If there are more than one, it returns the first. Can return a nil secret
|
|
// and a nil error if no secret is found
|
|
func ServiceAccountSecret(ctx context.Context, sa *v1.ServiceAccount, secretLister secretLister, secretClient clientv1.SecretInterface) (*v1.Secret, error) {
|
|
if sa == nil {
|
|
return nil, fmt.Errorf("cannot get secret for nil service account")
|
|
}
|
|
secrets, err := secretLister(sa.Namespace, labels.SelectorFromSet(map[string]string{
|
|
ServiceAccountSecretLabel: sa.Name,
|
|
}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not get secrets for service account: %w", err)
|
|
}
|
|
if len(secrets) < 1 {
|
|
return nil, nil
|
|
}
|
|
var result *v1.Secret
|
|
for _, s := range secrets {
|
|
if isSecretForServiceAccount(s, sa) {
|
|
if result == nil {
|
|
result = s
|
|
}
|
|
continue
|
|
}
|
|
logrus.Warnf("EnsureSecretForServiceAccount: secret [%s:%s] is invalid for service account [%s], deleting", s.Namespace, s.Name, sa.Name)
|
|
err = secretClient.Delete(ctx, s.Name, metav1.DeleteOptions{})
|
|
if err != nil {
|
|
// we don't want to return the delete failure since the success/failure of the cleanup shouldn't affect
|
|
// the ability of the caller to use any identified, valid secret
|
|
logrus.Errorf("unable to delete secret [%s:%s]: %v", s.Namespace, s.Name, err)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func isSecretForServiceAccount(secret *v1.Secret, sa *v1.ServiceAccount) bool {
|
|
if secret.Type != v1.SecretTypeServiceAccountToken {
|
|
return false
|
|
}
|
|
annotations := secret.Annotations
|
|
annotation := annotations[serviceAccountSecretAnnotation]
|
|
return sa.Name == annotation
|
|
}
|