ContainerImage.Pinniped/internal/controllerlib/test/integration/examplecontroller/controller/creating.go

180 lines
5.9 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package controller
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
corev1informers "k8s.io/client-go/informers/core/v1"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/events"
"k8s.io/klog/v2"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
)
//nolint:funlen
func NewExampleCreatingController(
services corev1informers.ServiceInformer,
secrets corev1informers.SecretInformer,
secretClient corev1client.SecretsGetter,
recorder events.EventRecorder,
secretData string,
) controllerlib.Controller {
serviceLister := services.Lister()
secretLister := secrets.Lister()
// note that these functions do not need to be inlined
// this just demonstrates that for simple Syncer implementations, everything can be in one place
requiresSecretGeneration := func(service *corev1.Service) (bool, error) {
// check the secret since it could not have been created yet
secretName := service.Annotations[api.SecretNameAnnotation]
if len(secretName) == 0 {
return false, nil
}
secret, err := secretLister.Secrets(service.Namespace).Get(secretName)
if apierrors.IsNotFound(err) {
// we have not created the secret yet
return true, nil
}
if err != nil {
return false, fmt.Errorf("unable to get the secret %s/%s: %w", service.Namespace, secretName, err)
}
if string(secret.Data[api.SecretDataKey]) == secretData {
return false, nil
}
// the secret exists but the data does not match what we expect (i.e. we have new secretData now)
return true, nil
}
generateSecret := func(service *corev1.Service) error {
klog.V(4).InfoS("generating new secret for service", "namespace", service.Namespace, "name", service.Name)
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: service.Annotations[api.SecretNameAnnotation],
Namespace: service.Namespace,
Annotations: map[string]string{
api.ServiceUIDAnnotation: string(service.UID),
api.ServiceNameAnnotation: service.Name,
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "Service",
Name: service.Name,
UID: service.UID,
},
},
Finalizers: nil, // TODO maybe add finalizer to guarantee we never miss a delete event?
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
api.SecretDataKey: []byte(secretData),
},
}
_, err := secretClient.Secrets(service.Namespace).Create(context.TODO(), secret, metav1.CreateOptions{})
if apierrors.IsAlreadyExists(err) {
actualSecret, getErr := secretClient.Secrets(service.Namespace).Get(context.TODO(), secret.Name, metav1.GetOptions{})
if getErr != nil {
return getErr
}
if actualSecret.Annotations[api.ServiceUIDAnnotation] != string(service.UID) {
utilruntime.HandleError(fmt.Errorf("secret %s/%s does not have corresponding service UID %v", actualSecret.Namespace, actualSecret.Name, service.UID))
return nil // drop from queue because we cannot safely update this secret
}
klog.V(4).InfoS("updating data in existing secret", "namespace", secret.Namespace, "name", secret.Name)
// Actually update the secret in the regeneration case (the secret already exists but we want to update to new secretData).
_, updateErr := secretClient.Secrets(secret.Namespace).Update(context.TODO(), secret, metav1.UpdateOptions{})
return updateErr
}
if err != nil {
return fmt.Errorf("unable to create secret %s/%s: %w", secret.Namespace, secret.Name, err)
}
return nil
}
syncer := controllerlib.SyncFunc(func(ctx controllerlib.Context) error {
service, err := serviceLister.Services(ctx.Key.Namespace).Get(ctx.Key.Name)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return fmt.Errorf("unable to get the service %s/%s: %w", service.Namespace, service.Name, err)
}
ok, err := requiresSecretGeneration(service)
if err != nil || !ok {
return err
}
return generateSecret(service)
})
config := controllerlib.Config{
Name: "example-controller-creating",
Syncer: syncer,
}
toServiceName := func(secret *corev1.Secret) (string, bool) {
serviceName := secret.Annotations[api.ServiceNameAnnotation]
return serviceName, len(serviceName) != 0
}
hasSecretNameAnnotation := func(obj metav1.Object) bool {
return len(obj.GetAnnotations()[api.SecretNameAnnotation]) != 0
}
hasSecretNameAnnotationUpdate := func(oldObj, newObj metav1.Object) bool {
return hasSecretNameAnnotation(newObj) || hasSecretNameAnnotation(oldObj)
}
return controllerlib.New(config,
controllerlib.WithInformer(services, controllerlib.FilterFuncs{
AddFunc: hasSecretNameAnnotation,
UpdateFunc: hasSecretNameAnnotationUpdate,
}, controllerlib.InformerOption{}),
controllerlib.WithInformer(secrets, controllerlib.FilterFuncs{
ParentFunc: func(obj metav1.Object) controllerlib.Key {
secret := obj.(*corev1.Secret)
serviceName, _ := toServiceName(secret)
return controllerlib.Key{Namespace: secret.Namespace, Name: serviceName}
},
DeleteFunc: func(obj metav1.Object) bool {
secret := obj.(*corev1.Secret)
serviceName, ok := toServiceName(secret)
if !ok {
return false
}
service, err := serviceLister.Services(secret.Namespace).Get(serviceName)
if apierrors.IsNotFound(err) {
return false
}
if err != nil {
utilruntime.HandleError(fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err))
return false
}
klog.V(4).InfoS("recreating secret", "namespace", service.Namespace, "name", service.Name)
return true
},
}, controllerlib.InformerOption{}),
controllerlib.WithRecorder(recorder), // TODO actually use the recorder
)
}