Skip to content

Commit 3a2a37c

Browse files
committed
refactor: generate effective tls policy & certificate reconciler
Signed-off-by: KevFan <[email protected]>
1 parent 18e3fb7 commit 3a2a37c

5 files changed

+381
-297
lines changed

api/v1/tlspolicy_types.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ func (p *TLSPolicy) GetMergeStrategy() machinery.MergeStrategy {
163163
}
164164
}
165165

166-
func (p *TLSPolicy) Merge(other machinery.Policy) machinery.Policy {
167-
return other
166+
func (p *TLSPolicy) Merge(_ machinery.Policy) machinery.Policy {
167+
return p
168168
}
169169

170170
func (p *TLSPolicy) GetLocator() string {
+314
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"reflect"
8+
"sync"
9+
10+
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
11+
"github.com/go-logr/logr"
12+
"github.com/kuadrant/policy-machinery/controller"
13+
"github.com/kuadrant/policy-machinery/machinery"
14+
"github.com/samber/lo"
15+
corev1 "k8s.io/api/core/v1"
16+
apierrors "k8s.io/apimachinery/pkg/api/errors"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18+
"k8s.io/apimachinery/pkg/types"
19+
"k8s.io/apimachinery/pkg/util/validation/field"
20+
"k8s.io/client-go/dynamic"
21+
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"
22+
23+
kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1"
24+
)
25+
26+
type CertificateReconciler struct {
27+
client *dynamic.DynamicClient
28+
}
29+
30+
func NewCertificateReconciler(client *dynamic.DynamicClient) *CertificateReconciler {
31+
return &CertificateReconciler{client: client}
32+
}
33+
34+
func (t *CertificateReconciler) Subscription() *controller.Subscription {
35+
return &controller.Subscription{
36+
Events: []controller.ResourceEventMatcher{
37+
{Kind: &machinery.GatewayGroupKind},
38+
{Kind: &kuadrantv1.TLSPolicyGroupKind},
39+
{Kind: &CertManagerCertificateKind},
40+
},
41+
ReconcileFunc: t.Reconcile,
42+
}
43+
}
44+
45+
type CertTarget struct {
46+
cert *certmanagerv1.Certificate
47+
target machinery.Targetable
48+
}
49+
50+
func (t *CertificateReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, s *sync.Map) error {
51+
logger := controller.LoggerFromContext(ctx).WithName("CertificateReconciler").WithName("Reconcile")
52+
53+
effectivePolicies, ok := s.Load(StateEffectiveTLSPolicies)
54+
if !ok {
55+
logger.Error(errors.New("missing effective tls policies"), "failed to reconcile certificate objects")
56+
return nil
57+
}
58+
effectivePoliciesMap := effectivePolicies.(EffectiveTLSPolicies)
59+
60+
certs := getCertificatesFromTopology(topology)
61+
62+
var certTargets []CertTarget
63+
for _, effectivePolicy := range effectivePoliciesMap {
64+
if len(effectivePolicy.Path) != 2 {
65+
logger.Error(errors.New("invalid effective policy"), "failed to reconcile certificate objects")
66+
continue
67+
}
68+
69+
target := effectivePolicy.Path[1]
70+
71+
l, ok := target.(*machinery.Listener)
72+
if !ok {
73+
return fmt.Errorf("unexpected type %T", target)
74+
}
75+
76+
if err := validateGatewayListenerBlock(field.NewPath(""), *l.Listener, l.Gateway).ToAggregate(); err != nil {
77+
logger.Info("Skipped a listener block: " + err.Error())
78+
continue
79+
}
80+
81+
hostname := getListenerHostname(l)
82+
83+
for _, certRef := range l.TLS.CertificateRefs {
84+
secretRef := getSecretReference(certRef, l)
85+
86+
cert := buildCertManagerCertificate(l, &effectivePolicy.Spec, secretRef, []string{hostname})
87+
certTargets = append(certTargets, CertTarget{target: l, cert: cert})
88+
}
89+
}
90+
91+
expectedCerts := t.reconcileCertificates(ctx, certTargets, topology, logger)
92+
93+
// Clean up orphaned certs
94+
uniqueExpectedCerts := lo.UniqBy(expectedCerts, func(item *certmanagerv1.Certificate) types.UID {
95+
return item.GetUID()
96+
})
97+
orphanedCerts, _ := lo.Difference(certs, uniqueExpectedCerts)
98+
for _, orphanedCert := range orphanedCerts {
99+
resource := t.client.Resource(CertManagerCertificatesResource).Namespace(orphanedCert.GetNamespace())
100+
if err := resource.Delete(ctx, orphanedCert.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
101+
logger.Error(err, "unable to delete orphaned certificate", "name", orphanedCert.GetName(), "namespace", orphanedCert.GetNamespace(), "uid", orphanedCert.GetUID())
102+
continue
103+
}
104+
}
105+
106+
return nil
107+
}
108+
109+
func (t *CertificateReconciler) reconcileCertificates(ctx context.Context, certTargets []CertTarget, topology *machinery.Topology, logger logr.Logger) []*certmanagerv1.Certificate {
110+
expectedCerts := make([]*certmanagerv1.Certificate, 0, len(certTargets))
111+
for _, certTarget := range certTargets {
112+
resource := t.client.Resource(CertManagerCertificatesResource).Namespace(certTarget.cert.GetNamespace())
113+
114+
// Check is cert already in topology
115+
objs := topology.Objects().Children(certTarget.target)
116+
obj, ok := lo.Find(objs, func(o machinery.Object) bool {
117+
return o.GroupVersionKind().GroupKind() == CertManagerCertificateKind && o.GetNamespace() == certTarget.cert.GetNamespace() && o.GetName() == certTarget.cert.GetName()
118+
})
119+
120+
// Create
121+
if !ok {
122+
expectedCerts = append(expectedCerts, certTarget.cert)
123+
un, err := controller.Destruct(certTarget.cert)
124+
if err != nil {
125+
logger.Error(err, "unable to destruct cert")
126+
continue
127+
}
128+
_, err = resource.Create(ctx, un, metav1.CreateOptions{})
129+
if err != nil {
130+
logger.Error(err, "unable to create certificate", "name", certTarget.cert.GetName(), "namespace", certTarget.cert.GetNamespace(), "uid", certTarget.target.GetLocator())
131+
}
132+
133+
continue
134+
}
135+
136+
// Update
137+
tCert := obj.(*controller.RuntimeObject).Object.(*certmanagerv1.Certificate)
138+
expectedCerts = append(expectedCerts, tCert)
139+
if reflect.DeepEqual(tCert.Spec, certTarget.cert.Spec) {
140+
logger.V(1).Info("skipping update, cert specs are the same, nothing to do")
141+
continue
142+
}
143+
144+
tCert.Spec = certTarget.cert.Spec
145+
un, err := controller.Destruct(tCert)
146+
if err != nil {
147+
logger.Error(err, "unable to destruct cert")
148+
continue
149+
}
150+
_, err = resource.Update(ctx, un, metav1.UpdateOptions{})
151+
if err != nil {
152+
logger.Error(err, "unable to update certificate", "name", certTarget.cert.GetName(), "namespace", certTarget.cert.GetNamespace(), "uid", certTarget.target.GetLocator())
153+
}
154+
}
155+
return expectedCerts
156+
}
157+
158+
func getCertificatesFromTopology(topology *machinery.Topology) []*certmanagerv1.Certificate {
159+
return lo.FilterMap(topology.Objects().Items(), func(item machinery.Object, _ int) (*certmanagerv1.Certificate, bool) {
160+
r, ok := item.(*controller.RuntimeObject)
161+
if !ok {
162+
return nil, false
163+
}
164+
c, ok := r.Object.(*certmanagerv1.Certificate)
165+
return c, ok
166+
})
167+
}
168+
169+
func getListenerHostname(l *machinery.Listener) string {
170+
hostname := "*"
171+
if l.Hostname != nil {
172+
hostname = string(*l.Hostname)
173+
}
174+
return hostname
175+
}
176+
177+
func getSecretReference(certRef gatewayapiv1.SecretObjectReference, l *machinery.Listener) corev1.ObjectReference {
178+
secretRef := corev1.ObjectReference{
179+
Name: string(certRef.Name),
180+
}
181+
if certRef.Namespace != nil {
182+
secretRef.Namespace = string(*certRef.Namespace)
183+
} else {
184+
secretRef.Namespace = l.GetNamespace()
185+
}
186+
return secretRef
187+
}
188+
189+
func buildCertManagerCertificate(l *machinery.Listener, tlsPolicy *kuadrantv1.TLSPolicy, secretRef corev1.ObjectReference, hosts []string) *certmanagerv1.Certificate {
190+
crt := &certmanagerv1.Certificate{
191+
ObjectMeta: metav1.ObjectMeta{
192+
Name: certName(l.Gateway.Name, l.Name),
193+
Namespace: secretRef.Namespace,
194+
Labels: CommonLabels(),
195+
},
196+
TypeMeta: metav1.TypeMeta{
197+
Kind: certmanagerv1.CertificateKind,
198+
APIVersion: certmanagerv1.SchemeGroupVersion.String(),
199+
},
200+
Spec: certmanagerv1.CertificateSpec{
201+
DNSNames: hosts,
202+
SecretName: secretRef.Name,
203+
IssuerRef: tlsPolicy.Spec.IssuerRef,
204+
Usages: certmanagerv1.DefaultKeyUsages(),
205+
},
206+
}
207+
translatePolicy(crt, tlsPolicy.Spec)
208+
return crt
209+
}
210+
211+
// https://cert-manager.io/docs/usage/gateway/#supported-annotations
212+
// Helper functions largely based on cert manager https://github.com/cert-manager/cert-manager/blob/master/pkg/controller/certificate-shim/sync.go
213+
214+
func validateGatewayListenerBlock(path *field.Path, l gatewayapiv1.Listener, ingLike metav1.Object) field.ErrorList {
215+
var errs field.ErrorList
216+
217+
if l.Hostname == nil || *l.Hostname == "" {
218+
errs = append(errs, field.Required(path.Child("hostname"), "the hostname cannot be empty"))
219+
}
220+
221+
if l.TLS == nil {
222+
errs = append(errs, field.Required(path.Child("tls"), "the TLS block cannot be empty"))
223+
return errs
224+
}
225+
226+
if len(l.TLS.CertificateRefs) == 0 {
227+
errs = append(errs, field.Required(path.Child("tls").Child("certificateRef"),
228+
"listener has no certificateRefs"))
229+
} else {
230+
// check that each CertificateRef is valid
231+
for i, secretRef := range l.TLS.CertificateRefs {
232+
if *secretRef.Group != "core" && *secretRef.Group != "" {
233+
errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Index(i).Child("group"),
234+
*secretRef.Group, []string{"core", ""}))
235+
}
236+
237+
if *secretRef.Kind != "Secret" && *secretRef.Kind != "" {
238+
errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Index(i).Child("kind"),
239+
*secretRef.Kind, []string{"Secret", ""}))
240+
}
241+
242+
if secretRef.Namespace != nil && string(*secretRef.Namespace) != ingLike.GetNamespace() {
243+
errs = append(errs, field.Invalid(path.Child("tls").Child("certificateRef").Index(i).Child("namespace"),
244+
*secretRef.Namespace, "cross-namespace secret references are not allowed in listeners"))
245+
}
246+
}
247+
}
248+
249+
if l.TLS.Mode == nil {
250+
errs = append(errs, field.Required(path.Child("tls").Child("mode"),
251+
"the mode field is required"))
252+
} else {
253+
if *l.TLS.Mode != gatewayapiv1.TLSModeTerminate {
254+
errs = append(errs, field.NotSupported(path.Child("tls").Child("mode"),
255+
*l.TLS.Mode, []string{string(gatewayapiv1.TLSModeTerminate)}))
256+
}
257+
}
258+
259+
return errs
260+
}
261+
262+
// translatePolicy updates the Certificate spec using the TLSPolicy spec
263+
// converted from https://github.com/cert-manager/cert-manager/blob/master/pkg/controller/certificate-shim/helper.go#L63
264+
func translatePolicy(crt *certmanagerv1.Certificate, tlsPolicy kuadrantv1.TLSPolicySpec) {
265+
if tlsPolicy.CommonName != "" {
266+
crt.Spec.CommonName = tlsPolicy.CommonName
267+
}
268+
269+
if tlsPolicy.Duration != nil {
270+
crt.Spec.Duration = tlsPolicy.Duration
271+
}
272+
273+
if tlsPolicy.RenewBefore != nil {
274+
crt.Spec.RenewBefore = tlsPolicy.RenewBefore
275+
}
276+
277+
if tlsPolicy.RenewBefore != nil {
278+
crt.Spec.RenewBefore = tlsPolicy.RenewBefore
279+
}
280+
281+
if tlsPolicy.Usages != nil {
282+
crt.Spec.Usages = tlsPolicy.Usages
283+
}
284+
285+
if tlsPolicy.RevisionHistoryLimit != nil {
286+
crt.Spec.RevisionHistoryLimit = tlsPolicy.RevisionHistoryLimit
287+
}
288+
289+
if tlsPolicy.PrivateKey != nil {
290+
if crt.Spec.PrivateKey == nil {
291+
crt.Spec.PrivateKey = &certmanagerv1.CertificatePrivateKey{}
292+
}
293+
294+
if tlsPolicy.PrivateKey.Algorithm != "" {
295+
crt.Spec.PrivateKey.Algorithm = tlsPolicy.PrivateKey.Algorithm
296+
}
297+
298+
if tlsPolicy.PrivateKey.Encoding != "" {
299+
crt.Spec.PrivateKey.Encoding = tlsPolicy.PrivateKey.Encoding
300+
}
301+
302+
if tlsPolicy.PrivateKey.Size != 0 {
303+
crt.Spec.PrivateKey.Size = tlsPolicy.PrivateKey.Size
304+
}
305+
306+
if tlsPolicy.PrivateKey.RotationPolicy != "" {
307+
crt.Spec.PrivateKey.RotationPolicy = tlsPolicy.PrivateKey.RotationPolicy
308+
}
309+
}
310+
}
311+
312+
func certName(gatewayName string, listenerName gatewayapiv1.SectionName) string {
313+
return fmt.Sprintf("%s-%s", gatewayName, listenerName)
314+
}

0 commit comments

Comments
 (0)