/
creating.go
179 lines (154 loc) · 5.93 KB
/
creating.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// 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
)
}