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

147 lines
4.2 KiB
Go

// Copyright 2020 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package controller
import (
"context"
"fmt"
"reflect"
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"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/controllerlib/test/integration/examplecontroller/api"
)
func NewExampleUpdatingController(
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
toServiceName := func(secret *corev1.Secret) (string, bool) {
serviceName := secret.Annotations[api.ServiceNameAnnotation]
return serviceName, len(serviceName) != 0
}
ensureSecretData := func(service *corev1.Service, secretCopy *corev1.Secret) bool {
var needsUpdate bool
expectedData := map[string][]byte{
api.SecretDataKey: []byte(secretData),
}
if !reflect.DeepEqual(secretCopy.Data, expectedData) {
secretCopy.Data = expectedData
needsUpdate = true
}
expectedOwnerReferences := []metav1.OwnerReference{
{
APIVersion: "v1",
Kind: "Service",
Name: service.Name,
UID: service.UID,
},
}
if !reflect.DeepEqual(secretCopy.OwnerReferences, expectedOwnerReferences) {
secretCopy.OwnerReferences = expectedOwnerReferences
needsUpdate = true
}
return needsUpdate
}
isSecretValidForService := func(service *corev1.Service, secret *corev1.Secret) bool {
if service.Annotations[api.SecretNameAnnotation] != secret.Name {
return false
}
if secret.Annotations[api.ServiceUIDAnnotation] != string(service.UID) {
return false
}
return true
}
getServiceForSecret := func(secret *corev1.Secret) (*corev1.Service, error) {
serviceName, ok := toServiceName(secret)
if !ok {
return nil, nil
}
service, err := serviceLister.Services(secret.Namespace).Get(serviceName)
if apierrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("unable to get service %s/%s: %w", secret.Namespace, serviceName, err)
}
return service, nil
}
syncer := controllerlib.SyncFunc(func(ctx controllerlib.Context) error {
secret, err := secretLister.Secrets(ctx.Key.Namespace).Get(ctx.Key.Name)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return fmt.Errorf("unable to get the secret %s/%s: %w", secret.Namespace, secret.Name, err)
}
service, err := getServiceForSecret(secret)
if err != nil || service == nil {
return err
}
if !isSecretValidForService(service, secret) {
utilruntime.HandleError(fmt.Errorf("secret %s/%s does not have corresponding service UID %v", secret.Namespace, secret.Name, service.UID))
return nil
}
// make a copy to avoid mutating cache state
secretCopy := secret.DeepCopy()
if needsUpdate := ensureSecretData(service, secretCopy); needsUpdate {
_, updateErr := secretClient.Secrets(secretCopy.Namespace).Update(context.TODO(), secretCopy, metav1.UpdateOptions{})
return updateErr
}
return nil
})
config := controllerlib.Config{
Name: "example-controller-updating",
Syncer: syncer,
}
addSecret := func(obj metav1.Object) bool {
secret := obj.(*corev1.Secret)
_, ok := toServiceName(secret)
return ok
}
return controllerlib.New(config,
controllerlib.WithInformer(services, controllerlib.FilterFuncs{}, controllerlib.InformerOption{SkipEvents: true}),
controllerlib.WithInformer(secrets, controllerlib.FilterFuncs{
AddFunc: addSecret,
UpdateFunc: func(oldObj, newObj metav1.Object) bool {
return addSecret(newObj) || addSecret(oldObj)
},
}, controllerlib.InformerOption{}),
controllerlib.WithRecorder(recorder), // TODO actually use the recorder
)
}