diff --git a/controllers/state_of_the_world.go b/controllers/state_of_the_world.go index 5af13f77f..830d526f5 100644 --- a/controllers/state_of_the_world.go +++ b/controllers/state_of_the_world.go @@ -16,6 +16,7 @@ import ( istioclientnetworkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" istioclientgosecurityv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -125,7 +126,11 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D CertManagerIssuerKind, CertManagerClusterIssuerKind, ), - // TODO: add object links + controller.WithObjectLinks( + LinkGatewayToCertificateFunc, + LinkGatewayToIssuerFunc, + LinkGatewayToClusterIssuerFunc, + ), ) // TODO: add tls policy specific tasks to workflow } @@ -135,9 +140,14 @@ func NewPolicyMachineryController(manager ctrlruntime.Manager, client *dynamic.D func buildReconciler(client *dynamic.DynamicClient) controller.ReconcileFunc { reconciler := &controller.Workflow{ - Precondition: NewEventLogger().Log, + Precondition: (&controller.Workflow{ + Precondition: NewEventLogger().Log, + Tasks: []controller.ReconcileFunc{ + NewTopologyFileReconciler(client, operatorNamespace).Reconcile, + }, + }).Run, Tasks: []controller.ReconcileFunc{ - NewTopologyFileReconciler(client, operatorNamespace).Reconcile, + NewTLSPolicyWorkflow(client).Run, }, } return reconciler.Run @@ -181,6 +191,11 @@ func (r *TopologyFileReconciler) Reconcile(ctx context.Context, _ []controller.R if len(existingTopologyConfigMaps) == 0 { _, err := r.Client.Resource(controller.ConfigMapsResource).Namespace(cm.Namespace).Create(ctx, unstructuredCM, metav1.CreateOptions{}) if err != nil { + if errors.IsAlreadyExists(err) { + // This error can happen when the operator is starting, and the create event for the topology has not being processed. + logger.Info("already created topology configmap, must not be in topology yet") + return nil + } logger.Error(err, "failed to write topology configmap") } return err @@ -234,3 +249,18 @@ func (e *EventLogger) Log(ctx context.Context, resourceEvents []controller.Resou return nil } + +func NewTLSPolicyWorkflow(client *dynamic.DynamicClient) *controller.Workflow { + validation := NewValidateTLSPolicyTask() + status := NewTLSPolicyStatusTask(client) + return &controller.Workflow{ + Precondition: (&controller.Subscription{ + ReconcileFunc: validation.Reconcile, + Events: validation.Subscription(), + }).Reconcile, + Postcondition: (&controller.Subscription{ + ReconcileFunc: status.Reconcile, + Events: status.Subscription(), + }).Reconcile, + } +} diff --git a/controllers/tlspolicy_certmanager_certificates.go b/controllers/tlspolicy_certmanager_certificates.go index c9c65aa52..4a98e6656 100644 --- a/controllers/tlspolicy_certmanager_certificates.go +++ b/controllers/tlspolicy_certmanager_certificates.go @@ -33,7 +33,7 @@ func (r *TLSPolicyReconciler) reconcileCertificates(ctx context.Context, tlsPoli // Reconcile Certificates for each gateway directly referred by the policy (existing and new) for _, gw := range append(gwDiffObj.GatewaysWithValidPolicyRef, gwDiffObj.GatewaysMissingPolicyRef...) { log.V(1).Info("reconcileCertificates: gateway with valid or missing policy ref", "key", gw.Key()) - expectedCertificates := r.expectedCertificatesForGateway(ctx, gw.Gateway, tlsPolicy) + expectedCertificates := expectedCertificatesForGateway(ctx, gw.Gateway, tlsPolicy) if err := r.createOrUpdateGatewayCertificates(ctx, tlsPolicy, expectedCertificates); err != nil { return fmt.Errorf("error creating and updating expected certificates for gateway %v: %w", gw.Gateway.Name, err) } @@ -102,7 +102,7 @@ func (r *TLSPolicyReconciler) deleteUnexpectedCertificates(ctx context.Context, return nil } -func (r *TLSPolicyReconciler) expectedCertificatesForGateway(ctx context.Context, gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy) []*certmanv1.Certificate { +func expectedCertificatesForGateway(ctx context.Context, gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy) []*certmanv1.Certificate { log := crlog.FromContext(ctx) tlsHosts := make(map[corev1.ObjectReference][]string) @@ -130,12 +130,12 @@ func (r *TLSPolicyReconciler) expectedCertificatesForGateway(ctx context.Context certs := make([]*certmanv1.Certificate, 0, len(tlsHosts)) for secretRef, hosts := range tlsHosts { - certs = append(certs, r.buildCertManagerCertificate(gateway, tlsPolicy, secretRef, hosts)) + certs = append(certs, buildCertManagerCertificate(gateway, tlsPolicy, secretRef, hosts)) } return certs } -func (r *TLSPolicyReconciler) buildCertManagerCertificate(gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy, secretRef corev1.ObjectReference, hosts []string) *certmanv1.Certificate { +func buildCertManagerCertificate(gateway *gatewayapiv1.Gateway, tlsPolicy *v1alpha1.TLSPolicy, secretRef corev1.ObjectReference, hosts []string) *certmanv1.Certificate { tlsCertLabels := commonTLSCertificateLabels(client.ObjectKeyFromObject(gateway), tlsPolicy) crt := &certmanv1.Certificate{ diff --git a/controllers/tlspolicy_controller.go b/controllers/tlspolicy_controller.go index 90ba0bc7d..d3de03131 100644 --- a/controllers/tlspolicy_controller.go +++ b/controllers/tlspolicy_controller.go @@ -21,11 +21,9 @@ import ( "fmt" "reflect" - "github.com/cert-manager/cert-manager/pkg/apis/certmanager" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,23 +37,12 @@ import ( "github.com/kuadrant/kuadrant-operator/api/v1alpha1" kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/pkg/library/gatewayapi" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" "github.com/kuadrant/kuadrant-operator/pkg/library/mappers" "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" ) const TLSPolicyFinalizer = "kuadrant.io/tls-policy" -var ( - CertManagerCertificatesResource = certmanagerv1.SchemeGroupVersion.WithResource("certificates") - CertManagerIssuersResource = certmanagerv1.SchemeGroupVersion.WithResource("issuers") - CertMangerClusterIssuersResource = certmanagerv1.SchemeGroupVersion.WithResource("clusterissuers") - - CertManagerCertificateKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.CertificateKind} - CertManagerIssuerKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.IssuerKind} - CertManagerClusterIssuerKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.ClusterIssuerKind} -) - // TLSPolicyReconciler reconciles a TLSPolicy object type TLSPolicyReconciler struct { *reconcilers.BaseReconciler @@ -99,7 +86,7 @@ func (r *TLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( if delResErr == nil { delResErr = err } - return r.reconcileStatus(ctx, tlsPolicy, targetReferenceObject, kuadrant.NewErrTargetNotFound(tlsPolicy.Kind(), tlsPolicy.GetTargetRef(), delResErr)) + return ctrl.Result{}, delResErr } return ctrl.Result{}, err } @@ -125,25 +112,9 @@ func (r *TLSPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return ctrl.Result{}, err } } - specErr := r.reconcileResources(ctx, tlsPolicy, targetReferenceObject) - statusResult, statusErr := r.reconcileStatus(ctx, tlsPolicy, targetReferenceObject, specErr) - - if specErr != nil { - return ctrl.Result{}, specErr - } - - if statusErr != nil { - return ctrl.Result{}, statusErr - } - - if statusResult.Requeue { - log.V(1).Info("Reconciling status not finished. Requeing.") - return statusResult, nil - } - - return statusResult, statusErr + return ctrl.Result{}, specErr } func (r *TLSPolicyReconciler) reconcileResources(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object) error { diff --git a/controllers/tlspolicy_links.go b/controllers/tlspolicy_links.go new file mode 100644 index 000000000..2f8c8624b --- /dev/null +++ b/controllers/tlspolicy_links.go @@ -0,0 +1,86 @@ +package controllers + +import ( + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func LinkGatewayToCertificateFunc(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), controller.ObjectAs[*gwapiv1.Gateway]) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: CertManagerCertificateKind, + Func: func(child machinery.Object) []machinery.Object { + o := child.(*controller.RuntimeObject) + cert := o.Object.(*certmanagerv1.Certificate) + + gateway, ok := lo.Find(gateways, func(item *gwapiv1.Gateway) bool { + for _, l := range item.Spec.Listeners { + if l.TLS != nil && l.TLS.CertificateRefs != nil { + for _, certRef := range l.TLS.CertificateRefs { + certRefNS := "" + if certRef.Namespace == nil { + certRefNS = item.GetNamespace() + } else { + certRefNS = string(*certRef.Namespace) + } + if certRefNS == cert.GetNamespace() && string(certRef.Name) == cert.GetName() { + return true + } + } + } + } + + return false + }) + + if ok { + return []machinery.Object{&machinery.Gateway{Gateway: gateway}} + } + + return nil + }, + } +} + +func LinkGatewayToIssuerFunc(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), controller.ObjectAs[*gwapiv1.Gateway]) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: CertManagerIssuerKind, + Func: func(child machinery.Object) []machinery.Object { + o := child.(*controller.RuntimeObject) + issuer := o.Object.(*certmanagerv1.Issuer) + + // TODO: Refine + gateway, ok := lo.Find(gateways, func(item *gwapiv1.Gateway) bool { + return item.GetNamespace() == issuer.GetNamespace() + }) + + if ok { + return []machinery.Object{&machinery.Gateway{Gateway: gateway}} + } + + return nil + }, + } +} + +func LinkGatewayToClusterIssuerFunc(objs controller.Store) machinery.LinkFunc { + gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), controller.ObjectAs[machinery.Object]) + + return machinery.LinkFunc{ + From: machinery.GatewayGroupKind, + To: CertManagerClusterIssuerKind, + Func: func(child machinery.Object) []machinery.Object { + o := child.(*controller.RuntimeObject) + _ = o.Object.(*certmanagerv1.ClusterIssuer) + return gateways + }, + } +} diff --git a/controllers/tlspolicy_status.go b/controllers/tlspolicy_status.go deleted file mode 100644 index b185b5944..000000000 --- a/controllers/tlspolicy_status.go +++ /dev/null @@ -1,162 +0,0 @@ -/* -Copyright 2024. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "errors" - "fmt" - "slices" - - certmanv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" - "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" - - "github.com/kuadrant/kuadrant-operator/api/v1alpha1" - "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" - "github.com/kuadrant/kuadrant-operator/pkg/library/reconcilers" - "github.com/kuadrant/kuadrant-operator/pkg/library/utils" -) - -func (r *TLSPolicyReconciler) reconcileStatus(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object, specErr error) (ctrl.Result, error) { - newStatus := r.calculateStatus(ctx, tlsPolicy, targetNetworkObject, specErr) - - equalStatus := equality.Semantic.DeepEqual(newStatus, tlsPolicy.Status) - if equalStatus && tlsPolicy.Generation == tlsPolicy.Status.ObservedGeneration { - return reconcile.Result{}, nil - } - - newStatus.ObservedGeneration = tlsPolicy.Generation - - tlsPolicy.Status = *newStatus - updateErr := r.Client().Status().Update(ctx, tlsPolicy) - if updateErr != nil { - // Ignore conflicts, resource might just be outdated. - if apierrors.IsConflict(updateErr) { - return ctrl.Result{Requeue: true}, nil - } - return ctrl.Result{}, updateErr - } - - if kuadrant.IsTargetNotFound(specErr) { - return ctrl.Result{Requeue: true}, nil - } - - return ctrl.Result{}, nil -} - -func (r *TLSPolicyReconciler) calculateStatus(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object, specErr error) *v1alpha1.TLSPolicyStatus { - newStatus := &v1alpha1.TLSPolicyStatus{ - // Copy initial conditions. Otherwise, status will always be updated - Conditions: slices.Clone(tlsPolicy.Status.Conditions), - ObservedGeneration: tlsPolicy.Status.ObservedGeneration, - } - - acceptedCond := kuadrant.AcceptedCondition(tlsPolicy, specErr) - meta.SetStatusCondition(&newStatus.Conditions, *acceptedCond) - - // Do not set enforced condition if Accepted condition is false - if meta.IsStatusConditionFalse(newStatus.Conditions, string(gatewayapiv1alpha2.PolicyReasonAccepted)) { - meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) - return newStatus - } - - enforcedCond := r.enforcedCondition(ctx, tlsPolicy, targetNetworkObject) - meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) - - return newStatus -} - -func (r *TLSPolicyReconciler) enforcedCondition(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object) *metav1.Condition { - if err := r.isIssuerReady(ctx, tlsPolicy); err != nil { - return kuadrant.EnforcedCondition(tlsPolicy, kuadrant.NewErrUnknown(tlsPolicy.Kind(), err), false) - } - - if err := r.isCertificatesReady(ctx, tlsPolicy, targetNetworkObject); err != nil { - return kuadrant.EnforcedCondition(tlsPolicy, kuadrant.NewErrUnknown(tlsPolicy.Kind(), err), false) - } - - return kuadrant.EnforcedCondition(tlsPolicy, nil, true) -} - -func (r *TLSPolicyReconciler) isIssuerReady(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy) error { - var conditions []certmanv1.IssuerCondition - - switch tlsPolicy.Spec.IssuerRef.Kind { - case "", certmanv1.IssuerKind: - issuer := &certmanv1.Issuer{} - if err := r.Client().Get(ctx, client.ObjectKey{Name: tlsPolicy.Spec.IssuerRef.Name, Namespace: tlsPolicy.Namespace}, issuer); err != nil { - return err - } - conditions = issuer.Status.Conditions - case certmanv1.ClusterIssuerKind: - issuer := &certmanv1.ClusterIssuer{} - if err := r.Client().Get(ctx, client.ObjectKey{Name: tlsPolicy.Spec.IssuerRef.Name}, issuer); err != nil { - return err - } - conditions = issuer.Status.Conditions - default: - return fmt.Errorf(`invalid value %q for issuerRef.kind. Must be empty, %q or %q`, tlsPolicy.Spec.IssuerRef.Kind, certmanv1.IssuerKind, certmanv1.ClusterIssuerKind) - } - - transformedCond := utils.Map(conditions, func(c certmanv1.IssuerCondition) metav1.Condition { - return metav1.Condition{Reason: c.Reason, Status: metav1.ConditionStatus(c.Status), Type: string(c.Type), Message: c.Message} - }) - - if !meta.IsStatusConditionTrue(transformedCond, string(certmanv1.IssuerConditionReady)) { - return errors.New("issuer not ready") - } - - return nil -} - -func (r *TLSPolicyReconciler) isCertificatesReady(ctx context.Context, tlsPolicy *v1alpha1.TLSPolicy, targetNetworkObject client.Object) error { - gwDiffObj, err := reconcilers.ComputeGatewayDiffs(ctx, r.Client(), tlsPolicy, targetNetworkObject) - if err != nil { - return err - } - - if len(gwDiffObj.GatewaysWithValidPolicyRef) == 0 { - return errors.New("no valid gateways found") - } - - for _, gw := range gwDiffObj.GatewaysWithValidPolicyRef { - expectedCertificates := r.expectedCertificatesForGateway(ctx, gw.Gateway, tlsPolicy) - - for _, cert := range expectedCertificates { - c := &certmanv1.Certificate{} - if err := r.Client().Get(ctx, client.ObjectKeyFromObject(cert), c); err != nil { - return err - } - conditions := utils.Map(c.Status.Conditions, func(c certmanv1.CertificateCondition) metav1.Condition { - return metav1.Condition{Reason: c.Reason, Status: metav1.ConditionStatus(c.Status), Type: string(c.Type), Message: c.Message} - }) - - if !meta.IsStatusConditionTrue(conditions, string(certmanv1.CertificateConditionReady)) { - return fmt.Errorf("certificate %s not ready", cert.Name) - } - } - } - - return nil -} diff --git a/controllers/tlspolicy_tasks.go b/controllers/tlspolicy_tasks.go new file mode 100644 index 000000000..8f373581c --- /dev/null +++ b/controllers/tlspolicy_tasks.go @@ -0,0 +1,286 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "slices" + "sync" + + "github.com/cert-manager/cert-manager/pkg/apis/certmanager" + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/kuadrant/policy-machinery/controller" + "github.com/kuadrant/policy-machinery/machinery" + "github.com/samber/lo" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" + gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + kuadrantv1alpha1 "github.com/kuadrant/kuadrant-operator/api/v1alpha1" + "github.com/kuadrant/kuadrant-operator/pkg/library/kuadrant" + "github.com/kuadrant/kuadrant-operator/pkg/library/utils" +) + +var ( + CertManagerCertificatesResource = certmanagerv1.SchemeGroupVersion.WithResource("certificates") + CertManagerIssuersResource = certmanagerv1.SchemeGroupVersion.WithResource("issuers") + CertMangerClusterIssuersResource = certmanagerv1.SchemeGroupVersion.WithResource("clusterissuers") + + CertManagerCertificateKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.CertificateKind} + CertManagerIssuerKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.IssuerKind} + CertManagerClusterIssuerKind = schema.GroupKind{Group: certmanager.GroupName, Kind: certmanagerv1.ClusterIssuerKind} +) + +type ValidateTLSPolicyTask struct{} + +func NewValidateTLSPolicyTask() *ValidateTLSPolicyTask { + return &ValidateTLSPolicyTask{} +} + +func (t *ValidateTLSPolicyTask) Subscription() []controller.ResourceEventMatcher { + return []controller.ResourceEventMatcher{ + {Kind: &machinery.GatewayGroupKind}, + {Kind: &kuadrantv1alpha1.TLSPolicyKind, EventType: ptr.To(controller.CreateEvent)}, + {Kind: &kuadrantv1alpha1.TLSPolicyKind, EventType: ptr.To(controller.UpdateEvent)}, + } +} +func (t *ValidateTLSPolicyTask) Reconcile(_ context.Context, events []controller.ResourceEvent, topology *machinery.Topology, _ error, s *sync.Map) error { + // TODO: Determine by event type instead ? + // Get all TLS Policies + policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1alpha1.TLSPolicy, bool) { + p, ok := item.(*kuadrantv1alpha1.TLSPolicy) + return p, ok + }) + + // Get all gateways + gws := lo.FilterMap(topology.Targetables().Items(), func(item machinery.Targetable, index int) (*machinery.Gateway, bool) { + gw, ok := item.(*machinery.Gateway) + return gw, ok + }) + + for _, policy := range policies { + // TODO: Should only be one target ref for now, but what would happen if we support multiple target refs + targetRefs := policy.GetTargetRefs() + for _, targetRef := range targetRefs { + // Find gateway defined by target ref + _, ok := lo.Find(gws, func(item *machinery.Gateway) bool { + if item.GetName() == targetRef.GetName() && item.GetNamespace() == targetRef.GetNamespace() { + return true + } + return false + }) + + // Can't find target ref + if !ok { + s.Store(TLSPolicyValidKey(policy.GetUID()), false) + continue + } + + s.Store(TLSPolicyValidKey(policy.GetUID()), true) + } + } + + return nil +} + +type TLSPolicyStatusTask struct { + Client *dynamic.DynamicClient +} + +func NewTLSPolicyStatusTask(client *dynamic.DynamicClient) *TLSPolicyStatusTask { + return &TLSPolicyStatusTask{Client: client} +} + +func (t *TLSPolicyStatusTask) Subscription() []controller.ResourceEventMatcher { + return []controller.ResourceEventMatcher{ + {Kind: &machinery.GatewayGroupKind}, + {Kind: &kuadrantv1alpha1.TLSPolicyKind, EventType: ptr.To(controller.CreateEvent)}, + {Kind: &kuadrantv1alpha1.TLSPolicyKind, EventType: ptr.To(controller.UpdateEvent)}, + {Kind: &CertManagerCertificateKind}, + {Kind: &CertManagerIssuerKind}, + {Kind: &CertManagerClusterIssuerKind}, + } +} + +func (t *TLSPolicyStatusTask) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, s *sync.Map) error { + logger := controller.LoggerFromContext(ctx).WithName("TLSPolicyStatusTask") + + // Get all TLS Policies + policies := lo.FilterMap(topology.Policies().Items(), func(item machinery.Policy, index int) (*kuadrantv1alpha1.TLSPolicy, bool) { + p, ok := item.(*kuadrantv1alpha1.TLSPolicy) + return p, ok + }) + + for _, policy := range policies { + // policy is being deleted, nothing to do + if policy.DeletionTimestamp != nil { + continue + } + + newStatus := &kuadrantv1alpha1.TLSPolicyStatus{ + // Copy initial conditions. Otherwise, status will always be updated + Conditions: slices.Clone(policy.Status.Conditions), + ObservedGeneration: policy.Status.ObservedGeneration, + } + + var err error + isValid, ok := s.Load(TLSPolicyValidKey(policy.GetUID())) + // Should not happen + if !ok { + err = fmt.Errorf("unable to find %s key in sync map", policy.GetUID()) + logger.Error(err, "unexpected error") + continue + } + // Target Ref not found + if !isValid.(bool) { + err = kuadrant.NewErrTargetNotFound(policy.Kind(), policy.GetTargetRef(), apierrors.NewNotFound(kuadrantv1alpha1.TLSPoliciesResource.GroupResource(), policy.GetName())) + } + + meta.SetStatusCondition(&newStatus.Conditions, *kuadrant.AcceptedCondition(policy, err)) + + // Do not set enforced condition if Accepted condition is false + if meta.IsStatusConditionFalse(newStatus.Conditions, string(gatewayapiv1alpha2.PolicyReasonAccepted)) { + meta.RemoveStatusCondition(&newStatus.Conditions, string(kuadrant.PolicyConditionEnforced)) + } else { + enforcedCond := t.enforcedCondition(ctx, policy, topology) + meta.SetStatusCondition(&newStatus.Conditions, *enforcedCond) + } + + // Nothing to do + equalStatus := equality.Semantic.DeepEqual(newStatus, policy.Status) + if equalStatus && policy.Generation == policy.Status.ObservedGeneration { + continue + } + newStatus.ObservedGeneration = policy.Generation + policy.Status = *newStatus + + resource := t.Client.Resource(kuadrantv1alpha1.TLSPoliciesResource).Namespace(policy.GetNamespace()) + un, err := controller.Destruct(policy) + if err != nil { + logger.Error(err, "unable to destruct policy") + continue + } + + _, err = resource.UpdateStatus(ctx, un, metav1.UpdateOptions{}) + if err != nil { + logger.Error(err, "unable to update status for TLSPolicy", "uid", policy.GetUID()) + } + } + + return nil +} + +func (t *TLSPolicyStatusTask) enforcedCondition(ctx context.Context, tlsPolicy *kuadrantv1alpha1.TLSPolicy, topology *machinery.Topology) *metav1.Condition { + if err := t.isIssuerReady(ctx, tlsPolicy, topology); err != nil { + return kuadrant.EnforcedCondition(tlsPolicy, kuadrant.NewErrUnknown(tlsPolicy.Kind(), err), false) + } + + if err := t.isCertificatesReady(ctx, tlsPolicy, topology); err != nil { + return kuadrant.EnforcedCondition(tlsPolicy, kuadrant.NewErrUnknown(tlsPolicy.Kind(), err), false) + } + + return kuadrant.EnforcedCondition(tlsPolicy, nil, true) +} + +func (t *TLSPolicyStatusTask) isIssuerReady(_ context.Context, tlsPolicy *kuadrantv1alpha1.TLSPolicy, topology *machinery.Topology) error { + // Get all gateways + gws := lo.FilterMap(topology.Targetables().Items(), func(item machinery.Targetable, index int) (*machinery.Gateway, bool) { + gw, ok := item.(*machinery.Gateway) + return gw, ok + }) + + // Find gateway defined by target ref + gw, _ := lo.Find(gws, func(item *machinery.Gateway) bool { + if item.GetName() == string(tlsPolicy.GetTargetRef().Name) && item.GetNamespace() == tlsPolicy.GetNamespace() { + return true + } + return false + }) + + var conditions []certmanagerv1.IssuerCondition + + switch tlsPolicy.Spec.IssuerRef.Kind { + case "", certmanagerv1.IssuerKind: + objs := topology.Objects().Children(gw) + obj, _ := lo.Find(objs, func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == CertManagerIssuerKind && o.GetNamespace() == tlsPolicy.GetNamespace() && o.GetName() == tlsPolicy.Spec.IssuerRef.Name + }) + issuer := obj.(*controller.RuntimeObject).Object.(*certmanagerv1.Issuer) + + conditions = issuer.Status.Conditions + case certmanagerv1.ClusterIssuerKind: + // TODO: Why cant use gateway children + obj, _ := lo.Find(topology.Objects().Items(), func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == CertManagerClusterIssuerKind && o.GetName() == tlsPolicy.Spec.IssuerRef.Name + }) + issuer := obj.(*controller.RuntimeObject).Object.(*certmanagerv1.ClusterIssuer) + conditions = issuer.Status.Conditions + default: + return fmt.Errorf(`invalid value %q for issuerRef.kind. Must be empty, %q or %q`, tlsPolicy.Spec.IssuerRef.Kind, certmanagerv1.IssuerKind, certmanagerv1.ClusterIssuerKind) + } + + transformedCond := utils.Map(conditions, func(c certmanagerv1.IssuerCondition) metav1.Condition { + return metav1.Condition{Reason: c.Reason, Status: metav1.ConditionStatus(c.Status), Type: string(c.Type), Message: c.Message} + }) + + if !meta.IsStatusConditionTrue(transformedCond, string(certmanagerv1.IssuerConditionReady)) { + return errors.New("issuer not ready") + } + + return nil +} + +func (t *TLSPolicyStatusTask) isCertificatesReady(ctx context.Context, p machinery.Policy, topology *machinery.Topology) error { + tlsPolicy, ok := p.(*kuadrantv1alpha1.TLSPolicy) + if !ok { + return errors.New("invalid policy") + } + + // Get all gateways that contains this policy + gws := lo.FilterMap(topology.Targetables().Items(), func(item machinery.Targetable, index int) (*machinery.Gateway, bool) { + gw, ok := item.(*machinery.Gateway) + + return gw, ok && lo.Contains(gw.Policies(), p) + }) + + if len(gws) == 0 { + return errors.New("no valid gateways found") + } + + for _, gw := range gws { + expectedCertificates := expectedCertificatesForGateway(ctx, gw.Gateway, tlsPolicy) + + for _, cert := range expectedCertificates { + // TODO: Why cant use gateway + obj, ok := lo.Find(topology.Objects().Children(gw), func(o machinery.Object) bool { + return o.GroupVersionKind().GroupKind() == CertManagerCertificateKind && o.GetNamespace() == cert.GetNamespace() && o.GetName() == cert.GetName() + }) + + if !ok { + return errors.New("certificate not found") + } + + c := obj.(*controller.RuntimeObject).Object.(*certmanagerv1.Certificate) + + conditions := utils.Map(c.Status.Conditions, func(c certmanagerv1.CertificateCondition) metav1.Condition { + return metav1.Condition{Reason: c.Reason, Status: metav1.ConditionStatus(c.Status), Type: string(c.Type), Message: c.Message} + }) + + if !meta.IsStatusConditionTrue(conditions, string(certmanagerv1.CertificateConditionReady)) { + return fmt.Errorf("certificate %s not ready", cert.Name) + } + } + } + + return nil +} + +func TLSPolicyValidKey(uid types.UID) string { + return fmt.Sprintf("TLSPolicyValid:%s", uid) +} diff --git a/controllers/tlspolicy_tasks_test.go b/controllers/tlspolicy_tasks_test.go new file mode 100644 index 000000000..34d28f144 --- /dev/null +++ b/controllers/tlspolicy_tasks_test.go @@ -0,0 +1,33 @@ +package controllers + +import ( + "testing" + + "k8s.io/apimachinery/pkg/types" +) + +func TestTLSPolicyValidKey(t *testing.T) { + type args struct { + uid types.UID + } + tests := []struct { + name string + args args + want string + }{ + { + name: "test uid is appended", + args: args{ + types.UID("unqiueid"), + }, + want: "TLSPolicyValid:unqiueid", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := TLSPolicyValidKey(tt.args.uid); got != tt.want { + t.Errorf("TLSPolicyValidKey() = %v, want %v", got, tt.want) + } + }) + } +}