-
Notifications
You must be signed in to change notification settings - Fork 173
OCPEDGE-2094: Add console notification for pacemaker podman-etcd failures #1654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lucaconsalvi
wants to merge
8
commits into
openshift:main
Choose a base branch
from
lucaconsalvi:ocpedge-2094-console-notification
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
caead3c
OCPEDGE-2094: Add console notification for pacemaker podman-etcd fail…
lucaconsalvi 70b2491
Use periods instead of semicolons in console notification banner
lucaconsalvi 6b659b1
Fix test to actually set unhealthy etcd on offline node
lucaconsalvi d1c3611
OCPEDGE-2118: Record Kubernetes events for etcd transition lifecycle
lucaconsalvi 2c99f54
Lowercase event name to comply with RFC 1123 subdomain rules
lucaconsalvi 559a4a9
Fix gofmt alignment in const block
lucaconsalvi 67f426c
Split console notifications per problem category with correct doc links
lucaconsalvi f6b061e
Skip console notifications for uninitialized PacemakerCluster CR
lucaconsalvi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,210 @@ | ||
| package pacemaker | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "strings" | ||
| "time" | ||
|
|
||
| consolev1 "github.com/openshift/api/console/v1" | ||
| pacmkrv1 "github.com/openshift/api/etcd/v1" | ||
| "github.com/openshift/library-go/pkg/controller/factory" | ||
| "github.com/openshift/library-go/pkg/operator/events" | ||
| apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
| "k8s.io/apimachinery/pkg/runtime" | ||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||
| "k8s.io/client-go/dynamic" | ||
| "k8s.io/client-go/tools/cache" | ||
| "k8s.io/klog/v2" | ||
| ) | ||
|
|
||
| const ( | ||
| consoleNotificationName = "pacemaker-etcd-failure" | ||
|
|
||
| // PatternFly danger palette for the banner. | ||
| notificationTextColor = "#fff" | ||
| notificationBackgroundColor = "#c9190b" | ||
|
|
||
| troubleshootingURL = "https://docs.openshift.com/container-platform/latest/edge_computing/two-node-fencing/tnf-troubleshooting.html" | ||
| troubleshootingText = "Troubleshooting guide" | ||
| ) | ||
|
|
||
| var consoleNotificationGVR = schema.GroupVersionResource{ | ||
| Group: "console.openshift.io", | ||
| Version: "v1", | ||
| Resource: "consolenotifications", | ||
| } | ||
|
|
||
| // consoleNotificationController manages a ConsoleNotification banner that surfaces | ||
| // pacemaker health problems directly in the OpenShift web console. It watches the | ||
| // PacemakerCluster CR (via a shared informer) and creates or removes the banner | ||
| // based on the health of etcd resources and fencing agents. | ||
| // | ||
| // Uses the dynamic client because the console client-go package is not vendored. | ||
| type consoleNotificationController struct { | ||
| dynamicClient dynamic.Interface | ||
| pacemakerInformer cache.SharedIndexInformer | ||
| } | ||
|
|
||
| // NewConsoleNotificationController creates a controller that manages a ConsoleNotification | ||
| // banner for pacemaker podman-etcd failures. The controller shares the PacemakerCluster | ||
| // informer with the healthcheck and metrics controllers. | ||
| func NewConsoleNotificationController( | ||
| pacemakerInformer cache.SharedIndexInformer, | ||
| dynamicClient dynamic.Interface, | ||
| eventRecorder events.Recorder, | ||
| ) factory.Controller { | ||
| c := &consoleNotificationController{ | ||
| dynamicClient: dynamicClient, | ||
| pacemakerInformer: pacemakerInformer, | ||
| } | ||
|
|
||
| return factory.New(). | ||
| ResyncEvery(HealthCheckResyncInterval). | ||
| WithInformers(pacemakerInformer). | ||
| WithSync(c.sync). | ||
| ToController("ConsoleNotificationController", eventRecorder.WithComponentSuffix("console-notification")) | ||
| } | ||
|
|
||
| func (c *consoleNotificationController) sync(ctx context.Context, _ factory.SyncContext) error { | ||
| klog.V(4).Infof("syncing console notification for pacemaker health") | ||
|
|
||
| item, exists, err := c.pacemakerInformer.GetStore().GetByKey(PacemakerClusterResourceName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if !exists { | ||
| return c.deleteNotification(ctx) | ||
| } | ||
|
|
||
| cr, ok := item.(*pacmkrv1.PacemakerCluster) | ||
| if !ok { | ||
| return fmt.Errorf("unexpected object type in informer store: %T", item) | ||
| } | ||
|
|
||
| problems := evaluateHealth(cr) | ||
| if len(problems) == 0 { | ||
| return c.deleteNotification(ctx) | ||
| } | ||
|
|
||
| return c.ensureNotification(ctx, problems) | ||
| } | ||
|
|
||
| // evaluateHealth inspects the PacemakerCluster CR for conditions that warrant a | ||
| // console notification. Returns a list of human-readable problem descriptions, | ||
| // or nil when the cluster is healthy. | ||
| func evaluateHealth(cr *pacmkrv1.PacemakerCluster) []string { | ||
| var problems []string | ||
|
|
||
| if !cr.Status.LastUpdated.IsZero() && time.Since(cr.Status.LastUpdated.Time) > StatusStalenessThreshold { | ||
| problems = append(problems, "Pacemaker status has not been updated recently — health monitoring may be offline") | ||
| } | ||
|
|
||
| if getConditionStatus(cr.Status.Conditions, pacmkrv1.ClusterInServiceConditionType) == metav1.ConditionFalse { | ||
| problems = append(problems, "Pacemaker cluster is in maintenance mode") | ||
| } | ||
|
|
||
| if cr.Status.Nodes == nil { | ||
| return problems | ||
| } | ||
|
|
||
| for i := range *cr.Status.Nodes { | ||
| node := &(*cr.Status.Nodes)[i] | ||
| name := node.NodeName | ||
|
|
||
| if getConditionStatus(node.Conditions, pacmkrv1.NodeOnlineConditionType) == metav1.ConditionFalse { | ||
| problems = append(problems, fmt.Sprintf("Node %s is offline", name)) | ||
| continue | ||
| } | ||
|
|
||
| for j := range node.Resources { | ||
| res := &node.Resources[j] | ||
| if res.Name != pacmkrv1.PacemakerClusterResourceNameEtcd { | ||
| continue | ||
| } | ||
| if getConditionStatus(res.Conditions, pacmkrv1.ResourceHealthyConditionType) == metav1.ConditionFalse { | ||
| problems = append(problems, fmt.Sprintf("Etcd resource is unhealthy on node %s", name)) | ||
| } | ||
| } | ||
|
|
||
| if getConditionStatus(node.Conditions, pacmkrv1.NodeFencingAvailableConditionType) == metav1.ConditionFalse { | ||
| problems = append(problems, fmt.Sprintf("Fencing is unavailable on node %s — automatic recovery is not possible", name)) | ||
| } | ||
| } | ||
|
|
||
| return problems | ||
| } | ||
|
|
||
| func (c *consoleNotificationController) ensureNotification(ctx context.Context, problems []string) error { | ||
| text := strings.Join(problems, "; ") + ". Check pacemaker status for details." | ||
|
|
||
| notification := &consolev1.ConsoleNotification{ | ||
| TypeMeta: metav1.TypeMeta{ | ||
| APIVersion: "console.openshift.io/v1", | ||
| Kind: "ConsoleNotification", | ||
| }, | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: consoleNotificationName, | ||
| Labels: map[string]string{ | ||
| "app": "cluster-etcd-operator", | ||
| "app.kubernetes.io/part-of": "cluster-etcd-operator", | ||
| "app.kubernetes.io/managed-by": "cluster-etcd-operator", | ||
| }, | ||
| }, | ||
| Spec: consolev1.ConsoleNotificationSpec{ | ||
| Text: text, | ||
| Location: consolev1.BannerTop, | ||
| Color: notificationTextColor, | ||
| BackgroundColor: notificationBackgroundColor, | ||
| Link: &consolev1.Link{ | ||
| Href: troubleshootingURL, | ||
| Text: troubleshootingText, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(notification) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to convert ConsoleNotification to unstructured: %w", err) | ||
| } | ||
| u := &unstructured.Unstructured{Object: obj} | ||
|
|
||
| existing, err := c.dynamicClient.Resource(consoleNotificationGVR).Get(ctx, consoleNotificationName, metav1.GetOptions{}) | ||
| if apierrors.IsNotFound(err) { | ||
| if _, err := c.dynamicClient.Resource(consoleNotificationGVR).Create(ctx, u, metav1.CreateOptions{}); err != nil { | ||
| return fmt.Errorf("failed to create ConsoleNotification: %w", err) | ||
| } | ||
| klog.Infof("Created console notification: %s", text) | ||
|
lucaconsalvi marked this conversation as resolved.
Outdated
|
||
| return nil | ||
| } | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get ConsoleNotification: %w", err) | ||
| } | ||
|
|
||
| existingText, _, _ := unstructured.NestedString(existing.Object, "spec", "text") | ||
| if existingText == text { | ||
| klog.V(4).Infof("Console notification already up to date") | ||
| return nil | ||
| } | ||
|
|
||
| u.SetResourceVersion(existing.GetResourceVersion()) | ||
| if _, err := c.dynamicClient.Resource(consoleNotificationGVR).Update(ctx, u, metav1.UpdateOptions{}); err != nil { | ||
| return fmt.Errorf("failed to update ConsoleNotification: %w", err) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| klog.Infof("Updated console notification: %s", text) | ||
| return nil | ||
| } | ||
|
|
||
| func (c *consoleNotificationController) deleteNotification(ctx context.Context) error { | ||
| err := c.dynamicClient.Resource(consoleNotificationGVR).Delete(ctx, consoleNotificationName, metav1.DeleteOptions{}) | ||
| if err != nil && !apierrors.IsNotFound(err) { | ||
| return fmt.Errorf("failed to delete ConsoleNotification: %w", err) | ||
| } | ||
| if err == nil { | ||
| klog.Infof("Deleted console notification: pacemaker health restored") | ||
| } | ||
| return nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.