clusterhealth

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 15, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

clusterhealth

Library for running health and diagnostics checks against a Kubernetes cluster. It returns structured data only; callers decide logging and formatting.

This is a standalone Go module (github.com/opendatahub-io/opendatahub-operator/v2/pkg/clusterhealth) that can be imported without pulling in the full operator dependency tree. Its only runtime dependencies are controller-runtime, client-go, and k8s.io/apimachinery.

Quick start

Build a Config with your controller-runtime client and namespace/CR names, then call Run:

import (
	"context"
	"github.com/opendatahub-io/opendatahub-operator/v2/pkg/clusterhealth"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	ctrl "sigs.k8s.io/controller-runtime/pkg/client/config"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)

// Get a client (e.g. from controller-runtime)
kubeConfig, _ := ctrl.GetConfig()
c, _ := client.New(kubeConfig, client.Options{Scheme: clientgoscheme.Scheme})

cfg := clusterhealth.Config{
	Client:   c,
	Operator: clusterhealth.OperatorConfig{Namespace: "op-ns", Name: "op-deploy"},
	Namespaces: clusterhealth.NamespaceConfig{
		Apps:       "my-apps",
		Monitoring: "my-monitoring",
		Extra:      []string{"kube-system"},
	},
	DSCI: types.NamespacedName{}, // empty = discover DSCI on cluster
	DSC:  types.NamespacedName{}, // empty = discover DSC on cluster
}

report, err := clusterhealth.Run(context.Background(), cfg)
if err != nil {
	// Only when client is nil or config invalid
	return err
}

if !report.Healthy() {
	// One or more sections have Error set
	for _, name := range report.SectionsRun {
		// Inspect report.Nodes.Error, report.Deployments.Error, etc.
	}
}

// Human-readable table (optional long format with conditions/details)
fmt.Print(report.PrettyPrint(false))  // summary only
fmt.Print(report.PrettyPrint(true))   // summary + details (-l style)

// Or use structured data
for _, node := range report.Nodes.Data.Nodes {
	if node.UnhealthyReason != "" {
		// handle unhealthy node
	}
}
for _, cond := range report.DSC.Data.Conditions {
	// Type, Status, Message
}

Config

  • Client (required): controller-runtime client.Client.
  • Operator (required when running the operator section): namespace and deployment name of the main operator. If either is empty and the operator section runs, it returns error "operator config missing namespace or name".
  • Namespaces (optional): Apps, Monitoring, and Extra namespaces to scan for deployments, pods, events, quotas. None are required; empty or zero values are valid. If List() returns no namespaces, those sections return empty data with no error. The Nodes section does not use namespaces.
  • DSCI / DSC: types.NamespacedName. Leave empty to discover the singleton DSCI/DSC on the cluster; set to check a specific CR.
  • OnlySections: run only these sections (see Section constants). Overrides Layers when non-empty.
  • Layers: run sections from these layers (see Layer constants). Ignored if OnlySections is set.
Running a subset of sections
// Only nodes and quotas (infrastructure)
cfg.Layers = []string{clusterhealth.LayerInfrastructure}

// Only workload sections (deployments, pods, events, operator, DSCI, DSC)
cfg.Layers = []string{clusterhealth.LayerWorkload}

// Only operator and CRs (operator deployment, DSCI, DSC)
cfg.Layers = []string{clusterhealth.LayerOperator}

// Specific sections by name
cfg.OnlySections = []string{
	clusterhealth.SectionNodes,
	clusterhealth.SectionDSCI,
	clusterhealth.SectionDSC,
}

// Section constants: SectionNodes, SectionDeployments, SectionPods, SectionEvents,
// SectionQuotas, SectionOperator, SectionDSCI, SectionDSC.
// Layer constants: LayerInfrastructure (nodes + quotas), LayerWorkload (rest), LayerOperator (operator, DSCI, DSC).

Report

  • CollectedAt: time the run started.

  • SectionsRun: section names that were executed (useful when using OnlySections or Layers). PrettyPrint shows only these.

  • Per-section: Nodes, Deployments, Pods, Events, Quotas, Operator, DSCI, DSC, each a SectionResult[T] with:

    • Error: non-empty if the check failed or found an unhealthy state.
    • Data: typed section data (e.g. NodesSection, DeploymentsSection, CRConditionsSection).

    For DSCI and DSC (CR condition sections), Error is set when the CR is not found, when status.conditions is malformed (e.g. not a slice), or when conditions indicate unhealthy.

Healthy() returns true only when every section has no error. Partial failures (e.g. one namespace list fails) still populate other sections and set the failed section’s Error.

PrettyPrint

report.PrettyPrint(long bool) returns a human-readable table. If long is true, it appends a "Details" block listing conditions and per-item data (e.g. each DSCI/DSC condition with type, status, message; each node with status; deployments/pods/events/quotas/operator details).

Operator section and dependent operators

The operator section checks the main operator deployment and all known dependent operators (see the main repo README "External Operators (Prerequisites)" table). For each dependent we look for deployments in its known namespace; if any exist we treat it as installed. Every known dependent is listed in report.Operator.Data.DependentOperators with Installed true or false (and when installed: name, deployment, pods, error). Not-installed dependents are recorded but do not cause a section error.

Adding a CR condition check

To add health checks for a new Custom Resource (CR) that has status.conditions (same shape as DSCI/DSC), wire it through the following. Use DSCI/DSC as the reference implementation.

  1. GVK (pkg/clusterhealth/types.go): Add a package-level schema.GroupVersionKind var for the CR (e.g. MyCRGVK). The Kind string is used to look up an optional unhealthy checker.

  2. Section constant and layers (pkg/clusterhealth/sections.go):

    • Add a section constant, e.g. SectionMyCR = "mycr".
    • Add the section to layerSections for any layers that should include it (e.g. LayerWorkload, LayerOperator).
    • Add it to the default list in sectionsToRun() (the slice passed to sliceToSet when no OnlySections/Layers are set).
  3. Config (pkg/clusterhealth/config.go): Add a field for the CR's namespaced name, e.g. MyCR types.NamespacedName. Empty name means "discover singleton via List".

  4. Report and Healthy() (pkg/clusterhealth/types.go): Add a field to Report, e.g. MyCR SectionResult[CRConditionsSection], and include r.MyCR.Error == "" in Healthy().

  5. Run (pkg/clusterhealth/run.go): Append the new section name to sectionOrder. Add a conditional: if run[SectionMyCR] { report.MyCR = runCRConditionsSection(ctx, cfg.Client, MyCRGVK, cfg.MyCR) }.

  6. Format (pkg/clusterhealth/format.go): Add an entry to sectionDisplayOrder. In sectionStatusForKey and sectionSummaryForKey, add a case for the new section (status from r.MyCR.Error, summary e.g. name + condition count). In longDetailsForSection, add a case that calls r.longDetailsCRConditions(r.MyCR.Data.Name, r.MyCR.Data.Conditions).

  7. Optional – custom unhealthy logic (pkg/clusterhealth/cr_helpers.go): By default, any condition with status != True is reported as unhealthy. To override (e.g. ignore certain condition types or respect managementState: Removed), add an entry to kindUnhealthyCheckers keyed by the CR’s Kind and implement an UnhealthyChecker: func(obj map[string]interface{}, conditions []ConditionSummary) []string returning messages for conditions that count as unhealthy.

  8. CLI (cmd/health-check/main.go): In loadConfig, set the new Config field (e.g. MyCR: types.NamespacedName{}). The -sections and -layer flags already accept section names by string, so the new section is selectable as soon as it appears in sections.go.

The CR must expose status.conditions as a slice of objects with type, status, and optional message. If status.conditions is missing, the section gets no error and empty conditions; if it is present but malformed (e.g. not a slice), the section’s Error is set.

CLI

The cmd/health-check binary uses this library. It is its own Go module (cmd/health-check/go.mod), so run it from within that directory. Set the same env vars as e2e (E2E_TEST_OPERATOR_NAMESPACE, E2E_TEST_APPLICATIONS_NAMESPACE, E2E_TEST_WORKBENCHES_NAMESPACE, E2E_TEST_DSC_MONITORING_NAMESPACE), then:

cd cmd/health-check
go run .                              # all sections, summary
go run . -l                           # all sections, long format (conditions/details)
go run . -json                        # full report as JSON
go run . -layer=infrastructure
go run . -layer=operator
go run . -sections=nodes,dsci,dsc -l

Makefile targets (from repo root, see cmd/health-check/Makefile):

  • make cluster-health — all sections
  • make cluster-health-l — all sections, long format
  • make cluster-health-json — JSON output
  • make cluster-health-infrastructure, make cluster-health-workload, make cluster-health-operator-layer — by layer (infrastructure = nodes + quotas; workload = deployments, pods, events, operator, DSCI, DSC; operator = operator, DSCI, DSC)
  • make cluster-health-infrastructure-l, make cluster-health-workload-l, make cluster-health-operator-layer-l — by layer, long format

To run a single section (e.g. nodes or dsc), use the CLI: go run . -sections=nodes or -sections=dsc -l for long format.

Documentation

Index

Constants

View Source
const (
	SectionNodes       = "nodes"
	SectionDeployments = "deployments"
	SectionPods        = "pods"
	SectionEvents      = "events"
	SectionQuotas      = "quotas"
	SectionOperator    = "operator"
	SectionDSCI        = "dsci"
	SectionDSC         = "dsc"
)

Section name constants for use in Config.OnlySections.

View Source
const (
	// LayerInfrastructure is cluster-level health: nodes and resource quotas.
	LayerInfrastructure = "infrastructure"
	// LayerWorkload is on-cluster components: deployments, pods, events, operator, DSCI, DSC.
	LayerWorkload = "workload"
	// LayerOperator is operator and CRs: operator deployment, DSCI, DSC.
	LayerOperator = "operator"
)

Layer name constants. Layers group sections for common use cases. Use Config.Layers to run only checks in one or more layers.

View Source
const DefaultEventsWindow = 5 * time.Minute

Variables

View Source
var (
	DSCInitializationGVK = schema.GroupVersionKind{
		Group:   "dscinitialization.opendatahub.io",
		Version: "v2",
		Kind:    "DSCInitialization",
	}
	DataScienceClusterGVK = schema.GroupVersionKind{
		Group:   "datasciencecluster.opendatahub.io",
		Version: "v2",
		Kind:    "DataScienceCluster",
	}
)

GVKs for the ODH custom resources checked by the DSCI and DSC sections.

View Source
var KnownComponents = map[string]string{
	"dashboard": "Dashboard", "workbenches": "Workbenches",
	"kserve": "Kserve", "ray": "Ray", "kueue": "Kueue",
	"modelregistry": "ModelRegistry", "trustyai": "TrustyAI",
	"datasciencepipelines": "DataSciencePipelines",
	"trainingoperator":     "TrainingOperator", "feastoperator": "FeastOperator",
	"llamastackoperator": "LlamaStackOperator", "modelmeshserving": "ModelMeshServing",
	"mlflowoperator": "MLflowOperator", "sparkoperator": "SparkOperator",
	"modelcontroller": "ModelController", "modelsasservice": "ModelsAsService",
	"trainer": "Trainer",
}

KnownComponents maps component name (user-facing) to CR Kind.

Functions

This section is empty.

Types

type CRConditionsSection

type CRConditionsSection struct {
	Name       string                     `json:"name"`
	Conditions []ConditionSummary         `json:"conditions"`
	Data       *unstructured.Unstructured `json:"data,omitempty"` // raw CR for tests or fields we don't parse
}

type ComponentStatusResult

type ComponentStatusResult struct {
	Component   string             `json:"component"`
	CRFound     bool               `json:"crFound"`
	Conditions  []ConditionSummary `json:"conditions"`
	Deployments []DeploymentInfo   `json:"deployments"`
	Pods        []PodInfo          `json:"pods"`
	Errors      []string           `json:"errors,omitempty"`
}

ComponentStatusResult holds the health details for a single ODH component.

func GetComponentStatus

func GetComponentStatus(ctx context.Context, c client.Client, name, appsNS string) (*ComponentStatusResult, error)

GetComponentStatus fetches the CR conditions, deployments, and pods for a named component.

type ConditionSummary

type ConditionSummary struct {
	Type    string `json:"type"`
	Status  string `json:"status"`
	Message string `json:"message"`
}

type Config

type Config struct {
	Client     client.Client
	Operator   OperatorConfig
	Namespaces NamespaceConfig
	DSCI       types.NamespacedName
	DSC        types.NamespacedName
	// OnlySections limits which sections to run. Empty or nil = run all.
	// Use section constants (SectionNodes, SectionDeployments, etc.) or layer
	// constants (LayerInfrastructure, LayerWorkload, LayerOperator) to run a subset.
	OnlySections []string
	// Layers limits which sections to run. Empty or nil = run all.
	Layers []string
}

Config holds the client and namespace needed to run health checks. All inputs are passed explicitly; no package-level globals.

type ContainerInfo

type ContainerInfo struct {
	Name           string `json:"name"`
	Ready          bool   `json:"ready"`
	IsInit         bool   `json:"isInit,omitempty"`
	RestartCount   int32  `json:"restartCount"`
	Waiting        string `json:"waiting"`                  // reason/message if waiting
	Terminated     string `json:"terminated"`               // reason/exit if terminated
	RequestsCPU    *int64 `json:"requestsCPU,omitempty"`    // in millicores
	RequestsMemory *int64 `json:"requestsMemory,omitempty"` // in bytes
	LimitsCPU      *int64 `json:"limitsCPU,omitempty"`      // in millicores
	LimitsMemory   *int64 `json:"limitsMemory,omitempty"`   // in bytes
}

type DependentOperatorResult

type DependentOperatorResult struct {
	Name       string          `json:"name"`
	Installed  bool            `json:"installed"` // true if a deployment was found in the dependent's namespace
	Deployment *DeploymentInfo `json:"deployment,omitempty"`
	Pods       []PodInfo       `json:"pods,omitempty"`
	Error      string          `json:"error,omitempty"`
}

type DeploymentInfo

type DeploymentInfo struct {
	Namespace  string             `json:"namespace"`
	Name       string             `json:"name"`
	Ready      int32              `json:"ready"`
	Replicas   int32              `json:"replicas"`
	Conditions []ConditionSummary `json:"conditions"`
}

type DeploymentsSection

type DeploymentsSection struct {
	ByNamespace map[string][]DeploymentInfo `json:"byNamespace"`
	Data        []appsv1.Deployment         `json:"data,omitempty"` // raw Deployment list for tests or fields we don't parse
}

type EventInfo

type EventInfo struct {
	Namespace string    `json:"namespace"`
	Kind      string    `json:"kind"`
	Name      string    `json:"name"`
	Type      string    `json:"type"`
	Reason    string    `json:"reason"`
	Message   string    `json:"message"`
	LastTime  time.Time `json:"lastTime"`
}

func RunRecentEvents

func RunRecentEvents(ctx context.Context, cfg RecentEventsConfig) ([]EventInfo, error)

RunRecentEvents returns recent events across namespaces, filtered by type and sorted most-recent-first.

type EventsSection

type EventsSection struct {
	Events []EventInfo    `json:"events"`
	Data   []corev1.Event `json:"data,omitempty"` // raw Event list for tests or fields we don't parse
}

type NamespaceConfig

type NamespaceConfig struct {
	Apps       string
	Monitoring string
	Extra      []string
}

NamespaceConfig configures which namespaces to scan for deployments, pods, events, quotas. None are required; empty or zero values are valid. If List() returns no namespaces, those sections return empty data with no error. The Nodes section does not use namespaces.

func (NamespaceConfig) List

func (n NamespaceConfig) List() []string

List returns the deduplicated list of namespaces to scan, skipping empty ones. When Apps and Monitoring point to the same namespace (common in ODH), the namespace appears only once to avoid duplicate API calls and metric lines.

type NodeInfo

type NodeInfo struct {
	Name            string             `json:"name"`
	Role            string             `json:"role,omitempty"` // e.g. "master" or "worker"
	Conditions      []ConditionSummary `json:"conditions"`
	Allocatable     string             `json:"allocatable"` // human-readable (e.g. "4 CPU, 8Gi memory")
	Capacity        string             `json:"capacity"`
	UnhealthyReason string             `json:"unhealthyReason,omitempty"` // non-empty if node is in a bad state

	// Actual resource usage from the Metrics Server (nil when unavailable).
	UsageCPUMillicores *int64 `json:"usageCPUMillicores,omitempty"`
	UsageMemoryBytes   *int64 `json:"usageMemoryBytes,omitempty"`
}

type NodesSection

type NodesSection struct {
	Nodes []NodeInfo    `json:"nodes"`
	Data  []corev1.Node `json:"data,omitempty"` // raw Node list (e.g. .Status for NodeStatus) for tests or fields we don't parse
}

type OperatorConfig

type OperatorConfig struct {
	Namespace string
	Name      string
}

OperatorConfig configures which operator deployment and namespace to check. The deployment name is supplied by the caller (e.g. from platform: ODH vs RHODS operator).

type OperatorSection

type OperatorSection struct {
	Deployment         *DeploymentInfo           `json:"deployment"`
	Pods               []PodInfo                 `json:"pods"`
	DependentOperators []DependentOperatorResult `json:"dependentOperators,omitempty"`
	Data               *OperatorSectionData      `json:"data,omitempty"` // raw Deployment and Pods for tests or fields we don't parse
}

type OperatorSectionData

type OperatorSectionData struct {
	Deployment *appsv1.Deployment `json:"deployment,omitempty"`
	Pods       []corev1.Pod       `json:"pods,omitempty"`
}

type PodInfo

type PodInfo struct {
	Namespace  string          `json:"namespace"`
	Name       string          `json:"name"`
	Phase      string          `json:"phase"`
	NodeName   string          `json:"nodeName,omitempty"`
	CreatedAt  time.Time       `json:"createdAt"`
	Containers []ContainerInfo `json:"containers"`
}

type PodsSection

type PodsSection struct {
	ByNamespace map[string][]PodInfo `json:"byNamespace"`
	Data        []corev1.Pod         `json:"data,omitempty"` // raw Pod list for tests or fields we don't parse
}

type QuotasSection

type QuotasSection struct {
	ByNamespace map[string][]ResourceQuotaInfo `json:"byNamespace"`
	Data        []corev1.ResourceQuota         `json:"data,omitempty"` // raw ResourceQuota list for tests or fields we don't parse
}

type RecentEventsConfig

type RecentEventsConfig struct {
	Client     client.Client
	Namespaces []string      // namespaces to scan (caller resolves these)
	Since      time.Duration // look-back window; zero = DefaultEventsWindow (5m)
	EventType  string        // "Warning", "Normal", or "" for all
}

RecentEventsConfig holds parameters for a standalone recent-events query.

type Report

type Report struct {
	CollectedAt time.Time `json:"collectedAt"`
	// SectionsRun is the list of section names that were executed (e.g. when using OnlySections or Layers).
	// Empty or nil means all sections were run. Used by PrettyPrint to show only those rows.
	SectionsRun []string `json:"sectionsRun,omitempty"`

	Nodes       SectionResult[NodesSection]        `json:"nodes"`
	Deployments SectionResult[DeploymentsSection]  `json:"deployments"`
	Pods        SectionResult[PodsSection]         `json:"pods"`
	Events      SectionResult[EventsSection]       `json:"events"`
	Quotas      SectionResult[QuotasSection]       `json:"quotas"`
	Operator    SectionResult[OperatorSection]     `json:"operator"`
	DSCI        SectionResult[CRConditionsSection] `json:"dsci"`
	DSC         SectionResult[CRConditionsSection] `json:"dsc"`
}

Report is the full result of Run(). All sections are independent; partial failures are recorded per section.

func Run

func Run(ctx context.Context, cfg Config) (*Report, error)

Run runs all health checks and returns a Report. Config.Client and namespace configuration must be set by the caller; no globals are used.

Run always returns a non-nil Report when err is nil. It returns an error only when the report cannot be built at all (e.g. nil client). Partial failures are recorded per section in the Report (SectionResult.Error and optional Data).

func (*Report) Healthy

func (r *Report) Healthy() bool

Healthy returns true if the report has no section errors (all checks succeeded or returned partial data without a fatal error). Used by CLI to decide exit code.

func (*Report) PrettyPrint

func (r *Report) PrettyPrint(long bool) string

PrettyPrint returns a human-readable table of section results. When Report.SectionsRun is set (e.g. from -sections or -layer), only those sections are shown; otherwise all eight are shown. If long is true, appends a details block listing conditions and other per-item data (like ls -l).

func (*Report) PrometheusExport

func (r *Report) PrometheusExport() []string

PrometheusExport converts the Report to Prometheus exposition format lines. Each line includes the CollectedAt timestamp in Unix seconds. The output can be appended to a file for bulk import into VictoriaMetrics via /api/v1/import/prometheus.

type ResourceQuotaInfo

type ResourceQuotaInfo struct {
	Namespace string            `json:"namespace"`
	Name      string            `json:"name"`
	Used      map[string]string `json:"used"`
	Hard      map[string]string `json:"hard"`
	Exceeded  []string          `json:"exceeded"`
}

type SectionResult

type SectionResult[T any] struct {
	Error string `json:"error"` // non-empty if this section failed or was skipped
	Data  T      `json:"data"`  // populated with whatever was collected (may be zero value)
}

SectionResult carries the result of one health-check section: optional error and typed data. Partial failures set Error and may still populate Data with what was collected.

type UnhealthyChecker

type UnhealthyChecker func(obj map[string]any, conditions []ConditionSummary) []string

UnhealthyChecker is the common signature for kind-specific condition checks. Receives the raw CR object and parsed conditions; returns messages for any conditions that count as unhealthy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL