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
5 changes: 3 additions & 2 deletions hcloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/cache"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/config"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/events"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/instance"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/metrics"
Expand All @@ -60,7 +61,7 @@ type cloud struct {
serverCache *cache.Cache[hcloud.Server]
lbTypeCache *cache.Cache[hcloud.LoadBalancerType]
cfg config.HCCMConfiguration
recorder record.EventRecorder
recorder *events.Recorder
networkID int64
cidr string
nodeLister corelisters.NodeLister
Expand Down Expand Up @@ -181,7 +182,7 @@ func (c *cloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder,
}()

recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "hcloud-cloud-controller-manager"})
c.recorder = recorder
c.recorder = events.NewRecorder(recorder)
}

func (c *cloud) Instances() (cloudprovider.Instances, bool) {
Expand Down
6 changes: 3 additions & 3 deletions hcloud/load_balancers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/config"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/events"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/lbspec"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/metrics"
Expand Down Expand Up @@ -41,10 +41,10 @@ type LoadBalancerOps interface {
type loadBalancers struct {
lbOps LoadBalancerOps
cfg *config.LoadBalancerConfiguration
recorder record.EventRecorder
recorder *events.Recorder
}

func newLoadBalancers(lbOps LoadBalancerOps, lbCfg *config.LoadBalancerConfiguration, recorder record.EventRecorder) *loadBalancers {
func newLoadBalancers(lbOps LoadBalancerOps, lbCfg *config.LoadBalancerConfiguration, recorder *events.Recorder) *loadBalancers {
return &loadBalancers{
lbOps: lbOps,
cfg: lbCfg,
Expand Down
3 changes: 2 additions & 1 deletion hcloud/load_balancers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/annotation"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/config"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/events"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/lbspec"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
Expand Down Expand Up @@ -989,7 +990,7 @@ func TestLoadBalancer_matchNodeSelector(t *testing.T) {

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
spec, err := lbspec.Resolve(record.NewFakeRecorder(10), c.service, config.LoadBalancerConfiguration{})
spec, err := lbspec.Resolve(events.NewRecorder(record.NewFakeRecorder(10)), c.service, config.LoadBalancerConfiguration{})
if err != nil {
t.Fatal(err)
}
Expand Down
5 changes: 3 additions & 2 deletions hcloud/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"k8s.io/client-go/tools/record"

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/config"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/events"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/hcops"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/lbspec"
"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/mocks"
Expand Down Expand Up @@ -83,13 +84,13 @@ func (tt *LoadBalancerTestCase) run(t *testing.T) {

// The error is ignored on purpose: a test case may set an invalid
// annotation to check how the cloud provider reports it.
tt.Spec, _ = lbspec.Resolve(&record.FakeRecorder{}, tt.Service, *lbConfig)
tt.Spec, _ = lbspec.Resolve(events.NewRecorder(&record.FakeRecorder{}), tt.Service, *lbConfig)

if tt.Mock != nil {
tt.Mock(t, tt)
}

tt.LoadBalancers = newLoadBalancers(tt.LBOps, lbConfig, &record.FakeRecorder{})
tt.LoadBalancers = newLoadBalancers(tt.LBOps, lbConfig, events.NewRecorder(&record.FakeRecorder{}))
tt.Perform(t, tt)

tt.LBOps.AssertExpectations(t)
Expand Down
31 changes: 31 additions & 0 deletions internal/events/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package events

type Reason string

// Instances controller event reasons.
const (
PossibleNodeDeletion Reason = "PossibleNodeDeletion"
InstanceLookupFailed Reason = "InstanceLookupFailed"
IgnoredExternalIPv6 Reason = "IgnoredExternalIPv6"
InvalidExternalIPv6 Reason = "InvalidExternalIPv6"
MisconfiguredInternalIP Reason = "MisconfiguredInternalIP"
)

// Route controller event reasons.
const (
ClusterCIDRMisconfigured Reason = "ClusterCIDRMisconfigured"
)

// Load balancer controller event reasons.
const (
PrimaryIPChangeUnsupported Reason = "PrimaryIPChangeUnsupported"
UnknownProviderIDPrefix Reason = "UnknownProviderIDPrefix"
InternalIPNotConfigured Reason = "InternalIPNotConfigured"
ExternalIPTargetReplaced Reason = "ExternalIPTargetReplaced"
ServerNotFound Reason = "ServerNotFound"
MaxTargetsReached Reason = "MaxTargetsReached"
UnsupportedProtocolConfigured Reason = "UnsupportedProtocolConfigured"
LoadBalancerTypeUnconfigured Reason = "LoadBalancerTypeUnconfigured"
LoadBalancerTypeDeprecated Reason = "LoadBalancerTypeDeprecated"
InvalidLoadBalancerAnnotation Reason = "InvalidLoadBalancerAnnotation"
)
69 changes: 69 additions & 0 deletions internal/events/recorder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package events

import (
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"
)

type Recorder struct {
recorder record.EventRecorder
}

func NewRecorder(recorder record.EventRecorder) *Recorder {
return &Recorder{recorder: recorder}
}

// Warnf emits a warning event and a structured info log message.
func (r *Recorder) Warnf(obj runtime.Object, reason Reason, format string, args ...any) {
r.emit(obj, corev1.EventTypeWarning, reason, fmt.Sprintf(format, args...))
}

// Normalf emits a normal event and a structured info log message.
func (r *Recorder) Normalf(obj runtime.Object, reason Reason, format string, args ...any) {
r.emit(obj, corev1.EventTypeNormal, reason, fmt.Sprintf(format, args...))
}

// Errorf emits a warning event and a structured error log message.
func (r *Recorder) Errorf(obj runtime.Object, reason Reason, format string, args ...any) {
msg := fmt.Sprintf(format, args...)
r.recorder.Event(obj, corev1.EventTypeWarning, string(reason), msg)
// Depth 1 skips Warnf/Normalf so file:line points at the caller.
klog.ErrorSDepth(1, nil, msg, logKeysAndValues(obj, corev1.EventTypeWarning, reason)...)
}

func (r *Recorder) emit(obj runtime.Object, eventType string, reason Reason, msg string) {
r.recorder.Event(obj, eventType, string(reason), msg)
// Depth 2 skips emit and Warnf/Normalf so file:line points at the caller.
klog.InfoSDepth(2, msg, logKeysAndValues(obj, eventType, reason)...)
}

func logKeysAndValues(obj runtime.Object, eventType string, reason Reason) []any {
return []any{
"kind", objKind(obj),
"object", objRef(obj),
"type", eventType,
"reason", string(reason),
}
}

func objRef(obj runtime.Object) any {
if m, ok := obj.(klog.KMetadata); ok {
return klog.KObj(m)
}
return fmt.Sprintf("%T", obj)
}

func objKind(obj runtime.Object) string {
if gvk := obj.GetObjectKind().GroupVersionKind(); gvk.Kind != "" {
return gvk.Kind
}
if gvks, _, err := scheme.Scheme.ObjectKinds(obj); err == nil && len(gvks) > 0 {
return gvks[0].Kind
}
return fmt.Sprintf("%T", obj)
}
146 changes: 146 additions & 0 deletions internal/events/recorder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package events

import (
"testing"

"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
"k8s.io/klog/v2"

"github.com/hetznercloud/hcloud-cloud-controller-manager/internal/testsupport"
)

type unregisteredObject struct {
metav1.TypeMeta
}

func (o *unregisteredObject) DeepCopyObject() runtime.Object {
return &unregisteredObject{TypeMeta: o.TypeMeta}
}

func TestRecorder(t *testing.T) {
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "my-node"}}
svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "my-svc", Namespace: "default"}}

tests := []struct {
name string
emit func(r *Recorder)
expectedEvent string
expectedLevel string
expectedLog string
}{
{
name: "warning",
emit: func(r *Recorder) {
r.Warnf(node, PossibleNodeDeletion, "node %s might be deleted", node.Name)
},
expectedEvent: "Warning PossibleNodeDeletion node my-node might be deleted",
expectedLevel: "I",
expectedLog: `"node my-node might be deleted" kind="Node" object="my-node" type="Warning" reason="PossibleNodeDeletion"`,
},
{
name: "normal",
emit: func(r *Recorder) {
r.Normalf(svc, LoadBalancerTypeDeprecated, "type %s is deprecated", "lb11")
},
expectedEvent: "Normal LoadBalancerTypeDeprecated type lb11 is deprecated",
expectedLevel: "I",
expectedLog: `"type lb11 is deprecated" kind="Service" object="default/my-svc" type="Normal" reason="LoadBalancerTypeDeprecated"`,
},
{
name: "error",
emit: func(r *Recorder) {
r.Errorf(svc, InvalidLoadBalancerAnnotation, "Invalid Load Balancer annotation %s", "foo")
},
expectedEvent: "Warning InvalidLoadBalancerAnnotation Invalid Load Balancer annotation foo",
expectedLevel: "E",
expectedLog: `"Invalid Load Balancer annotation foo" kind="Service" object="default/my-svc" type="Warning" reason="InvalidLoadBalancerAnnotation"`,
},
{
name: "without args",
emit: func(r *Recorder) {
r.Warnf(svc, InternalIPNotConfigured, "no InternalIP found")
},
expectedEvent: "Warning InternalIPNotConfigured no InternalIP found",
expectedLevel: "I",
expectedLog: `"no InternalIP found" kind="Service" object="default/my-svc" type="Warning" reason="InternalIPNotConfigured"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logs := testsupport.CaptureKlog(t)
fakeRecorder := record.NewFakeRecorder(1)

tt.emit(NewRecorder(fakeRecorder))

testsupport.AssertEvent(t, fakeRecorder, tt.expectedEvent)
assert.Regexp(t, `^`+tt.expectedLevel+`\d`, logs.String())
assert.Contains(t, logs.String(), tt.expectedLog)
assert.Regexp(t, `recorder_test\.go:\d+\] `, logs.String())
})
}
}

func TestObjKind(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
expected string
}{
{
name: "from type meta",
obj: &corev1.Node{TypeMeta: metav1.TypeMeta{Kind: "CustomNode"}},
expected: "CustomNode",
},
{
name: "from scheme",
obj: &corev1.Service{},
expected: "Service",
},
{
name: "unregistered type",
obj: &unregisteredObject{},
expected: "*events.unregisteredObject",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, objKind(tt.obj))
})
}
}

func TestObjRef(t *testing.T) {
tests := []struct {
name string
obj runtime.Object
expected any
}{
{
name: "cluster scoped",
obj: &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "my-node"}},
expected: klog.KRef("", "my-node"),
},
{
name: "namespaced",
obj: &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "my-svc", Namespace: "default"}},
expected: klog.KRef("default", "my-svc"),
},
{
name: "without metadata",
obj: &unregisteredObject{},
expected: "*events.unregisteredObject",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, objRef(tt.obj))
})
}
}
Loading
Loading