Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions controller/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ func emitChangeEvent(e events.EventEmitter, ch plan.Changes, reason events.Reaso
if e == nil {
return
}
for _, change := range ch.Create {
e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionCreate, reason))
for _, ep := range ch.Create {
e.Add(events.NewEventFromEndpoint(ep, events.ActionCreate, reason))
}
for _, change := range ch.UpdateNew {
e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionUpdate, reason))
for _, ep := range ch.UpdateNew {
e.Add(events.NewEventFromEndpoint(ep, events.ActionUpdate, reason))
}
for _, change := range ch.Delete {
e.Add(events.NewEvent(change.RefObject(), change.Describe(), events.ActionDelete, events.RecordDeleted))
for _, ep := range ch.Delete {
e.Add(events.NewEventFromEndpoint(ep, events.ActionDelete, events.RecordDeleted))
}
}
6 changes: 3 additions & 3 deletions controller/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ func TestEmit_RecordReady(t *testing.T) {
},
asserts: func(em *fake.EventEmitter, ch plan.Changes) {
for _, ep := range ch.Create {
em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionCreate, events.RecordReady))
em.AssertCalled(t, "Add", events.NewEventFromEndpoint(ep, events.ActionCreate, events.RecordReady))
}
for _, ep := range ch.Delete {
em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionDelete, events.RecordDeleted))
em.AssertCalled(t, "Add", events.NewEventFromEndpoint(ep, events.ActionDelete, events.RecordDeleted))
}
em.AssertNotCalled(t, "Add", mock.MatchedBy(func(e events.Event) bool {
return e.EventType() == events.EventTypeWarning
Expand All @@ -75,7 +75,7 @@ func TestEmit_RecordReady(t *testing.T) {
},
asserts: func(em *fake.EventEmitter, ch plan.Changes) {
for _, ep := range ch.Delete {
em.AssertCalled(t, "Add", events.NewEvent(ep.RefObject(), ep.Describe(), events.ActionDelete, events.RecordDeleted))
em.AssertCalled(t, "Add", events.NewEventFromEndpoint(ep, events.ActionDelete, events.RecordDeleted))
}
em.AssertCalled(t, "Add", mock.MatchedBy(func(e events.Event) bool {
return e.EventType() == events.EventTypeNormal &&
Expand Down
4 changes: 4 additions & 0 deletions docs/advanced/events.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
---
tags: ["advanced", "area/events", "events"]
---
# Kubernetes Events in External-DNS

External-DNS manages DNS records dynamically based on Kubernetes resources like Services and Ingresses.
Expand Down Expand Up @@ -31,6 +34,7 @@ kubectl describe service <name>
kubectl get events --field-selector involvedObject.kind=Service
kubectl get events --field-selector type=Normal|Warning
kubectl get events --field-selector reason=RecordReady|RecordDeleted|RecordError
kubectl get events --field-selector reportingComponent=external-dns
```

Or integrate with tools like:
Expand Down
29 changes: 29 additions & 0 deletions endpoint/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ type Endpoint struct {
// +optional
ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
// refObject stores reference object
// TODO: should be an array, as endpoints merged from multiple sources may have multiple ref objects
// +optional
refObject *ObjectRef `json:"-"`
}
Expand Down Expand Up @@ -524,3 +525,31 @@ func (t Targets) ValidateSRVRecord() bool {
}
return true
}

// GetDNSName returns the DNS name of the endpoint.
func (e *Endpoint) GetDNSName() string {
return e.DNSName
}

// GetRecordType returns the record type of the endpoint.
func (e *Endpoint) GetRecordType() string {
return e.RecordType
}

// GetRecordTTL returns the TTL of the endpoint as int64.
func (e *Endpoint) GetRecordTTL() int64 {
return int64(e.RecordTTL)
}

// GetTargets returns the targets of the endpoint.
func (e *Endpoint) GetTargets() []string {
return e.Targets
}

// GetOwner returns the owner of the endpoint from labels or set identifier.
func (e *Endpoint) GetOwner() string {
if val, ok := e.Labels[OwnerLabelKey]; ok {
return val
}
return e.SetIdentifier
}
2 changes: 1 addition & 1 deletion pkg/events/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func (ec *Controller) emit(event *eventsv1.Event) {
log.Debugf("skipping event %s/%s/%s with reason %s as not configured to emit", event.Kind, event.Namespace, event.Name, event.Reason)
return
}
event.ReportingController = controllerName + "-" + ec.hostname
event.ReportingController = controllerName
ec.queue.Add(event)
}

Expand Down
49 changes: 46 additions & 3 deletions pkg/events/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package events

import (
"fmt"
"reflect"
"regexp"
"slices"
"strings"
Expand All @@ -27,8 +28,10 @@ import (
apiv1 "k8s.io/api/core/v1"
eventsv1 "k8s.io/api/events/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes/scheme"
runtime "sigs.k8s.io/controller-runtime/pkg/client"
)

Expand Down Expand Up @@ -85,12 +88,34 @@ type (
emitEvents sets.Set[Reason]
dryRun bool
}

// EndpointInfo defines the interface for endpoint data needed to create events.
EndpointInfo interface {
GetDNSName() string
GetRecordType() string
GetRecordTTL() int64
GetTargets() []string
GetOwner() string
RefObject() *ObjectReference
}
)

func NewObjectReference(obj runtime.Object, source string) *ObjectReference {
// Kubernetes API doesn't populate TypeMeta (Kind/APIVersion) when retrieving
// objects via informers. Look up the Kind from the scheme without mutating the object.
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Kind == "" {
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
if err == nil && len(gvks) > 0 {
gvk = gvks[0]
} else {
// Fallback to reflection for types not in scheme
gvk = schema.GroupVersionKind{Kind: reflect.TypeOf(obj).Elem().Name()}
}
}
return &ObjectReference{
Kind: obj.GetObjectKind().GroupVersionKind().Kind,
ApiVersion: obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
Kind: gvk.Kind,
ApiVersion: gvk.GroupVersion().String(),
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
UID: obj.GetUID(),
Expand All @@ -112,6 +137,17 @@ func NewEvent(obj *ObjectReference, msg string, a Action, r Reason) Event {
}
}

// NewEventFromEndpoint creates an Event from an EndpointInfo with formatted message.
func NewEventFromEndpoint(ep EndpointInfo, a Action, r Reason) Event {
if ep == nil || ep.RefObject() == nil {
return Event{}
}
msg := fmt.Sprintf("(external-dns) record:%s,owner:%s,type:%s,ttl:%d,targets:%s",
ep.GetDNSName(), ep.GetOwner(), ep.GetRecordType(), ep.GetRecordTTL(),
strings.Join(ep.GetTargets(), ","))
return NewEvent(ep.RefObject(), msg, a, r)
}

func (e *Event) description() string {
return fmt.Sprintf("%s/%s/%s", e.ref.Kind, e.ref.Namespace, e.ref.Name)
}
Expand Down Expand Up @@ -141,10 +177,17 @@ func (e *Event) event() *eventsv1.Event {

timestamp := metav1.MicroTime{Time: time.Now()}

// Events are namespaced resources. For cluster-scoped objects like Nodes,
// the namespace is empty, so we default to "default" namespace.
namespace := e.ref.Namespace
if namespace == "" {
namespace = "default"
}

event := &eventsv1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: sanitize(e.ref.Name),
Namespace: e.ref.Namespace,
Namespace: namespace,
},
EventTime: timestamp,
ReportingInstance: controllerName + "/source/" + e.ref.Source,
Expand Down
Loading
Loading