Skip to content

Commit d0ee9bd

Browse files
committed
refactor: generate effective tls policy & certificate reconciler
Signed-off-by: KevFan <[email protected]>
1 parent 5dd9eb5 commit d0ee9bd

5 files changed

+382
-294
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 {
+309
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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+
hostname := getListenerHostname(l)
77+
78+
for _, certRef := range l.TLS.CertificateRefs {
79+
secretRef := getSecretReference(certRef, l)
80+
81+
cert := buildCertManagerCertificate(l, &effectivePolicy.Spec, secretRef, []string{hostname})
82+
certTargets = append(certTargets, CertTarget{target: l, cert: cert})
83+
}
84+
}
85+
86+
expectedCerts := t.reconcileCertificates(ctx, certTargets, topology, logger)
87+
88+
// Clean up orphaned certs
89+
uniqueExpectedCerts := lo.UniqBy(expectedCerts, func(item *certmanagerv1.Certificate) types.UID {
90+
return item.GetUID()
91+
})
92+
orphanedCerts, _ := lo.Difference(certs, uniqueExpectedCerts)
93+
for _, orphanedCert := range orphanedCerts {
94+
resource := t.client.Resource(CertManagerCertificatesResource).Namespace(orphanedCert.GetNamespace())
95+
if err := resource.Delete(ctx, orphanedCert.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
96+
logger.Error(err, "unable to delete orphaned certificate", "name", orphanedCert.GetName(), "namespace", orphanedCert.GetNamespace(), "uid", orphanedCert.GetUID())
97+
continue
98+
}
99+
}
100+
101+
return nil
102+
}
103+
104+
func (t *CertificateReconciler) reconcileCertificates(ctx context.Context, certTargets []CertTarget, topology *machinery.Topology, logger logr.Logger) []*certmanagerv1.Certificate {
105+
expectedCerts := make([]*certmanagerv1.Certificate, 0, len(certTargets))
106+
for _, certTarget := range certTargets {
107+
resource := t.client.Resource(CertManagerCertificatesResource).Namespace(certTarget.cert.GetNamespace())
108+
109+
// Check is cert already in topology
110+
objs := topology.Objects().Children(certTarget.target)
111+
obj, ok := lo.Find(objs, func(o machinery.Object) bool {
112+
return o.GroupVersionKind().GroupKind() == CertManagerCertificateKind && o.GetNamespace() == certTarget.cert.GetNamespace() && o.GetName() == certTarget.cert.GetName()
113+
})
114+
115+
// Create
116+
if !ok {
117+
expectedCerts = append(expectedCerts, certTarget.cert)
118+
un, err := controller.Destruct(certTarget.cert)
119+
if err != nil {
120+
logger.Error(err, "unable to destruct cert")
121+
continue
122+
}
123+
_, err = resource.Create(ctx, un, metav1.CreateOptions{})
124+
if err != nil {
125+
logger.Error(err, "unable to create certificate", "name", certTarget.cert.GetName(), "namespace", certTarget.cert.GetNamespace(), "uid", certTarget.target.GetLocator())
126+
}
127+
128+
continue
129+
}
130+
131+
// Update
132+
tCert := obj.(*controller.RuntimeObject).Object.(*certmanagerv1.Certificate)
133+
expectedCerts = append(expectedCerts, tCert)
134+
if reflect.DeepEqual(tCert.Spec, certTarget.cert.Spec) {
135+
logger.V(1).Info("skipping update, cert specs are the same, nothing to do")
136+
continue
137+
}
138+
139+
tCert.Spec = certTarget.cert.Spec
140+
un, err := controller.Destruct(tCert)
141+
if err != nil {
142+
logger.Error(err, "unable to destruct cert")
143+
continue
144+
}
145+
_, err = resource.Update(ctx, un, metav1.UpdateOptions{})
146+
if err != nil {
147+
logger.Error(err, "unable to update certificate", "name", certTarget.cert.GetName(), "namespace", certTarget.cert.GetNamespace(), "uid", certTarget.target.GetLocator())
148+
}
149+
}
150+
return expectedCerts
151+
}
152+
153+
func getCertificatesFromTopology(topology *machinery.Topology) []*certmanagerv1.Certificate {
154+
return lo.FilterMap(topology.Objects().Items(), func(item machinery.Object, _ int) (*certmanagerv1.Certificate, bool) {
155+
r, ok := item.(*controller.RuntimeObject)
156+
if !ok {
157+
return nil, false
158+
}
159+
c, ok := r.Object.(*certmanagerv1.Certificate)
160+
return c, ok
161+
})
162+
}
163+
164+
func getListenerHostname(l *machinery.Listener) string {
165+
hostname := "*"
166+
if l.Hostname != nil {
167+
hostname = string(*l.Hostname)
168+
}
169+
return hostname
170+
}
171+
172+
func getSecretReference(certRef gatewayapiv1.SecretObjectReference, l *machinery.Listener) corev1.ObjectReference {
173+
secretRef := corev1.ObjectReference{
174+
Name: string(certRef.Name),
175+
}
176+
if certRef.Namespace != nil {
177+
secretRef.Namespace = string(*certRef.Namespace)
178+
} else {
179+
secretRef.Namespace = l.GetNamespace()
180+
}
181+
return secretRef
182+
}
183+
184+
func buildCertManagerCertificate(l *machinery.Listener, tlsPolicy *kuadrantv1.TLSPolicy, secretRef corev1.ObjectReference, hosts []string) *certmanagerv1.Certificate {
185+
crt := &certmanagerv1.Certificate{
186+
ObjectMeta: metav1.ObjectMeta{
187+
Name: certName(l.Gateway.Name, l.Name),
188+
Namespace: secretRef.Namespace,
189+
Labels: CommonLabels(),
190+
},
191+
TypeMeta: metav1.TypeMeta{
192+
Kind: certmanagerv1.CertificateKind,
193+
APIVersion: certmanagerv1.SchemeGroupVersion.String(),
194+
},
195+
Spec: certmanagerv1.CertificateSpec{
196+
DNSNames: hosts,
197+
SecretName: secretRef.Name,
198+
IssuerRef: tlsPolicy.Spec.IssuerRef,
199+
Usages: certmanagerv1.DefaultKeyUsages(),
200+
},
201+
}
202+
translatePolicy(crt, tlsPolicy.Spec)
203+
return crt
204+
}
205+
206+
// https://cert-manager.io/docs/usage/gateway/#supported-annotations
207+
// Helper functions largely based on cert manager https://github.com/cert-manager/cert-manager/blob/master/pkg/controller/certificate-shim/sync.go
208+
209+
func validateGatewayListenerBlock(path *field.Path, l gatewayapiv1.Listener, ingLike metav1.Object) field.ErrorList {
210+
var errs field.ErrorList
211+
212+
if l.Hostname == nil || *l.Hostname == "" {
213+
errs = append(errs, field.Required(path.Child("hostname"), "the hostname cannot be empty"))
214+
}
215+
216+
if l.TLS == nil {
217+
errs = append(errs, field.Required(path.Child("tls"), "the TLS block cannot be empty"))
218+
return errs
219+
}
220+
221+
if len(l.TLS.CertificateRefs) == 0 {
222+
errs = append(errs, field.Required(path.Child("tls").Child("certificateRef"),
223+
"listener has no certificateRefs"))
224+
} else {
225+
// check that each CertificateRef is valid
226+
for i, secretRef := range l.TLS.CertificateRefs {
227+
if *secretRef.Group != "core" && *secretRef.Group != "" {
228+
errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Index(i).Child("group"),
229+
*secretRef.Group, []string{"core", ""}))
230+
}
231+
232+
if *secretRef.Kind != "Secret" && *secretRef.Kind != "" {
233+
errs = append(errs, field.NotSupported(path.Child("tls").Child("certificateRef").Index(i).Child("kind"),
234+
*secretRef.Kind, []string{"Secret", ""}))
235+
}
236+
237+
if secretRef.Namespace != nil && string(*secretRef.Namespace) != ingLike.GetNamespace() {
238+
errs = append(errs, field.Invalid(path.Child("tls").Child("certificateRef").Index(i).Child("namespace"),
239+
*secretRef.Namespace, "cross-namespace secret references are not allowed in listeners"))
240+
}
241+
}
242+
}
243+
244+
if l.TLS.Mode == nil {
245+
errs = append(errs, field.Required(path.Child("tls").Child("mode"),
246+
"the mode field is required"))
247+
} else {
248+
if *l.TLS.Mode != gatewayapiv1.TLSModeTerminate {
249+
errs = append(errs, field.NotSupported(path.Child("tls").Child("mode"),
250+
*l.TLS.Mode, []string{string(gatewayapiv1.TLSModeTerminate)}))
251+
}
252+
}
253+
254+
return errs
255+
}
256+
257+
// translatePolicy updates the Certificate spec using the TLSPolicy spec
258+
// converted from https://github.com/cert-manager/cert-manager/blob/master/pkg/controller/certificate-shim/helper.go#L63
259+
func translatePolicy(crt *certmanagerv1.Certificate, tlsPolicy kuadrantv1.TLSPolicySpec) {
260+
if tlsPolicy.CommonName != "" {
261+
crt.Spec.CommonName = tlsPolicy.CommonName
262+
}
263+
264+
if tlsPolicy.Duration != nil {
265+
crt.Spec.Duration = tlsPolicy.Duration
266+
}
267+
268+
if tlsPolicy.RenewBefore != nil {
269+
crt.Spec.RenewBefore = tlsPolicy.RenewBefore
270+
}
271+
272+
if tlsPolicy.RenewBefore != nil {
273+
crt.Spec.RenewBefore = tlsPolicy.RenewBefore
274+
}
275+
276+
if tlsPolicy.Usages != nil {
277+
crt.Spec.Usages = tlsPolicy.Usages
278+
}
279+
280+
if tlsPolicy.RevisionHistoryLimit != nil {
281+
crt.Spec.RevisionHistoryLimit = tlsPolicy.RevisionHistoryLimit
282+
}
283+
284+
if tlsPolicy.PrivateKey != nil {
285+
if crt.Spec.PrivateKey == nil {
286+
crt.Spec.PrivateKey = &certmanagerv1.CertificatePrivateKey{}
287+
}
288+
289+
if tlsPolicy.PrivateKey.Algorithm != "" {
290+
crt.Spec.PrivateKey.Algorithm = tlsPolicy.PrivateKey.Algorithm
291+
}
292+
293+
if tlsPolicy.PrivateKey.Encoding != "" {
294+
crt.Spec.PrivateKey.Encoding = tlsPolicy.PrivateKey.Encoding
295+
}
296+
297+
if tlsPolicy.PrivateKey.Size != 0 {
298+
crt.Spec.PrivateKey.Size = tlsPolicy.PrivateKey.Size
299+
}
300+
301+
if tlsPolicy.PrivateKey.RotationPolicy != "" {
302+
crt.Spec.PrivateKey.RotationPolicy = tlsPolicy.PrivateKey.RotationPolicy
303+
}
304+
}
305+
}
306+
307+
func certName(gatewayName string, listenerName gatewayapiv1.SectionName) string {
308+
return fmt.Sprintf("%s-%s", gatewayName, listenerName)
309+
}

0 commit comments

Comments
 (0)