Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/content/en/docs/documentation/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This section contains detailed documentation for all Java Operator SDK features
- **[Eventing](eventing/)** - Understanding the event-driven model
- **[Event filters](event-filters/)** - Filtering events and the default filter behavior
- **[Accessing Resources in Caches](working-with-es-caches/)** - How to access resources in caches
- **[Recording Kubernetes Events](kubernetes-events/)** - Surfacing what a controller does in `kubectl describe`
- **[Operations](operations/)** - Helm chart, metrics, logging, configurations, leader election
- **[Other Features](features/)** - Additional capabilities and integrations

Expand Down
115 changes: 115 additions & 0 deletions docs/content/en/docs/documentation/kubernetes-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Recording Kubernetes Events
weight: 51
---

A controller can record Kubernetes events, so that what it does shows up where users already look
for it: `kubectl describe` on the resource the event is about. Events are informational and best
effort by design, Kubernetes keeps them for a limited time and treats them as supplemental data, so
they complement the status of a resource rather than replace it.

## Recording from a reconciliation

Inside a reconciliation the recorder is available from the `Context`, bound to the primary resource:

```java
@Override
public UpdateControl<MyResource> reconcile(MyResource resource, Context<MyResource> context) {
context.eventRecorder().normal("Reconciled", "resource reconciled");
context.eventRecorder().warn("SomethingIsOff", "this is a warning about the resource");
return UpdateControl.noUpdate();
}
```

For more control, build an `EventRecord`, which also carries an optional action, reporting
component, labels and annotations:

```java
context.eventRecorder().record(EventRecord.builder()
.type(EventType.WARNING)
.reason("DeploymentFailed")
.message("could not scale the deployment")
.action("Scaling")
.build());
```

## Recording about another object

An event does not have to be about the primary resource. A controller reconciling a child resource
can, for example, record on the parent, so that `kubectl describe parent` shows what happened to its
children. Bind the recorder to the other object instead:

```java
var parent = context.getSecondaryResource(Parent.class).orElseThrow();
context.eventRecorder(parent).warn("ChildFailed", "could not reconcile child " + name);
```

The event references the given object as its involved object and lives in that object's namespace,
or, for a cluster scoped object, in the namespace configured for such events (see below). It is
still attributed to the controller recording it. Aggregation works per object: two children
recording the same reason and message on one parent count as repeats of a single event on the
parent, while different messages stay separate events.

## Recording outside of a reconciliation

From a background task or anywhere else no `Context` is at hand, use the recorder bound to the
controller, available from the `RegisteredController` returned when the controller is registered or
from `Operator#getRegisteredController(String)`. It records about any object:

```java
RegisteredController<MyResource> registered = operator.register(new MyReconciler());

registered.eventRecorder().record(someResource, EventRecord.normal("Noticed", "something happened"));
registered.eventRecorder().forResource(someResource).normal("Noticed", "something happened");
```

## What an event carries

The recorder fills in everything derivable from the controller and the object: the involved object
reference, the reporting controller (the controller name, or the reporting component set on the
record), the reporting instance (the leader election identity when configured, otherwise the host
name, which in a pod is the pod name), the timestamps and the count.

Repeats are aggregated, not duplicated. Events are named deterministically, after the object they
are about plus a hash of everything that identifies the event, so recording the same event again
resolves to the event already recorded and counts the occurrence on it. That is what makes `kubectl
describe` report a repeating event once, as `(x12 over 3m)`, instead of listing copies. Use
`EventRecord.Builder#key(...)` to control what counts as the same event, or configure the default
through `DefaultEventRecorder.builder(sink).keyStrategy(...)`, see the javadoc of
`EventKeyStrategy` and `EventNamingStrategy`.

Events about cluster scoped objects live in the namespace configured through
`withClusterScopedEventNamespace(...)` or the `josdk.events.cluster-scoped-namespace` property,
`default` unless configured otherwise. The API server only accepts `default` and `kube-system`.

## Failure handling

Recording is best effort by default: a failure to write the event is logged as a warning and
swallowed, and never fails the caller. To make such a failure fail the reconciliation instead, so
that it goes through the usual error handling and retries, configure the default recorder with
`failOnError(true)`. Outside of a reconciliation the exception then reaches whatever called the
recorder, for example the background task.

```java
KubernetesClient client = new KubernetesClientBuilder().build();
Operator operator = new Operator(override -> override
.withKubernetesClient(client)
.withEventRecorder(DefaultEventRecorder.builder(new DefaultEventSink(client))
.failOnError(true)
.build()));
```

## Custom recorders

To assemble events differently or send them somewhere else entirely, plug in an `EventRecorder` of
your own for the whole operator with `withEventRecorder(...)`. A custom recorder is shared by all
controllers, so it must be stateless and thread safe; it implements a single method,
`record(HasMetadata regarding, EventRecord event, ControllerConfiguration<?> controller)`, and gets
the bound views for free. Extend `DefaultEventRecorder` and override `toEvent(...)` to change how
events are assembled, or implement an `EventSink` to change where the assembled events go.

## Permissions

Recording needs `get`, `create` and `patch` on `events` in the core API group in every namespace
events are recorded in, see the
[Events section of the configuration page](operations/configuration.md#events).
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,9 @@ All operator-level keys are prefixed with `josdk.`.

#### Events

See [Recording Kubernetes Events](../kubernetes-events.md) for how events are recorded. The key
below configures where events about cluster scoped resources go.

| Key | Type | Description |
|---|---|---|
| `josdk.events.cluster-scoped-namespace` | `String` | Namespace to record events about cluster scoped resources in (defaults to `default`); the API server only accepts `default` and `kube-system` |
Expand Down
25 changes: 25 additions & 0 deletions docs/content/en/docs/migration/v5-7-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,28 @@ on one of the following:
executor supplier, `null` to have the event source create an executor of its own.
- `ExecutorServiceManager.scheduledExecutorService()` is deprecated. The framework doesn't use it
anymore, it creates the executor on first use, and shuts it down when the manager is stopped.

## Event recorder: events about any object

The event recorder can now record events about objects other than the primary resource, from a
reconciliation with `context.eventRecorder(otherObject)`, and outside of a reconciliation through
`RegisteredController#eventRecorder()`, see
[Recording Kubernetes Events](../documentation/kubernetes-events.md). Reconcilers that only use
`context.eventRecorder()` are not affected. The low level API, which is experimental, changed to
make this possible:

- A custom `EventRecorder` implements
`record(HasMetadata regarding, EventRecord event, ControllerConfiguration<?> controller)`.
`record(EventRecord, Context<?>)` and `forContext(Context<?>)` are gone, which also means that an
override of either in a subclass of `DefaultEventRecorder` no longer compiles: move its logic to
the new `record`. Callers use `context.eventRecorder()` instead, or
`forResource(context.getPrimaryResource(), context.getControllerConfiguration())` when only the
recorder is at hand. The bound views `forResource(...)` and `forController(...)` are default
methods that delegate to `record`.
- A custom `EventSink` implements `emit(Event, ControllerConfiguration<?>)` instead of
`emit(Event, Context<?>)`.
- A subclass of `DefaultEventRecorder` overrides
`toEvent(HasMetadata regarding, EventRecord record, ControllerConfiguration<?> controller)`
instead of `toEvent(Context<?>, EventRecord)`.
- `Controller#eventRecorder()` returns the `ControllerEventRecorder` bound to the controller
instead of the `EventRecorder` configured for the operator.
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,31 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.NamespaceChangeable;
import io.javaoperatorsdk.operator.api.event.ControllerEventRecorder;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;
import io.javaoperatorsdk.operator.health.ControllerHealthInfo;

import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;

public interface RegisteredController<P extends HasMetadata> extends NamespaceChangeable {

ControllerConfiguration<P> getConfiguration();

ControllerHealthInfo getControllerHealthInfo();

/**
* Returns an event recorder bound to this controller, to record Kubernetes events about any
* object outside of a reconciliation, for example from a background task. Within a
* reconciliation, use {@link io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder()}
* or {@link io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder(HasMetadata)}
* instead.
*
* @return an event recorder bound to this controller
* @throws UnsupportedOperationException if the implementation does not provide an event recorder
*/
@Experimental(API_MIGHT_CHANGE)
default ControllerEventRecorder eventRecorder() {
throw new UnsupportedOperationException(
"This implementation of RegisteredController does not provide a ControllerEventRecorder");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,9 @@ default String clusterScopedEventNamespace() {
*
* <p>When empty, which is the default, every controller gets a {@link DefaultEventRecorder} of
* its own. A recorder configured here is shared by all controllers of the operator instead, which
* is why the reconciliation an event is recorded from is passed to it per call rather than
* configured on it: implementations have to be stateless and thread safe.
* is why the object an event is about and the configuration of the controller recording it are
* passed to it per call rather than configured on it: implementations have to be stateless and
* thread safe.
*
* @return the event recorder to use for the whole operator, or an empty optional to let each
* controller use its own default one
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Java Operator SDK Authors
*
* 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 io.javaoperatorsdk.operator.api.event;

import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;

/**
* A {@link ControllerEventRecorder} that records through an {@link EventRecorder} on behalf of a
* fixed controller.
*/
record BoundControllerEventRecorder(EventRecorder delegate, ControllerConfiguration<?> controller)
implements ControllerEventRecorder {

BoundControllerEventRecorder {
Objects.requireNonNull(delegate, "the event recorder to record through must not be null");
Objects.requireNonNull(
controller, "the configuration of the controller recording events must not be null");
}

@Override
public void record(HasMetadata regarding, EventRecord event) {
delegate.record(regarding, event, controller);
}

@Override
public ResourceEventRecorder forResource(HasMetadata regarding) {
return delegate.forResource(regarding, controller);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Java Operator SDK Authors
*
* 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 io.javaoperatorsdk.operator.api.event;

import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;

/**
* A {@link ResourceEventRecorder} that records through an {@link EventRecorder} about a fixed
* object on behalf of a fixed controller.
*/
record BoundResourceEventRecorder(
EventRecorder delegate, HasMetadata regarding, ControllerConfiguration<?> controller)
implements ResourceEventRecorder {

BoundResourceEventRecorder {
Objects.requireNonNull(delegate, "the event recorder to record through must not be null");
Objects.requireNonNull(regarding, "the object events are recorded about must not be null");
Objects.requireNonNull(
controller, "the configuration of the controller recording events must not be null");
}

@Override
public void normal(String reason, String message) {
record(EventRecord.normal(reason, message));
}

@Override
public void warn(String reason, String message) {
record(EventRecord.warning(reason, message));
}

@Override
public void record(EventRecord event) {
delegate.record(regarding, event, controller);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Java Operator SDK Authors
*
* 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 io.javaoperatorsdk.operator.api.event;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Experimental;

import static io.javaoperatorsdk.operator.api.reconciler.Experimental.API_MIGHT_CHANGE;

/**
* An {@link EventRecorder} bound to a controller: events recorded through it are attributed to that
* controller and assembled from its configuration, and can be about any object. This is the form to
* use outside of a reconciliation, for example from a background task, see {@link
* io.javaoperatorsdk.operator.RegisteredController#eventRecorder()}. Within a reconciliation,
* prefer {@link io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder()} for events
* about the primary resource and {@link
* io.javaoperatorsdk.operator.api.reconciler.Context#eventRecorder(HasMetadata)} for events about
* another object.
*/
@Experimental(API_MIGHT_CHANGE)
public interface ControllerEventRecorder {

/**
* Records an event about the given object, attributed to the bound controller.
*
* @param regarding the object the event is about, referenced as its involved object
* @param event the event to record
*/
void record(HasMetadata regarding, EventRecord event);

/**
* Returns a view of this recorder bound to the given object as well, so that the object doesn't
* have to be passed for every event.
*
* @param regarding the object subsequent events will be about
* @return a recorder bound to the controller and to {@code regarding}
*/
ResourceEventRecorder forResource(HasMetadata regarding);
}
Loading
Loading