Skip to content

Commit

Permalink
sotw: dnspolicy (Kuadrant#937)
Browse files Browse the repository at this point in the history
* sotw: dnspolicy init

Add basic setup for DNSPolicy state of the world tasks, dnsrecord types,
watcher and linker function (Listener -> DNSRecord)

* Update dns policy validator in preparation for status updates, adds
  correct errors for acceptance.
* Add common labels that get applied to all dnsrecord resources created
  by the kuadrant operator
* Add filter to topology for dnsrecords to only add records that contain

* sotw: dnspolicy delete orphan records

Move all logic to delete orphan dnsrecord resources for a DNSPolicy to
the sotw reconciler and based all decisions on the current topology.

Orphan record is one that no longer has a valid path between it's
owner DNSPolicy and itself in the topology. This covers the following
scenarios:

* Listener is deleted from the Gateway
* Gateway is deleted
* Policy is deleted(K8s will also deal with this due to the owner
  relationship)
* Policy ref is changed

Does not deal with the removal of records based on the state of the
gateway.

* sotw dnspolicy: status and dnspolicies reconciliation

* Bump policy-machinery v0.6.1

---------

Signed-off-by: Michael Nairn <mnairn@redhat.com>
Signed-off-by: R-Lawton <rlawton@redhat.com>
  • Loading branch information
mikenairn authored and R-Lawton committed Oct 29, 2024
1 parent b6494ca commit ae2844e
Show file tree
Hide file tree
Showing 21 changed files with 856 additions and 695 deletions.
26 changes: 24 additions & 2 deletions api/v1alpha1/dnspolicy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package v1alpha1

import (
"context"
"fmt"
"net"
"strings"

dnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -70,8 +73,23 @@ type DNSPolicySpec struct {

// ExcludeAddresses is a list of addresses (either hostnames, CIDR or IPAddresses) that DNSPolicy should not use as values in the configured DNS provider records. The default is to allow all addresses configured in the Gateway DNSPolicy is targeting
// +optional
// +kubebuilder:validation:MaxItems=20
ExcludeAddresses []string `json:"excludeAddresses,omitempty"`
ExcludeAddresses ExcludeAddresses `json:"excludeAddresses,omitempty"`
}

// +kubebuilder:validation:MaxItems=20
type ExcludeAddresses []string

func (ea ExcludeAddresses) Validate() error {
for _, exclude := range ea {
//Only a CIDR will have / in the address so attempt to parse fail if not valid
if strings.Contains(exclude, "/") {
_, _, err := net.ParseCIDR(exclude)
if err != nil {
return fmt.Errorf("could not parse the CIDR from the excludeAddresses field %w", err)
}
}
}
return nil
}

type LoadBalancingSpec struct {
Expand Down Expand Up @@ -159,6 +177,10 @@ type DNSPolicy struct {
Status DNSPolicyStatus `json:"status,omitempty"`
}

func (p *DNSPolicy) Validate() error {
return p.Spec.ExcludeAddresses.Validate()
}

func (p *DNSPolicy) GetWrappedNamespace() gatewayapiv1.Namespace {
return gatewayapiv1.Namespace(p.Namespace)
}
Expand Down
21 changes: 20 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions controllers/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package controllers

const (
KuadrantAppName = "kuadrant"
)

var (
AppLabelKey = "app"
AppLabelValue = KuadrantAppName
)

func CommonLabels() map[string]string {
return map[string]string{
AppLabelKey: AppLabelValue,
"app.kubernetes.io/component": KuadrantAppName,
"app.kubernetes.io/managed-by": "kuadrant-operator",
"app.kubernetes.io/instance": KuadrantAppName,
"app.kubernetes.io/name": KuadrantAppName,
"app.kubernetes.io/part-of": KuadrantAppName,
}
}
77 changes: 1 addition & 76 deletions controllers/dns_helper.go
Original file line number Diff line number Diff line change
@@ -1,101 +1,26 @@
package controllers

import (
"context"
"fmt"
"net"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayapiv1 "sigs.k8s.io/gateway-api/apis/v1"

kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1"
"github.com/kuadrant/dns-operator/pkg/builder"

"github.com/kuadrant/kuadrant-operator/api/v1alpha1"
)

const (
LabelGatewayReference = "kuadrant.io/gateway"
LabelGatewayNSRef = "kuadrant.io/gateway-namespace"
LabelListenerReference = "kuadrant.io/listener-name"
)

type dnsHelper struct {
client.Client
}

func commonDNSRecordLabels(gwKey client.ObjectKey, p *v1alpha1.DNSPolicy) map[string]string {
commonLabels := map[string]string{}
for k, v := range policyDNSRecordLabels(p) {
commonLabels[k] = v
}
for k, v := range gatewayDNSRecordLabels(gwKey) {
commonLabels[k] = v
}
return commonLabels
}

func policyDNSRecordLabels(p *v1alpha1.DNSPolicy) map[string]string {
return map[string]string{
p.DirectReferenceAnnotationName(): p.Name,
fmt.Sprintf("%s-namespace", p.DirectReferenceAnnotationName()): p.Namespace,
}
}

func gatewayDNSRecordLabels(gwKey client.ObjectKey) map[string]string {
return map[string]string{
LabelGatewayNSRef: gwKey.Namespace,
LabelGatewayReference: gwKey.Name,
}
}

// removeDNSForDeletedListeners remove any DNSRecords that are associated with listeners that no longer exist in this gateway
func (dh *dnsHelper) removeDNSForDeletedListeners(ctx context.Context, upstreamGateway *gatewayapiv1.Gateway) error {
dnsList := &kuadrantdnsv1alpha1.DNSRecordList{}
//List all dns records that belong to this gateway
labelSelector := &client.MatchingLabels{
LabelGatewayReference: upstreamGateway.Name,
}
if err := dh.List(ctx, dnsList, labelSelector, &client.ListOptions{Namespace: upstreamGateway.Namespace}); err != nil {
return err
}

for i, dnsRecord := range dnsList.Items {
listenerExists := false
rootHostMatches := false
for _, listener := range upstreamGateway.Spec.Listeners {
if listener.Name == gatewayapiv1.SectionName(dnsRecord.Labels[LabelListenerReference]) {
listenerExists = true
rootHostMatches = string(*listener.Hostname) == dnsRecord.Spec.RootHost
break
}
}
if !listenerExists || !rootHostMatches {
if err := dh.Delete(ctx, &dnsList.Items[i], &client.DeleteOptions{}); client.IgnoreNotFound(err) != nil {
return err
}
}
}
return nil
}

func dnsRecordName(gatewayName, listenerName string) string {
return fmt.Sprintf("%s-%s", gatewayName, listenerName)
}

func (dh *dnsHelper) deleteDNSRecordForListener(ctx context.Context, owner metav1.Object, listener gatewayapiv1.Listener) error {
recordName := dnsRecordName(owner.GetName(), string(listener.Name))
dnsRecord := kuadrantdnsv1alpha1.DNSRecord{
ObjectMeta: metav1.ObjectMeta{
Name: recordName,
Namespace: owner.GetNamespace(),
},
}
return dh.Delete(ctx, &dnsRecord, &client.DeleteOptions{})
}

// GatewayWrapper is a wrapper for gateway to implement interface form the builder
type GatewayWrapper struct {
*gatewayapiv1.Gateway
Expand All @@ -106,7 +31,7 @@ func NewGatewayWrapper(gateway *gatewayapiv1.Gateway) *GatewayWrapper {
return &GatewayWrapper{Gateway: gateway}
}

func (g GatewayWrapper) GetAddresses() []builder.TargetAddress {
func (g *GatewayWrapper) GetAddresses() []builder.TargetAddress {
addresses := make([]builder.TargetAddress, len(g.Status.Addresses))
for i, address := range g.Status.Addresses {
addresses[i] = builder.TargetAddress{
Expand Down
140 changes: 137 additions & 3 deletions controllers/dns_workflow.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,141 @@
package controllers

import "github.com/kuadrant/policy-machinery/controller"
import (
"fmt"
"sync"

func NewDNSWorkflow() *controller.Workflow {
return &controller.Workflow{}
"github.com/samber/lo"

"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

"github.com/kuadrant/policy-machinery/controller"
"github.com/kuadrant/policy-machinery/machinery"

kuadrantdnsv1alpha1 "github.com/kuadrant/dns-operator/api/v1alpha1"

"github.com/kuadrant/kuadrant-operator/api/v1alpha1"
"github.com/kuadrant/kuadrant-operator/pkg/library/utils"
)

const (
DNSRecordKind = "DNSRecord"
StateDNSPolicyAcceptedKey = "DNSPolicyValid"
StateDNSPolicyErrorsKey = "DNSPolicyErrors"
)

var (
DNSRecordResource = kuadrantdnsv1alpha1.GroupVersion.WithResource("dnsrecords")
DNSRecordGroupKind = schema.GroupKind{Group: kuadrantdnsv1alpha1.GroupVersion.Group, Kind: DNSRecordKind}
)

//+kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
//+kubebuilder:rbac:groups=kuadrant.io,resources=dnspolicies,verbs=get;list;watch;update;patch;delete
//+kubebuilder:rbac:groups=kuadrant.io,resources=dnspolicies/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=kuadrant.io,resources=dnspolicies/finalizers,verbs=update

//+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=kuadrant.io,resources=dnsrecords/status,verbs=get

func NewDNSWorkflow(client *dynamic.DynamicClient, scheme *runtime.Scheme) *controller.Workflow {
return &controller.Workflow{
Precondition: NewDNSPoliciesValidator().Subscription().Reconcile,
Tasks: []controller.ReconcileFunc{
NewEffectiveDNSPoliciesReconciler(client, scheme).Subscription().Reconcile,
},
Postcondition: NewDNSPolicyStatusUpdater(client).Subscription().Reconcile,
}
}

func LinkListenerToDNSRecord(objs controller.Store) machinery.LinkFunc {
gateways := lo.Map(objs.FilterByGroupKind(machinery.GatewayGroupKind), controller.ObjectAs[*gwapiv1.Gateway])
listeners := lo.FlatMap(lo.Map(gateways, func(g *gwapiv1.Gateway, _ int) *machinery.Gateway {
return &machinery.Gateway{Gateway: g}
}), machinery.ListenersFromGatewayFunc)

return machinery.LinkFunc{
From: machinery.ListenerGroupKind,
To: DNSRecordGroupKind,
Func: func(child machinery.Object) []machinery.Object {
return lo.FilterMap(listeners, func(l *machinery.Listener, _ int) (machinery.Object, bool) {
if dnsRecord, ok := child.(*controller.RuntimeObject).Object.(*kuadrantdnsv1alpha1.DNSRecord); ok {
return l, l.GetNamespace() == dnsRecord.GetNamespace() &&
dnsRecord.GetName() == dnsRecordName(l.Gateway.Name, string(l.Name))
}
return nil, false
})
},
}
}

func LinkDNSPolicyToDNSRecord(objs controller.Store) machinery.LinkFunc {
policies := lo.Map(objs.FilterByGroupKind(v1alpha1.DNSPolicyGroupKind), controller.ObjectAs[*v1alpha1.DNSPolicy])

return machinery.LinkFunc{
From: v1alpha1.DNSPolicyGroupKind,
To: DNSRecordGroupKind,
Func: func(child machinery.Object) []machinery.Object {
if dnsRecord, ok := child.(*controller.RuntimeObject).Object.(*kuadrantdnsv1alpha1.DNSRecord); ok {
return lo.FilterMap(policies, func(dnsPolicy *v1alpha1.DNSPolicy, _ int) (machinery.Object, bool) {
return dnsPolicy, utils.IsOwnedBy(dnsRecord, dnsPolicy)
})
}
return nil
},
}
}

func dnsPolicyAcceptedStatusFunc(state *sync.Map) func(policy machinery.Policy) (bool, error) {
validatedPolicies, validated := state.Load(StateDNSPolicyAcceptedKey)
if !validated {
return dnsPolicyAcceptedStatus
}
validatedPoliciesMap := validatedPolicies.(map[string]error)
return func(policy machinery.Policy) (bool, error) {
err, pValidated := validatedPoliciesMap[policy.GetLocator()]
if pValidated {
return err == nil, err
}
return dnsPolicyAcceptedStatus(policy)
}
}

func dnsPolicyAcceptedStatus(policy machinery.Policy) (accepted bool, err error) {
p, ok := policy.(*v1alpha1.DNSPolicy)
if !ok {
return
}
if condition := meta.FindStatusCondition(p.Status.Conditions, string(gatewayapiv1alpha2.PolicyConditionAccepted)); condition != nil {
accepted = condition.Status == metav1.ConditionTrue
if !accepted {
err = fmt.Errorf(condition.Message)
}
return
}
return
}

func dnsPolicyErrorFunc(state *sync.Map) func(policy machinery.Policy) error {
var policyErrorsMap map[string]error
policyErrors, exists := state.Load(StateDNSPolicyErrorsKey)
if exists {
policyErrorsMap = policyErrors.(map[string]error)
}
return func(policy machinery.Policy) error {
return policyErrorsMap[policy.GetLocator()]
}
}

type dnsPolicyTypeFilter func(item machinery.Policy, index int) (*v1alpha1.DNSPolicy, bool)

func dnsPolicyTypeFilterFunc() func(item machinery.Policy, _ int) (*v1alpha1.DNSPolicy, bool) {
return func(item machinery.Policy, _ int) (*v1alpha1.DNSPolicy, bool) {
p, ok := item.(*v1alpha1.DNSPolicy)
return p, ok
}
}
Loading

0 comments on commit ae2844e

Please sign in to comment.