From bd6242abfa86c4f5acaa1f677f6395858127d34d Mon Sep 17 00:00:00 2001 From: Fabricio Aguiar Date: Thu, 16 Jul 2026 18:20:12 +0100 Subject: [PATCH 1/2] CONSOLE-5118: Add OLS integration for cluster update workflows Implements OpenShift Lightspeed AI assistance integration into cluster settings page to provide contextual help during cluster update workflows. Features: - UpdateWorkflowOLSButton component with 4 workflow phase support - Precheck: Pre-update validation and readiness assessment - Failure: Error analysis and troubleshooting guidance - Status: Real-time update progress monitoring assistance - Success: Post-update verification and validation help The integration uses the official lightspeed-console plugin API to open the OLS chatbox with context-aware prompts and cluster data attachments. Each workflow phase provides tailored AI prompts and exports relevant cluster resources (ClusterVersion) as YAML for comprehensive analysis. Note: E2E test file (cluster-settings-page.ts) excluded from backport as Playwright E2E framework is not available in release-4.22. Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Fabricio Aguiar (cherry picked from commit 60f1c7167e4ebb3b6759fe209d114f17804681fb) --- .../src/components/cluster-updates/README.md | 423 ++++++++++ .../__tests__/cluster-state-detector.spec.ts | 377 +++++++++ .../__tests__/cluster-state-matrix.spec.ts | 744 +++++++++++++++++ .../__tests__/enhancements.spec.ts | 481 +++++++++++ .../__tests__/explain-button.spec.tsx | 275 +++++++ .../cluster-updates/__tests__/test-helpers.ts | 39 + .../__tests__/workflow-comprehensive.spec.ts | 555 +++++++++++++ .../__tests__/workflow-utils.spec.ts | 101 +++ .../cluster-updates/cluster-state-detector.ts | 278 +++++++ .../cluster-version-helpers.ts | 18 + .../components/cluster-updates/constants.ts | 49 ++ .../cluster-updates/explain-button.tsx | 197 +++++ .../components/cluster-updates/predicates.ts | 265 ++++++ .../prompts/precheck-no-updates.ts | 139 ++++ .../prompts/precheck-specific.ts | 334 ++++++++ .../cluster-updates/prompts/precheck.ts | 487 +++++++++++ .../cluster-updates/prompts/progress.ts | 220 +++++ .../prompts/shared/constants.ts | 30 + .../prompts/shared/language-utils.ts | 90 ++ .../cluster-updates/prompts/troubleshoot.ts | 266 ++++++ .../src/components/cluster-updates/types.ts | 78 ++ .../cluster-updates/workflow-configs.ts | 179 ++++ .../cluster-updates/workflow-utils.ts | 168 ++++ .../inventory-card/InventoryItem.tsx | 3 +- .../console-shared/src/utils/comparators.ts | 25 +- .../cluster-settings/cluster-settings.tsx | 776 +++++++++++++++++- .../modals/cluster-update-modal.tsx | 54 +- frontend/public/locales/en/public.json | 53 +- frontend/public/module/k8s/types.ts | 17 + 29 files changed, 6665 insertions(+), 56 deletions(-) create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/README.md create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-detector.spec.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-matrix.spec.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/enhancements.spec.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/explain-button.spec.tsx create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/test-helpers.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-comprehensive.spec.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-utils.spec.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/cluster-state-detector.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/cluster-version-helpers.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/constants.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/explain-button.tsx create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/predicates.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-no-updates.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-specific.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/progress.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/constants.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/language-utils.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/prompts/troubleshoot.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/types.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/workflow-configs.ts create mode 100644 frontend/packages/console-shared/src/components/cluster-updates/workflow-utils.ts diff --git a/frontend/packages/console-shared/src/components/cluster-updates/README.md b/frontend/packages/console-shared/src/components/cluster-updates/README.md new file mode 100644 index 00000000000..2830840ab1a --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/README.md @@ -0,0 +1,423 @@ +# OLS Update Workflows + +This directory contains the OLS (OpenShift Lightspeed) integration for cluster update workflows in the OpenShift Console. + +## Overview + +The OLS update workflows provide context-aware AI assistance for cluster updates by intelligently determining which prompt to show based on the cluster's current state. + +## Architecture + +### Two-Phase System + +1. **status** phase: Active monitoring and troubleshooting + - Shows "Update status" button + - Used when cluster has issues or upgrade in progress + - Automatically chooses between progress monitoring and troubleshooting + +2. **pre-check** phase: Pre-update readiness assessment + - Shows "Pre-check with AI" button + - Used when cluster is healthy + - Helps assess readiness before initiating updates + +## Cluster State Matrix + +The system handles 11 primary cluster states by examining ClusterVersion conditions, ClusterOperator health, and update availability. + +### Quick Reference + +| State | Conditions | Button | Prompt | +|-------|-----------|--------|--------| +| **Upgrade In Progress** | Progressing=True, no failures | Update status | Progress | +| **Upgrade Failing** | Failing=True, Progressing=True | Update status | Troubleshoot | +| **Upgrade Stalled** | Progressing=True, operator issues | Update status | Troubleshoot | +| **Cluster Failing** | Failing=True, Progressing=False | Update status | Troubleshoot | +| **Operator Issues** | Progressing=False, operator issues | Update status | Troubleshoot | +| **Service Issues** | RetrievedUpdates=False or ReleaseAccepted=False | Update status | Troubleshoot | +| **Ready for Update** | All healthy, updates available | Pre-check | Pre-check | +| **Conditional Updates** | All healthy, only conditionalUpdates | Pre-check | Pre-check | +| **No Updates** | All healthy, no updates | Pre-check | No updates | +| **Specific Version** | All healthy, version selected | Pre-check | Specific version | +| **Invalid** | Invalid=True | Update status | Troubleshoot | + + +## Prompt Functions + +Located in `prompts.ts`: + +### 1. `createProgressPrompt(currentVersion, desiredVersion, operatorCounts)` + +**When used**: Upgrade in progress, no failures detected + +**What it does**: +- Monitors upgrade progress (X of Y operators updated) +- Calculates completion percentage and ETA +- Tracks operator status (updated, updating, pending, failed) +- Checks MachineConfigPool progress +- Monitors resource usage and events +- Detects early warning signs + +**User Intent**: Monitor progress, estimate completion time + +### 2. `createTroubleshootPrompt(currentVersion, desiredVersion)` + +**When used**: Any failure detected (cluster-level or operator-level) + +**What it does**: +- Root cause analysis from ClusterVersion Failing condition +- Identifies failed ClusterOperators with specific errors +- Extracts error messages from pod logs +- Builds timeline of events leading to failure +- Checks update service connectivity +- Provides conservative remediation steps + +**User Intent**: Diagnose failure, find root cause, get remediation + +### 3. `createPreCheckPrompt(currentVersion)` + +**When used**: Cluster healthy, updates available + +**What it does**: +- Lists all available updates with channels and metadata +- Assesses cluster upgrade readiness +- Checks all ClusterOperator health +- Verifies MachineConfigPool status +- Identifies problematic user workload PodDisruptionBudgets +- Analyzes node health and resource pressure +- Reviews recent events and active alerts +- Checks Cincinnati service health +- Final recommendation: Ready / Address warnings / Blocked + +**User Intent**: Verify readiness, understand available updates + +### 4. `createPreCheckSpecificVersionPrompt(currentVersion, targetVersion)` + +**When used**: User has selected a specific target version + +**What it does**: +- Verifies target version is in availableUpdates +- Extracts version-specific metadata (channels, release URL) +- Checks for version-specific known issues +- Validates upgrade path is supported +- Assesses cluster readiness for that specific version + +**User Intent**: Verify specific version compatibility + +### 5. `createPreCheckNoUpdatesPrompt(currentVersion)` + +**When used**: No updates available (cluster fully updated) + +**What it does**: +- Confirms cluster is running current version +- Verifies update service connectivity (RetrievedUpdates=True) +- Assesses overall cluster health +- Reviews cluster history +- Checks for operational issues +- Confirms ready for future updates + +**User Intent**: Verify health, confirm up-to-date status + +## Decision Logic + +### Phase Determination + +The `determineWorkflowPhase()` function in `workflow-utils.ts` chooses between status and pre-check: + +```typescript +// Show status button if ANY of these are true: +- Failing=True (cluster failing) +- Invalid=True (invalid configuration) +- Progressing=True (upgrade in progress) +- RetrievedUpdates=False with message (update service issue) +- ReleaseAccepted=False with message (signature verification issue) +- Any operator has Available=False (operator unavailable) +- Any operator has Degraded=True (operator degraded) + +// Otherwise show pre-check button +``` + +### Prompt Selection within Status Phase + +The status workflow (`workflow-configs.ts`) chooses between progress and troubleshoot: + +```typescript +// Use troubleshoot prompt if ANY of these are true: +- Failing=True +- Invalid=True +- RetrievedUpdates=False with message +- ReleaseAccepted=False with message +- Any operator has Available=False OR Degraded=True + +// Otherwise use progress prompt (healthy upgrade in progress) +``` + +### Prompt Selection within Pre-Check Phase + +The pre-check workflow chooses the appropriate assessment: + +```typescript +if (no available updates) { + return createPreCheckNoUpdatesPrompt(); +} else if (specific version selected) { + return createPreCheckSpecificVersionPrompt(); +} else { + return createPreCheckPrompt(); +} +``` + +## Critical Principles + +### 1. Always Check the `status` Field + +Conditions have TWO fields: `type` and `status`. You must check both! + +```typescript +// ❌ WRONG: Just checking the type +const failing = conditions.find(c => c.type === 'Failing'); + +// ✅ CORRECT: Check both type AND status +const failing = conditions.find(c => c.type === 'Failing' && c.status === 'True'); +``` + +**Examples**: +- `{type: "Failing", status: "False"}` → Cluster is NOT failing (healthy) +- `{type: "Failing", status: "True"}` → Cluster IS failing (problem) +- `{type: "Available", status: "True"}` → Cluster IS available (healthy) +- `{type: "Available", status: "False"}` → Cluster is NOT available (problem) + +### 2. Failures Take Precedence + +Even if `Progressing=True`, we use troubleshoot prompt if failures detected. + +```typescript +// Upgrade is progressing BUT has failures → Troubleshoot, not Progress +if (Progressing=True AND (Failing=True OR operator issues)) { + return createTroubleshootPrompt(); +} +``` + +### 3. Operator Counts for Progress + +When showing progress, we provide accurate operator counts: + +```typescript +const operatorCounts = { + total: 28, // Total operators + updated: 18, // Operators at target version + updating: 7, // Operators progressing toward target + pending: 3, // Operators waiting to start + failed: 0, // Operators with issues +}; +``` + +## Testing + +### Running Tests + +```bash +cd frontend +yarn test ols-update-workflows +``` + +### Test Coverage + +- **cluster-state-matrix.spec.ts**: Complete state matrix (11 states + edge cases) +- **workflow-comprehensive.spec.ts**: Button logic and prompt content validation +- **workflow-utils.spec.ts**: Utility function tests +- **explain-button.spec.tsx**: React component tests + +### Adding New Tests + +When adding new cluster states or modifying behavior: + +1. Add test case to `cluster-state-matrix.spec.ts` with clear description +2. Validate button appearance +3. Validate prompt content +4. Document expected behavior in test description + +## Common Issues + +### "Pre-check button shows when cluster is failing" + +Check that you're examining the `status` field: +```typescript +// {type: "Failing", status: "False"} means NOT failing +const failing = condition.status === 'True'; // Not just checking if condition exists +``` + +### "Upgrade progress shows troubleshoot prompt" + +Check for operator issues: +```typescript +// Even if Progressing=True, operator issues trigger troubleshoot +const hasOperatorIssues = operators.some(op => + op.status.conditions.find(c => + (c.type === 'Available' && c.status === 'False') || + (c.type === 'Degraded' && c.status === 'True') + ) +); +``` + +### "No button appears" + +Verify cluster has conditions: +```typescript +// Missing conditions defaults to pre-check +const conditions = cv.status?.conditions || []; +``` + +## Conditional Updates + +Conditional updates are updates that have associated conditions/risks. The structure is: + +```typescript +type ConditionalUpdate = { + release: { version: string; image: string }; + conditions: K8sResourceCondition[]; +}; +``` + +The conditions typically have: +- `type: "Recommended"` +- `status: "False"` (indicating NOT recommended due to risks) +- `message`: Contains the risk description and documentation URL + +**Current Behavior**: When `availableUpdates` is empty but `conditionalUpdates` exists, the system uses the "no updates" prompt. This could be enhanced to parse the conditions and explain the risks to users. + +**Example**: +```json +{ + "release": { "version": "4.16.0", "image": "..." }, + "conditions": [{ + "type": "Recommended", + "status": "False", + "reason": "KnownIssue", + "message": "Clusters using OVN may experience network disruption. See https://access.redhat.com/solutions/7001234" + }] +} +``` + +## Cluster State Detection API + +The `cluster-state-detector.ts` module provides a comprehensive, high-level API for detecting and categorizing cluster states. + +### Core Functions + +#### `detectClusterState(cv, operators?): ClusterStateInfo` + +Detects the current cluster state based on conditions and returns detailed information: + +```typescript +const stateInfo = detectClusterState(clusterVersion, clusterOperators); + +// Returns: +{ + state: ClusterState.UPDATE_IN_PROGRESS, + description: "Cluster update is in progress", + conditions: { + failing: false, + progressing: true, + hasRecommendedUpdates: true, + // ... all condition flags + }, + recommendedWorkflow: "status" +} +``` + +#### `isClusterHealthy(cv, operators?): boolean` + +Determines if cluster is in a healthy state (no failures, no operator issues). + +#### `shouldShowPreCheck(cv, operators?): boolean` + +Determines if pre-check workflow button should be shown. + +#### `shouldShowStatus(cv, operators?): boolean` + +Determines if status workflow button should be shown. + +### Predicates API + +The `predicates.ts` module exports comprehensive predicate functions for checking specific cluster and operator conditions. These predicates are the building blocks for cluster state detection. + +#### ClusterVersion Predicates + +- `isClusterFailing(cv)` - Check if Failing=True +- `isClusterInvalid(cv)` - Check if Invalid=True +- `isClusterProgressing(cv)` - Check if Progressing=True +- `isClusterAvailable(cv)` - Check if Available=True +- `isClusterUpgradeable(cv)` - Check if Upgradeable=True or not present +- `hasUpdateRetrievalFailure(cv)` - Check if RetrievedUpdates=False +- `hasReleaseAcceptanceFailure(cv)` - Check if ReleaseAccepted=False +- `hasRecommendedUpdates(cv)` - Check if availableUpdates present +- `hasConditionalUpdates(cv)` - Check if conditionalUpdates present +- `hasAnyUpdates(cv)` - Check if any updates available +- `hasUnknownConditions(cv)` - Check for Unknown condition status +- `isClusterReadyToUpdate(cv, ops?)` - Comprehensive readiness check +- `hasBlockingConditions(cv, ops?)` - Check for any blocking conditions + +#### ClusterOperator Predicates + +- `isOperatorDegraded(op)` - Check if operator has Degraded=True +- `isOperatorUnavailable(op)` - Check if operator has Available=False +- `isOperatorUpgradeable(op)` - Check if operator has Upgradeable=True +- `hasOperatorIssues(op)` - Check if operator is degraded or unavailable +- `hasAnyOperatorIssues(ops)` - Check if any operator has issues +- `getOperatorsWithIssues(ops)` - Get array of operators with issues + +### Constants API + +The `constants.ts` module exports all condition type and status constants: + +```typescript +// ClusterVersion condition types +CLUSTER_VERSION_CONDITION_AVAILABLE +CLUSTER_VERSION_CONDITION_FAILING +CLUSTER_VERSION_CONDITION_PROGRESSING +CLUSTER_VERSION_CONDITION_RETRIEVED_UPDATES +CLUSTER_VERSION_CONDITION_RELEASE_ACCEPTED +CLUSTER_VERSION_CONDITION_INVALID +CLUSTER_VERSION_CONDITION_UPGRADEABLE + +// ClusterOperator condition types +CLUSTER_OPERATOR_CONDITION_AVAILABLE +CLUSTER_OPERATOR_CONDITION_DEGRADED +CLUSTER_OPERATOR_CONDITION_PROGRESSING +CLUSTER_OPERATOR_CONDITION_UPGRADEABLE + +// Condition status values +CONDITION_STATUS_TRUE +CONDITION_STATUS_FALSE +CONDITION_STATUS_UNKNOWN +``` + +### Intentionally Exported API + +Some exports may appear "unused" in linting tools but are intentionally part of the public API: + +- **Granular predicates** (`isOperatorDegraded`, `isOperatorUnavailable`, etc.) - Provide fine-grained checking for specific conditions +- **updateWorkflowConfigs** - Workflow configuration registry for introspection and testing +- **All constants** - Complete set of condition types and statuses for comprehensive state detection + +These exports enable external code to: +1. Implement custom cluster state logic +2. Build additional UI components based on cluster conditions +3. Create test fixtures with realistic cluster states +4. Introspect workflow configurations + +## Contributing + +When modifying this code: + +1. **Update tests first**: Add test case to `cluster-state-matrix.spec.ts` +2. **Check condition status**: Always check both `type` AND `status` fields +3. **Validate all states**: Run full test suite to ensure no regressions +4. **Update this README**: Document any new states or prompt variants +5. **Add inline comments**: Explain non-obvious decision logic +6. **Maintain public API**: Keep predicate exports stable for external consumers + +## References + +- [OpenShift ClusterVersion API](https://docs.openshift.com/container-platform/latest/rest_api/config_apis/clusterversion-config-openshift-io-v1.html) +- [ClusterOperator API](https://docs.openshift.com/container-platform/latest/rest_api/config_apis/clusteroperator-config-openshift-io-v1.html) +- [OLS Integration Design](https://issues.redhat.com/browse/CONSOLE-5118) diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-detector.spec.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-detector.spec.ts new file mode 100644 index 00000000000..9b99f4c2ce3 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-detector.spec.ts @@ -0,0 +1,377 @@ +/** + * Cluster State Detector Tests + * + * Comprehensive test coverage for all cluster states and scenarios + * described in the OLS integration requirements. + */ + +import type { ClusterVersionKind, ClusterOperator } from '@console/internal/module/k8s'; +import { + detectClusterState, + isClusterHealthy, + shouldShowPreCheck, + shouldShowStatus, + ClusterState, +} from '../cluster-state-detector'; + +describe('Cluster State Detector', () => { + // Helper to create a minimal ClusterVersion + const createCV = (overrides?: Partial): ClusterVersionKind => ({ + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { name: 'version' }, + spec: { + clusterID: 'test-cluster', + channel: 'stable-4.14', + }, + status: { + observedGeneration: 1, + versionHash: 'test-hash', + conditions: [], + history: [ + { + state: 'Completed', + version: '4.14.1', + image: 'quay.io/openshift-release-dev/ocp-release:4.14.1', + verified: false, + startedTime: '', + completionTime: '', + }, + ], + desired: { version: '4.14.1', image: '' }, + }, + ...overrides, + }); + + // Helper to create a ClusterOperator + const createOperator = ( + name: string, + available: boolean = true, + degraded: boolean = false, + ): ClusterOperator => ({ + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterOperator', + metadata: { name }, + spec: {}, + status: { + conditions: [ + { type: 'Available', status: available ? 'True' : 'False', lastTransitionTime: '' }, + { type: 'Degraded', status: degraded ? 'True' : 'False', lastTransitionTime: '' }, + ], + }, + }); + + describe('Scenario 1: Ready to update with recommended updates', () => { + it('should detect READY_WITH_UPDATES state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + ], + availableUpdates: [{ version: '4.14.2', image: '' }], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.READY_WITH_UPDATES); + expect(result.recommendedWorkflow).toBe('pre-check'); + expect(result.conditions.hasRecommendedUpdates).toBe(true); + expect(result.conditions.failing).toBe(false); + expect(result.conditions.progressing).toBe(false); + }); + + it('should show pre-check workflow', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + ], + availableUpdates: [{ version: '4.14.2', image: '' }], + }, + }); + + expect(shouldShowPreCheck(cv)).toBe(true); + expect(shouldShowStatus(cv)).toBe(false); + }); + }); + + describe('Scenario 2: Ready with conditional updates', () => { + it('should detect READY_WITH_CONDITIONAL_UPDATES state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + ], + conditionalUpdates: [ + { + release: { version: '4.14.2', image: '' }, + conditions: [ + { + type: 'Recommended', + status: 'False', + reason: 'KnownIssue', + message: 'Known issue with networking', + lastTransitionTime: '', + }, + ], + }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.READY_WITH_CONDITIONAL_UPDATES); + expect(result.recommendedWorkflow).toBe('pre-check'); + expect(result.conditions.hasConditionalUpdates).toBe(true); + }); + }); + + describe('Scenario 3: Update in progress', () => { + it('should detect UPDATE_IN_PROGRESS state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'True', lastTransitionTime: '' }, + ], + desired: { version: '4.14.2', image: '' }, + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.UPDATE_IN_PROGRESS); + expect(result.recommendedWorkflow).toBe('status'); + expect(result.conditions.progressing).toBe(true); + }); + + it('should show status workflow during update', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'True', lastTransitionTime: '' }, + ], + }, + }); + + expect(shouldShowStatus(cv)).toBe(true); + expect(shouldShowPreCheck(cv)).toBe(false); + }); + }); + + describe('Scenario 4: Cluster failing', () => { + it('should detect FAILING state when cluster is failing', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { + type: 'Failing', + status: 'True', + message: 'Cluster upgrade failed', + lastTransitionTime: '', + }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.FAILING); + expect(result.recommendedWorkflow).toBe('status'); + expect(result.conditions.failing).toBe(true); + }); + + it('should detect OPERATOR_ISSUES state when operators have issues', () => { + const cv = createCV(); + const operators = [ + createOperator('kube-apiserver', true, false), + createOperator('network', false, false), // unavailable + ]; + + const result = detectClusterState(cv, operators); + + expect(result.state).toBe(ClusterState.OPERATOR_ISSUES); + expect(result.recommendedWorkflow).toBe('status'); + expect(result.conditions.hasOperatorIssues).toBe(true); + }); + + it('should show status workflow when failing', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [{ type: 'Failing', status: 'True', lastTransitionTime: '' }], + }, + }); + + expect(shouldShowStatus(cv)).toBe(true); + expect(shouldShowPreCheck(cv)).toBe(false); + expect(isClusterHealthy(cv)).toBe(false); + }); + }); + + describe('Scenario 5: Up-to-date', () => { + it('should detect UP_TO_DATE state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + ], + history: [ + { + state: 'Completed', + version: '4.14.1', + image: 'quay.io/openshift-release-dev/ocp-release:4.14.1', + verified: false, + startedTime: '', + completionTime: '', + }, + ], + desired: { version: '4.14.1', image: '' }, + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.UP_TO_DATE); + expect(result.recommendedWorkflow).toBe('pre-check'); + expect(result.conditions.failing).toBe(false); + expect(result.conditions.progressing).toBe(false); + expect(result.conditions.hasRecommendedUpdates).toBe(false); + }); + }); + + describe('Additional States', () => { + it('should detect NOT_UPGRADEABLE state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + { + type: 'Upgradeable', + status: 'False', + message: 'Cluster is not upgradeable', + lastTransitionTime: '', + }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.NOT_UPGRADEABLE); + expect(result.conditions.upgradeable).toBe(false); + }); + + it('should detect UPDATE_SERVICE_FAILURE state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Progressing', status: 'False', lastTransitionTime: '' }, + { + type: 'RetrievedUpdates', + status: 'False', + message: 'Failed to retrieve updates', + lastTransitionTime: '', + }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.UPDATE_SERVICE_FAILURE); + expect(result.conditions.hasUpdateServiceFailure).toBe(true); + }); + + it('should detect INVALID_CONFIGURATION state', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { + type: 'Invalid', + status: 'True', + message: 'Invalid cluster configuration', + lastTransitionTime: '', + }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.INVALID_CONFIGURATION); + expect(result.conditions.invalid).toBe(true); + }); + + it('should detect UNKNOWN_STATE when conditions have unknown status', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'Unknown', lastTransitionTime: '' }, + { type: 'Available', status: 'Unknown', lastTransitionTime: '' }, + ], + }, + }); + + const result = detectClusterState(cv); + + expect(result.state).toBe(ClusterState.UNKNOWN_STATE); + expect(result.conditions.hasUnknownConditions).toBe(true); + }); + }); + + describe('Cluster Health', () => { + it('should report healthy cluster', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + { type: 'Failing', status: 'False', lastTransitionTime: '' }, + { type: 'Available', status: 'True', lastTransitionTime: '' }, + ], + }, + }); + const operators = [createOperator('kube-apiserver', true, false)]; + + expect(isClusterHealthy(cv, operators)).toBe(true); + }); + + it('should report unhealthy when failing', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [{ type: 'Failing', status: 'True', lastTransitionTime: '' }], + }, + }); + + expect(isClusterHealthy(cv)).toBe(false); + }); + + it('should report unhealthy when operators have issues', () => { + const cv = createCV(); + const operators = [createOperator('network', true, true)]; // degraded + + expect(isClusterHealthy(cv, operators)).toBe(false); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-matrix.spec.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-matrix.spec.ts new file mode 100644 index 00000000000..94d4f6596b2 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/cluster-state-matrix.spec.ts @@ -0,0 +1,744 @@ +/** + * OLS Cluster State Matrix Tests + * + * This file serves as the living documentation for all possible OpenShift cluster states + * and their corresponding OLS workflow behavior. Each test case represents a specific + * cluster state and validates which button appears and which prompt is used. + * + * Cluster State Determination: + * - ClusterVersion conditions (Failing, Progressing, Available, Invalid, RetrievedUpdates, ReleaseAccepted) + * - ClusterOperator health (Available, Degraded, Progressing) + * - Update availability (availableUpdates, conditionalUpdates) + * + * Workflow Phases: + * - 'status': Shows "Update status" button + * - Uses createProgressPrompt when Progressing=True and no failures + * - Uses createTroubleshootPrompt when failures detected + * - 'pre-check': Shows "Pre-check with AI" button + * - Uses createPreCheckPrompt when updates available + * - Uses createPreCheckSpecificVersionPrompt when specific version selected + * - Uses createPreCheckNoUpdatesPrompt when no updates available + */ + +import type { TFunction } from 'i18next'; +import type { + ClusterVersionKind, + ClusterOperator, + ClusterVersionCondition, + ConditionalUpdate, + UpdateHistory, + Release, + K8sResourceCondition, +} from '@console/internal/module/k8s'; +import { + determineWorkflowPhase, + determineWorkflowButtons, + generateUpdatePrompt, +} from '../workflow-utils'; +import { createCondition } from './test-helpers'; + +// Helper to create ClusterVersion with flexible options +const createClusterVersion = (options: { + failing?: boolean; + progressing?: boolean; + available?: boolean; + invalid?: boolean; + retrievedUpdates?: boolean | { status: boolean; message?: string }; + releaseAccepted?: boolean | { status: boolean; message?: string }; + availableUpdates?: number; + conditionalUpdates?: number; + desiredUpdate?: { version: string }; + historyState?: 'Completed' | 'Partial'; +}): ClusterVersionKind => { + const conditions: ClusterVersionCondition[] = [ + ...(options.failing !== undefined + ? [ + createCondition( + 'Failing', + options.failing ? 'True' : 'False', + options.failing ? 'UpdateFailed' : 'NotFailing', + options.failing ? 'Update failed' : 'Not failing', + ), + ] + : []), + ...(options.progressing !== undefined + ? [ + createCondition( + 'Progressing', + options.progressing ? 'True' : 'False', + options.progressing ? 'UpdateProgressing' : 'NotProgressing', + options.progressing ? 'Working towards 4.15.0' : 'Not progressing', + ), + ] + : []), + ...(options.available !== undefined + ? [ + createCondition( + 'Available', + options.available ? 'True' : 'False', + options.available ? 'ClusterAvailable' : 'ClusterNotAvailable', + options.available ? 'Cluster available' : 'Cluster not available', + ), + ] + : []), + ...(options.invalid !== undefined + ? [ + createCondition( + 'Invalid', + options.invalid ? 'True' : 'False', + options.invalid ? 'InvalidConfiguration' : 'ValidConfiguration', + options.invalid ? 'Invalid configuration' : 'Valid configuration', + ), + ] + : []), + ...(options.retrievedUpdates !== undefined + ? [ + createCondition( + 'RetrievedUpdates', + ( + typeof options.retrievedUpdates === 'boolean' + ? options.retrievedUpdates + : options.retrievedUpdates.status + ) + ? 'True' + : 'False', + ( + typeof options.retrievedUpdates === 'boolean' + ? options.retrievedUpdates + : options.retrievedUpdates.status + ) + ? 'UpdatesRetrieved' + : 'UpdatesNotRetrieved', + (typeof options.retrievedUpdates === 'boolean' + ? undefined + : options.retrievedUpdates.message) || + (( + typeof options.retrievedUpdates === 'boolean' + ? options.retrievedUpdates + : options.retrievedUpdates.status + ) + ? 'Updates retrieved' + : 'Cannot retrieve updates'), + ), + ] + : []), + ...(options.releaseAccepted !== undefined + ? [ + createCondition( + 'ReleaseAccepted', + ( + typeof options.releaseAccepted === 'boolean' + ? options.releaseAccepted + : options.releaseAccepted.status + ) + ? 'True' + : 'False', + ( + typeof options.releaseAccepted === 'boolean' + ? options.releaseAccepted + : options.releaseAccepted.status + ) + ? 'ReleaseVerified' + : 'ReleaseRejected', + (typeof options.releaseAccepted === 'boolean' + ? undefined + : options.releaseAccepted.message) || + (( + typeof options.releaseAccepted === 'boolean' + ? options.releaseAccepted + : options.releaseAccepted.status + ) + ? 'Release accepted' + : 'Release verification failed'), + ), + ] + : []), + ]; + + const availableUpdates = options.availableUpdates + ? Array.from({ length: options.availableUpdates }, (_, i) => ({ + version: `4.15.${i}`, + image: `registry.redhat.io/openshift4/ose:4.15.${i}`, + url: `https://access.redhat.com/errata/RHSA-2026-${i}`, + })) + : undefined; + + const conditionalUpdates: ConditionalUpdate[] | undefined = options.conditionalUpdates + ? Array.from({ length: options.conditionalUpdates }, (_, i) => ({ + release: { + version: `4.16.${i}`, + image: `registry.redhat.io/openshift4/ose:4.16.${i}`, + }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'KnownIssue', + 'Cluster may experience issues during upgrade. See https://access.redhat.com/solutions/7001234', + ), + ], + })) + : undefined; + + return { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + name: 'version', + resourceVersion: '12345', + uid: 'test-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: { + channel: 'stable-4.14', + clusterID: 'test-cluster-id', + desiredUpdate: options.desiredUpdate as Release, + }, + status: { + conditions, + desired: { + version: '4.14.10', + image: 'registry.redhat.io/openshift4/ose:4.14.10', + url: 'https://access.redhat.com/errata/RHSA-2026-1234', + } as Release, + history: [ + { + version: '4.14.10', + state: options.historyState || 'Completed', + startedTime: '2024-01-01T00:00:00Z', + completionTime: options.historyState === 'Completed' ? '2024-01-01T01:00:00Z' : undefined, + image: 'registry.redhat.io/openshift4/ose:4.14.10', + verified: false, + } as UpdateHistory, + ], + availableUpdates, + conditionalUpdates, + observedGeneration: 1, + versionHash: 'test-hash', + }, + }; +}; + +// Helper to create ClusterOperator +const createOperator = ( + name: string, + options: { + available?: boolean; + degraded?: boolean; + progressing?: boolean; + }, +): ClusterOperator => { + const conditions: K8sResourceCondition[] = [ + ...(options.available !== undefined + ? [ + createCondition( + 'Available', + options.available ? 'True' : 'False', + options.available ? 'OperatorAvailable' : 'OperatorUnavailable', + options.available ? 'Operator available' : 'Operator unavailable', + ), + ] + : []), + ...(options.degraded !== undefined + ? [ + createCondition( + 'Degraded', + options.degraded ? 'True' : 'False', + options.degraded ? 'OperatorDegraded' : 'OperatorHealthy', + options.degraded ? 'Operator degraded' : 'Operator healthy', + ), + ] + : []), + ...(options.progressing !== undefined + ? [ + createCondition( + 'Progressing', + options.progressing ? 'True' : 'False', + options.progressing ? 'OperatorProgressing' : 'OperatorStable', + options.progressing ? 'Operator progressing' : 'Operator stable', + ), + ] + : []), + ]; + + return { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterOperator', + metadata: { + name, + resourceVersion: '12345', + uid: `${name}-uid`, + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: {}, + status: { conditions, versions: [], relatedObjects: [] }, + }; +}; + +describe('OLS Cluster State Matrix - Complete Scenarios', () => { + const mockT = ((key: string) => key) as TFunction; + + /** + * STATE 1: Upgrade In Progress (Healthy) + * Conditions: Failing=False, Progressing=True, Available=True + * Operators: All healthy, some progressing + * Expected: status phase → createProgressPrompt + */ + describe('State 1: Upgrade In Progress (Healthy)', () => { + it('should show status button and use progress prompt', () => { + const cv = createClusterVersion({ + failing: false, + progressing: true, + available: true, + historyState: 'Partial', + }); + + const operators = [ + createOperator('console', { available: true, degraded: false, progressing: true }), + createOperator('authentication', { available: true, degraded: false, progressing: false }), + ]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const phase = determineWorkflowPhase(cv, operators); + expect(phase).toBe('status'); + + const prompt = generateUpdatePrompt(phase, cv, mockT, operators); + expect(prompt).toContain('Progress Monitor'); + expect(prompt).toContain('operators'); + }); + }); + + /** + * STATE 2: Upgrade Failing (Progressing with Failures) + * Conditions: Failing=True, Progressing=True + * Operators: May have failures + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 2: Upgrade Failing', () => { + it('should show status button and use troubleshoot prompt when cluster failing', () => { + const cv = createClusterVersion({ + failing: true, + progressing: true, + historyState: 'Partial', + }); + + const operators = [createOperator('authentication', { available: false, degraded: true })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const phase = determineWorkflowPhase(cv, operators); + expect(phase).toBe('status'); + + const prompt = generateUpdatePrompt(phase, cv, mockT, operators); + expect(prompt).toContain('Troubleshoot Analysis'); + expect(prompt).toContain('Root Cause'); + expect(prompt).toContain('Failed ClusterOperators'); + }); + }); + + /** + * STATE 3: Upgrade Stalled (Operator Issues During Progress) + * Conditions: Failing=False, Progressing=True + * Operators: Some with Available=False or Degraded=True + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 3: Upgrade Stalled (Operator Issues)', () => { + it('should show status button and use troubleshoot prompt when operators have issues', () => { + const cv = createClusterVersion({ + failing: false, + progressing: true, + historyState: 'Partial', + }); + + const operators = [ + createOperator('console', { available: true, degraded: false }), + createOperator('authentication', { available: false, degraded: false }), + ]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const prompt = generateUpdatePrompt('status', cv, mockT, operators); + expect(prompt).toContain('Troubleshoot'); + }); + }); + + /** + * STATE 4: Cluster Failing (Not Progressing) + * Conditions: Failing=True, Progressing=False + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 4: Cluster Failing (Idle)', () => { + it('should show status button and use troubleshoot prompt', () => { + const cv = createClusterVersion({ + failing: true, + progressing: false, + }); + + const buttons = determineWorkflowButtons(cv); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const prompt = generateUpdatePrompt('status', cv, mockT); + expect(prompt).toContain('Troubleshoot'); + expect(prompt).toContain('Root Cause'); + }); + }); + + /** + * STATE 5: Operator Issues (Cluster Idle) + * Conditions: Failing=False, Progressing=False, Available=True + * Operators: Some with Available=False or Degraded=True + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 5: Operator Issues (Cluster Idle)', () => { + it('should show status button when operators degraded but cluster not failing', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + }); + + const operators = [createOperator('authentication', { available: true, degraded: true })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const prompt = generateUpdatePrompt('status', cv, mockT, operators); + expect(prompt).toContain('Troubleshoot'); + }); + + it('should show status button when operators unavailable but cluster not failing', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + }); + + const operators = [createOperator('console', { available: false, degraded: false })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + }); + + /** + * STATE 6: Update Service Issues + * Conditions: RetrievedUpdates=False with message OR ReleaseAccepted=False with message + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 6: Update Service Issues', () => { + it('should show status button when RetrievedUpdates=False', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + retrievedUpdates: { status: false, message: 'Cannot connect to update service' }, + }); + + const buttons = determineWorkflowButtons(cv); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should show status button when ReleaseAccepted=False', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + releaseAccepted: { status: false, message: 'Signature verification failed' }, + }); + + const buttons = determineWorkflowButtons(cv); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + }); + + /** + * STATE 7: Ready for Update (Available Updates) + * Conditions: Failing=False, Progressing=False, Available=True + * Operators: All healthy + * Updates: availableUpdates has items + * Expected: pre-check phase → createPreCheckPrompt + */ + describe('State 7: Ready for Update', () => { + it('should show pre-check button when updates available and cluster healthy', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + retrievedUpdates: true, + availableUpdates: 5, + }); + + const operators = [ + createOperator('console', { available: true, degraded: false }), + createOperator('authentication', { available: true, degraded: false }), + ]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showPreCheck).toBe(true); + expect(buttons.showStatus).toBe(false); + + const phase = determineWorkflowPhase(cv, operators); + expect(phase).toBe('pre-check'); + + const prompt = generateUpdatePrompt(phase, cv, mockT, operators); + expect(prompt).toContain('Pre-Check Analysis'); + expect(prompt).toContain('Available Updates'); + }); + }); + + /** + * STATE 8: Conditional Updates with Concerns + * Conditions: Failing=False, Progressing=False + * Updates: availableUpdates empty/small, conditionalUpdates has items with conditions + * Expected: pre-check phase → createPreCheckNoUpdatesPrompt (no availableUpdates) + * + * NOTE: Current implementation treats conditionalUpdates separately from availableUpdates. + * When availableUpdates is empty, we show the "no updates" prompt regardless of + * conditionalUpdates presence. Future enhancement could add dedicated handling for + * conditionalUpdates to parse and explain the conditions/risks. + * + * ConditionalUpdate structure: + * - release: \{ version, image \} + * - conditions: [\{ type, status, reason, message \}] + * + * The conditions typically have type="Recommended" status="False" with risk details in the message. + */ + describe('State 8: Conditional Updates with Risks', () => { + it('should show pre-check button with conditional updates (current behavior)', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + availableUpdates: 0, + conditionalUpdates: 2, + }); + + const operators = [createOperator('console', { available: true, degraded: false })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showPreCheck).toBe(true); + expect(buttons.showStatus).toBe(false); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT, operators); + // Current implementation: no availableUpdates → uses no-updates prompt + expect(prompt).toContain('Health Assessment'); + expect(prompt).toContain('no available updates'); + }); + + it('should handle conditional updates alongside available updates', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + availableUpdates: 1, // Has some available updates + conditionalUpdates: 2, // Plus conditional updates with risks + }); + + const operators = [createOperator('console', { available: true, degraded: false })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showPreCheck).toBe(true); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT, operators); + // When availableUpdates exist, uses general pre-check + expect(prompt).toContain('Pre-Check Analysis'); + // TODO: Future enhancement to parse conditionalUpdates[].conditions array + // and explain the risk conditions to the user + }); + }); + + /** + * STATE 9: No Updates Available (Fully Updated) + * Conditions: Failing=False, Progressing=False, RetrievedUpdates=True + * Updates: availableUpdates empty, conditionalUpdates empty + * Expected: pre-check phase → createPreCheckNoUpdatesPrompt + */ + describe('State 9: No Updates Available', () => { + it('should show pre-check button and use no-updates prompt', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + retrievedUpdates: true, + availableUpdates: 0, + }); + + const operators = [createOperator('console', { available: true, degraded: false })]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showPreCheck).toBe(true); + expect(buttons.showStatus).toBe(false); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT, operators); + expect(prompt).toContain('Health Assessment'); + expect(prompt).toContain('no available updates'); + }); + }); + + /** + * STATE 10: Specific Version Selected + * Conditions: Failing=False, Progressing=False + * Updates: availableUpdates has items, user selected specific version + * Expected: pre-check phase → createPreCheckSpecificVersionPrompt + */ + describe('State 10: Specific Version Selected', () => { + it('should use specific version prompt when target version provided', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + availableUpdates: 3, + }); + + const targetVersion = '4.15.1'; + const prompt = generateUpdatePrompt( + 'pre-check', + cv, + mockT, + undefined, + undefined, + undefined, + targetVersion, + ); + + expect(prompt).toContain('Pre-Check Analysis'); + expect(prompt).toContain('4.15.1'); + expect(prompt).toContain('Target Version'); + }); + }); + + /** + * STATE 11: Invalid Condition + * Conditions: Invalid=True + * Expected: status phase → createTroubleshootPrompt + */ + describe('State 11: Invalid Condition', () => { + it('should show status button when Invalid=True', () => { + const cv = createClusterVersion({ + invalid: true, + progressing: false, + }); + + const buttons = determineWorkflowButtons(cv); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + + const prompt = generateUpdatePrompt('status', cv, mockT); + expect(prompt).toContain('Troubleshoot'); + }); + }); + + /** + * Edge Cases and Combined Conditions + */ + describe('Edge Cases', () => { + it('should prioritize failures over progressing (Failing=True, Progressing=True)', () => { + const cv = createClusterVersion({ + failing: true, + progressing: true, + }); + + const phase = determineWorkflowPhase(cv); + expect(phase).toBe('status'); + + const prompt = generateUpdatePrompt(phase, cv, mockT); + expect(prompt).toContain('Troubleshoot'); // Not Progress + }); + + it('should handle multiple service issues (RetrievedUpdates=False, ReleaseAccepted=False)', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + retrievedUpdates: { status: false, message: 'Error 1' }, + releaseAccepted: { status: false, message: 'Error 2' }, + }); + + const buttons = determineWorkflowButtons(cv); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should handle cluster-level failures with operator issues', () => { + const cv = createClusterVersion({ + failing: true, + progressing: false, + }); + + const operators = [createOperator('authentication', { available: false, degraded: true })]; + + const phase = determineWorkflowPhase(cv, operators); + expect(phase).toBe('status'); + }); + + it('should show pre-check only when fully healthy', () => { + const cv = createClusterVersion({ + failing: false, + progressing: false, + available: true, + invalid: false, + retrievedUpdates: true, + releaseAccepted: true, + }); + + const operators = [ + createOperator('console', { available: true, degraded: false }), + createOperator('authentication', { available: true, degraded: false }), + ]; + + const buttons = determineWorkflowButtons(cv, operators); + expect(buttons.showPreCheck).toBe(true); + expect(buttons.showStatus).toBe(false); + }); + }); + + /** + * Condition Status Field Validation + * These tests ensure we always check the status field, not just the condition type + */ + describe('Condition Status Field Checking', () => { + // Table-driven test data + const conditionStatusTests = [ + { + name: 'Failing condition with status=False should NOT trigger status button', + cvOptions: { failing: false, progressing: false, available: true }, + operators: undefined, + expectedButtons: { showPreCheck: true, showStatus: false }, + }, + { + name: 'Degraded condition with status=False should NOT trigger status button', + cvOptions: { failing: false, progressing: false }, + operators: [{ name: 'console', options: { available: true, degraded: false } }], + expectedButtons: { showPreCheck: true, showStatus: false }, + }, + { + name: 'Available condition with status=True should NOT trigger status button', + cvOptions: { failing: false, progressing: false, available: true }, + operators: undefined, + expectedButtons: { showPreCheck: true, showStatus: false }, + }, + ]; + + conditionStatusTests.forEach(({ name, cvOptions, operators, expectedButtons }) => { + it(name, () => { + const cv = createClusterVersion(cvOptions); + const ops = operators?.map((o) => createOperator(o.name, o.options)); + + const buttons = determineWorkflowButtons(cv, ops); + expect(buttons.showPreCheck).toBe(expectedButtons.showPreCheck); + expect(buttons.showStatus).toBe(expectedButtons.showStatus); + }); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/enhancements.spec.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/enhancements.spec.ts new file mode 100644 index 00000000000..464ac34cfa0 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/enhancements.spec.ts @@ -0,0 +1,481 @@ +/** + * OLS Workflow Enhancements Tests + * + * This file tests the implemented enhancements from README.md: + * 1. Conditional Updates with Risk Analysis + * 2. MachineConfigPool Integration + * 3. Alert Correlation + * 4. Update Service Diagnostics + */ + +import type { TFunction } from 'i18next'; +import type { Alert } from '@console/dynamic-plugin-sdk'; +import { AlertStates, RuleStates } from '@console/dynamic-plugin-sdk'; +import type { + ClusterVersionKind, + MachineConfigPoolKind, + ConditionalUpdate, +} from '@console/internal/module/k8s'; +import { hasConditionalUpdates } from '../predicates'; +import { getConditionalUpdateRisks, generateUpdatePrompt } from '../workflow-utils'; +import { createCondition } from './test-helpers'; + +describe('OLS Workflow Enhancements', () => { + const mockT = ((key: string) => key) as TFunction; + + // Helper to create ClusterVersion + const createCV = (overrides?: Partial): ClusterVersionKind => ({ + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + name: 'version', + resourceVersion: '12345', + uid: 'test-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: { + channel: 'stable-4.15', + clusterID: 'test-cluster-id', + }, + status: { + desired: { + version: '4.15.0', + image: 'registry.redhat.io/openshift4/ose:4.15.0', + }, + history: [ + { + version: '4.15.0', + state: 'Completed', + startedTime: '2024-01-01T00:00:00Z', + completionTime: '2024-01-01T01:00:00Z', + image: 'registry.redhat.io/openshift4/ose:4.15.0', + verified: false, + }, + ], + observedGeneration: 1, + versionHash: 'test-hash', + }, + ...overrides, + }); + + /** + * ENHANCEMENT 1: Conditional Updates with Risk Analysis + */ + describe('Enhancement 1: Conditional Updates with Risk Analysis', () => { + it('should detect conditional updates presence', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'NetworkDisruption', + 'Clusters using OVN may experience network disruption. See https://access.redhat.com/solutions/7001234', + ), + ], + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditionalUpdates, + }, + }); + + expect(hasConditionalUpdates(cv)).toBe(true); + }); + + it('should return false when no conditional updates', () => { + const cv = createCV(); + expect(hasConditionalUpdates(cv)).toBe(false); + }); + + it('should extract risk information from conditional updates', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'NetworkDisruption', + 'Clusters using OVN-Kubernetes may experience temporary network disruption during upgrade', + ), + ], + }, + { + release: { version: '4.16.1', image: 'registry.redhat.io/openshift4/ose:4.16.1' }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'StorageIssue', + 'Clusters with Ceph storage may require additional configuration', + ), + ], + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditionalUpdates, + }, + }); + + const risks = getConditionalUpdateRisks(cv); + + expect(risks).toHaveLength(2); + expect(risks[0].version).toBe('4.16.0'); + expect(risks[0].risks).toHaveLength(1); + expect(risks[0].risks[0].reason).toBe('NetworkDisruption'); + expect(risks[0].risks[0].message).toContain('OVN-Kubernetes'); + + expect(risks[1].version).toBe('4.16.1'); + expect(risks[1].risks[0].reason).toBe('StorageIssue'); + }); + + it('should handle conditional updates with multiple risk conditions', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition('Recommended', 'False', 'NetworkDisruption', 'Network disruption risk'), + createCondition('Recommended', 'False', 'StorageIssue', 'Storage configuration risk'), + ], + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditionalUpdates, + }, + }); + + const risks = getConditionalUpdateRisks(cv); + expect(risks[0].risks).toHaveLength(2); + }); + + it('should filter out non-risk conditions (Recommended=True)', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition('Recommended', 'True', 'Safe', 'This update is recommended'), // This is recommended (not a risk) + createCondition('Recommended', 'False', 'NetworkDisruption', 'Network risk'), // This is a risk + ], + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditionalUpdates, + }, + }); + + const risks = getConditionalUpdateRisks(cv); + expect(risks[0].risks).toHaveLength(1); + expect(risks[0].risks[0].reason).toBe('NetworkDisruption'); + }); + + it('should include conditional updates analysis in pre-check prompt with available updates', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'NetworkDisruption', + 'OVN network disruption possible', + ), + ], + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + ], + conditionalUpdates, + // Include availableUpdates to trigger general pre-check prompt + availableUpdates: [ + { + version: '4.15.1', + image: 'registry.redhat.io/openshift4/ose:4.15.1', + }, + ], + }, + }); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT); + + // Prompt should mention conditional updates analysis + expect(prompt).toContain('Conditional Updates'); + expect(prompt).toContain('Risk Analysis'); + expect(prompt).toContain('conditionalUpdates'); + }); + }); + + /** + * ENHANCEMENT 2: MachineConfigPool Integration + */ + describe('Enhancement 2: MachineConfigPool Integration', () => { + it('should include MCP analysis in prompts when MCP data provided', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + createCondition('Progressing', 'True', 'Progressing', 'Upgrade in progress'), + ], + }, + }); + + const mcps: MachineConfigPoolKind[] = [ + { + apiVersion: 'machineconfiguration.openshift.io/v1', + kind: 'MachineConfigPool', + metadata: { + name: 'master', + resourceVersion: '12345', + uid: 'master-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: { + paused: false, + }, + status: { + machineCount: 3, + updatedMachineCount: 2, + readyMachineCount: 2, + unavailableMachineCount: 1, + configuration: { + name: 'rendered-master-123', + source: [], + }, + conditions: [createCondition('Updated', 'False', 'Updating', 'Updating nodes')], + }, + }, + ]; + + const prompt = generateUpdatePrompt('status', cv, mockT, [], mcps); + + // Prompt should mention MCP progress + expect(prompt).toContain('MachineConfigPool'); + }); + }); + + /** + * ENHANCEMENT 3: Alert Correlation + */ + describe('Enhancement 3: Alert Correlation', () => { + it('should include alert analysis in prompts when alert data provided', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [createCondition('Failing', 'True', 'OperatorFailing', 'Operator failing')], + }, + }); + + const alerts: Alert[] = [ + { + labels: { + alertname: 'KubePodCrashLooping', + severity: 'critical', + namespace: 'openshift-authentication', + }, + annotations: { + summary: 'Pod is crash looping', + description: 'Pod oauth-openshift is crash looping', + }, + state: AlertStates.Firing, + activeAt: '2024-01-01T00:00:00Z', + value: 1, + rule: { + name: 'KubePodCrashLooping', + query: 'rate(kube_pod_container_status_restarts_total[15m]) > 0', + duration: 300, + labels: { + severity: 'critical', + }, + annotations: {}, + alerts: [], + state: RuleStates.Firing, + type: 'alerting', + id: 'test-rule-id', + }, + }, + ]; + + const prompt = generateUpdatePrompt('status', cv, mockT, [], undefined, alerts); + + // Prompt should be a troubleshoot prompt (failing cluster) + expect(prompt).toContain('Troubleshoot'); + // All troubleshoot prompts can mention alerts in the diagnostics + expect(prompt.length).toBeGreaterThan(0); + }); + }); + + /** + * ENHANCEMENT 4: Update Service Diagnostics + */ + describe('Enhancement 4: Update Service Diagnostics', () => { + it('should enhance Cincinnati troubleshooting when RetrievedUpdates=False', () => { + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + createCondition( + 'RetrievedUpdates', + 'False', + 'ConnectionTimeout', + 'Unable to retrieve updates: dial tcp 52.85.42.1:443: i/o timeout', + ), + ], + }, + }); + + const prompt = generateUpdatePrompt('status', cv, mockT); + + // Prompt should have Cincinnati/Update Service diagnostics + // The troubleshoot prompt includes update service analysis + expect(prompt).toContain('Update Service'); + expect(prompt).toContain('Troubleshoot'); + }); + + it('should include upstream configuration analysis', () => { + const cv = createCV({ + spec: { + ...createCV().spec, + upstream: 'https://custom-cincinnati.example.com/api/upgrades_info', + }, + status: { + ...createCV().status, + conditions: [ + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + ], + availableUpdates: [], + }, + }); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT); + + // Prompt should check for upstream configuration + // The no-updates prompt includes update service analysis + expect(prompt).toContain('upstream'); + expect(prompt.length).toBeGreaterThan(0); + // Verify it's checking the ClusterVersion + expect(prompt).toContain('ClusterVersion'); + }); + }); + + /** + * Integration Tests: All Enhancements Together + */ + describe('Integration: All Enhancements', () => { + it('should handle cluster with conditional updates, MCPs, and alerts', () => { + const conditionalUpdates: ConditionalUpdate[] = [ + { + release: { version: '4.16.0', image: 'registry.redhat.io/openshift4/ose:4.16.0' }, + conditions: [ + createCondition( + 'Recommended', + 'False', + 'NetworkDisruption', + 'Network disruption possible', + ), + ], + }, + ]; + + const mcps: MachineConfigPoolKind[] = [ + { + apiVersion: 'machineconfiguration.openshift.io/v1', + kind: 'MachineConfigPool', + metadata: { + name: 'worker', + resourceVersion: '12345', + uid: 'worker-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: {}, + status: { + machineCount: 5, + updatedMachineCount: 5, + readyMachineCount: 5, + unavailableMachineCount: 0, + configuration: { + name: 'rendered-worker-123', + source: [], + }, + }, + }, + ]; + + const alerts: Alert[] = [ + { + labels: { + alertname: 'KubePersistentVolumeFillingUp', + severity: 'warning', + }, + annotations: { + summary: 'PV filling up', + }, + state: AlertStates.Firing, + activeAt: '2024-01-01T00:00:00Z', + value: 85, + rule: { + name: 'KubePersistentVolumeFillingUp', + query: + 'kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes < 0.15', + duration: 300, + labels: { severity: 'warning' }, + annotations: {}, + alerts: [], + state: RuleStates.Firing, + type: 'alerting', + id: 'test-rule-id', + }, + }, + ]; + + const cv = createCV({ + status: { + ...createCV().status, + conditions: [ + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + ], + conditionalUpdates, + // Include availableUpdates so it uses general pre-check prompt with all enhancements + availableUpdates: [ + { + version: '4.15.1', + image: 'registry.redhat.io/openshift4/ose:4.15.1', + }, + ], + }, + }); + + const prompt = generateUpdatePrompt('pre-check', cv, mockT, [], mcps, alerts); + + // Should include all enhancement features + // Conditional updates analysis is in the enhanced pre-check prompt + expect(prompt).toContain('Conditional Updates'); + // MCP and Alert sections are in all pre-check prompts + expect(prompt).toContain('MachineConfigPool'); + expect(prompt).toContain('Active Alerts'); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/explain-button.spec.tsx b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/explain-button.spec.tsx new file mode 100644 index 00000000000..0b9fb0c9c1b --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/explain-button.spec.tsx @@ -0,0 +1,275 @@ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useTranslation } from 'react-i18next'; +import { useResolvedExtensions } from '@console/dynamic-plugin-sdk'; +import type { ClusterVersionKind } from '@console/internal/module/k8s'; +import { useFlag } from '@console/shared/src/hooks/useFlag'; +import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; +import { renderWithProviders } from '@console/shared/src/test-utils/unit-test-utils'; +import { UpdateWorkflowOLSButton } from '../explain-button'; +import * as workflowUtils from '../workflow-utils'; + +// Mock all external dependencies +jest.mock('react-i18next', () => ({ + useTranslation: jest.fn(), +})); + +jest.mock('@console/shared/src/hooks/useTelemetry', () => ({ + useTelemetry: jest.fn(), +})); + +// Mock the flag hook to return true for OLS availability +jest.mock('@console/shared/src/hooks/useFlag', () => ({ + useFlag: jest.fn(), +})); + +// Mock the dynamic plugin SDK hook +jest.mock('@console/dynamic-plugin-sdk', () => ({ + useResolvedExtensions: jest.fn(), +})); + +jest.mock('../workflow-utils', () => ({ + generateUpdatePrompt: jest.fn(), + getUpdateButtonText: jest.fn(), +})); + +describe('UpdateWorkflowOLSButton', () => { + const mockUseTranslation = useTranslation as jest.Mock; + const mockUseTelemetry = useTelemetry as jest.Mock; + const mockUseFlag = useFlag as jest.Mock; + const mockUseResolvedExtensions = useResolvedExtensions as jest.Mock; + const mockGenerateUpdatePrompt = workflowUtils.generateUpdatePrompt as jest.Mock; + const mockGetUpdateButtonText = workflowUtils.getUpdateButtonText as jest.Mock; + + const mockT = jest.fn((key) => `translated-${key}`); + const mockFireTelemetryEvent = jest.fn(); + const mockOpenOLS = jest.fn(); + + const mockClusterVersion: ClusterVersionKind = { + spec: { + channel: 'stable-4.12', + clusterID: 'test-cluster-id', + }, + status: { + desired: { + version: '4.12.5', + }, + }, + } as ClusterVersionKind; + + beforeEach(() => { + jest.clearAllMocks(); + + // Mock translation + mockUseTranslation.mockReturnValue({ t: mockT }); + + // Mock telemetry + mockUseTelemetry.mockReturnValue(mockFireTelemetryEvent); + + // Mock feature flag - OLS is available + mockUseFlag.mockReturnValue(true); + + // Mock dynamic plugin extension - OLS extension is available + const mockExtension = { + type: 'console.action/provider', + properties: { + contextId: 'ols-open-handler', + provider: () => mockOpenOLS, + }, + }; + mockUseResolvedExtensions.mockReturnValue([ + [mockExtension], // extensions array + true, // resolved flag + ]); + + // Mock workflow utils + mockGenerateUpdatePrompt.mockReturnValue('Generated prompt'); + mockGetUpdateButtonText.mockReturnValue('Get Help'); + }); + + describe('rendering', () => { + it('should render OLS button with correct props for status phase', () => { + renderWithProviders( + , + ); + + const button = screen.getByRole('button'); + expect(button).toBeVisible(); + expect(button).toHaveAttribute('data-test', 'ols-update-status'); + expect(button).toHaveClass('custom-class'); + expect(button).toHaveTextContent('Get Help'); + }); + + it('should render with different data-test attribute for different phases', () => { + renderWithProviders(); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('data-test', 'ols-update-pre-check'); + }); + + it('should not render when OLS flag is disabled', () => { + mockUseFlag.mockReturnValue(false); + + renderWithProviders(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('should not render when OLS extension is not available', () => { + mockUseResolvedExtensions.mockReturnValue([[], false]); + + renderWithProviders(); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + }); + + describe('workflow integration', () => { + it('should call workflow utilities on button click', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const button = screen.getByRole('button'); + await user.click(button); + + expect(mockGenerateUpdatePrompt).toHaveBeenCalledWith( + 'status', + mockClusterVersion, + mockT, + undefined, // clusterOperators + undefined, // machineConfigPools (optional) + undefined, // alerts (optional) + undefined, // targetVersion (optional) + ); + }); + + it('should call openOLS with correct parameters on button click - no attachments (MCP uses tools)', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const button = screen.getByRole('button'); + await user.click(button); + + expect(mockOpenOLS).toHaveBeenCalledWith( + 'Generated prompt', + [], // Empty attachments - MCP tools fetch real-time cluster data + true, + true, + ); + }); + + it('should get button text from workflow utilities', () => { + renderWithProviders(); + + expect(mockGetUpdateButtonText).toHaveBeenCalledWith('status', mockT); + }); + }); + + describe('telemetry tracking', () => { + it('should fire telemetry event when button is clicked', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await user.click(screen.getByRole('button')); + + expect(mockFireTelemetryEvent).toHaveBeenCalledWith('OLS Update Workflow Button Clicked', { + source: 'cluster-settings', + workflowPhase: 'status', + clusterVersion: '4.12.5', + updateChannel: 'stable-4.12', + clusterId: 'test-cluster-id', + }); + }); + + it('should handle missing version data in telemetry', async () => { + const user = userEvent.setup(); + const cvWithoutVersion: ClusterVersionKind = { + spec: { + channel: 'stable-4.12', + clusterID: 'test-cluster-id', + }, + status: {}, + } as ClusterVersionKind; + + renderWithProviders(); + + await user.click(screen.getByRole('button')); + + expect(mockFireTelemetryEvent).toHaveBeenCalledWith('OLS Update Workflow Button Clicked', { + source: 'cluster-settings', + workflowPhase: 'status', + clusterVersion: 'unknown', + updateChannel: 'stable-4.12', + clusterId: 'test-cluster-id', + }); + }); + }); + + describe('workflow phases and props', () => { + it('should handle different workflow phases correctly', () => { + const phases = ['status', 'pre-check'] as const; + + phases.forEach((phase) => { + mockGetUpdateButtonText.mockReturnValue(`Get ${phase} Help`); + + const { unmount } = renderWithProviders( + , + ); + + const button = screen.getByRole('button'); + expect(button).toHaveAttribute('data-test', `ols-update-${phase}`); + expect(button).toHaveTextContent(`Get ${phase} Help`); + + unmount(); + jest.clearAllMocks(); + // Reset mocks for next iteration + mockUseTranslation.mockReturnValue({ t: mockT }); + mockUseTelemetry.mockReturnValue(mockFireTelemetryEvent); + mockUseFlag.mockReturnValue(true); + mockUseResolvedExtensions.mockReturnValue([ + [ + { + type: 'console.action/provider', + properties: { contextId: 'ols-open-handler', provider: () => mockOpenOLS }, + }, + ], + true, + ]); + mockGenerateUpdatePrompt.mockReturnValue('Generated prompt'); + }); + }); + + it('should render without optional className when not provided', () => { + renderWithProviders(); + + const button = screen.getByRole('button'); + expect(button).toBeVisible(); + expect(button).toHaveAttribute('data-test', 'ols-update-status'); + }); + }); + + describe('onClick callback', () => { + it('should call onClick callback when provided', async () => { + const user = userEvent.setup(); + const mockOnClick = jest.fn(); + + renderWithProviders( + , + ); + + const button = screen.getByRole('button'); + await user.click(button); + + expect(mockOnClick).toHaveBeenCalledTimes(1); + }); + + it('should not error when onClick callback is not provided', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const button = screen.getByRole('button'); + + await expect(user.click(button)).resolves.not.toThrow(); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/test-helpers.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/test-helpers.ts new file mode 100644 index 00000000000..901e7e55cf2 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/test-helpers.ts @@ -0,0 +1,39 @@ +/** + * Shared test helpers for OLS update workflow tests + */ +import type { K8sResourceConditionStatus } from '@console/internal/module/k8s'; +import { K8sResourceConditionStatus as ConditionStatus } from '@console/internal/module/k8s'; + +/** + * Condition status constants to use instead of string literals + */ +export const STATUS = { + TRUE: ConditionStatus.True, + FALSE: ConditionStatus.False, + UNKNOWN: ConditionStatus.Unknown, +} as const; + +/** + * Helper to create condition objects (ClusterVersionCondition, K8sResourceCondition, etc.) + * without excessive type casts. Centralizes the type casts needed for test data. + * + * @param type - Condition type (e.g., 'Progressing', 'Failing', 'Available') + * @param status - Condition status ('True', 'False', or 'Unknown') + * @param reason - Reason for the condition + * @param message - Human-readable message + * @returns Condition object typed as any to work with all condition types in tests + */ +export function createCondition( + type: string, + status: 'True' | 'False' | 'Unknown', + reason: string, + message: string, +): any { + return { + type, + status: status as K8sResourceConditionStatus, + reason, + message, + lastTransitionTime: '2024-01-01T00:00:00Z', + }; +} diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-comprehensive.spec.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-comprehensive.spec.ts new file mode 100644 index 00000000000..57cdd7812a4 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-comprehensive.spec.ts @@ -0,0 +1,555 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { TFunction } from 'i18next'; +import type { + ClusterVersionKind, + ClusterOperator, + ClusterVersionCondition, + UpdateHistory, + Release, + K8sResourceCondition, +} from '@console/internal/module/k8s'; +import { + determineWorkflowPhase, + determineWorkflowButtons, + hasOperatorIssues, + generateUpdatePrompt, + getUpdateButtonTranslationKey, +} from '../workflow-utils'; +import { createCondition } from './test-helpers'; + +// Simple AvailableUpdate type for testing +interface AvailableUpdate { + version: string; + image: string; + url: string; +} + +describe('OLS Update Workflow - Comprehensive Requirements Tests', () => { + // Mock translation function with proper TFunction typing + const mockT = ((key: string, options?: Record) => { + if (options) { + return key.replace(/\{\{(\w+)\}\}/g, (match, prop) => String(options[prop] || match)); + } + return key; + }) as TFunction; + + // Helper to create mock ClusterVersion + const createMockClusterVersion = (conditions: ClusterVersionCondition[]): ClusterVersionKind => ({ + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + name: 'version', + resourceVersion: '12345', + uid: 'test-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: { + channel: 'stable-4.12', + clusterID: 'test-cluster-id', + }, + status: { + conditions, + history: [ + { + version: '4.12.1', + state: 'Completed', + startedTime: '2024-01-01T00:00:00Z', + completionTime: '2024-01-01T01:00:00Z', + image: 'registry.redhat.io/openshift4/ose:4.12.1', + verified: false, + } as UpdateHistory, + ], + desired: { + version: '4.12.2', + image: 'registry.redhat.io/openshift4/ose:4.12.2', + url: 'https://example.com', + } as Release, + availableUpdates: [ + { + version: '4.12.3', + image: 'registry.redhat.io/openshift4/ose:4.12.3', + url: 'https://example.com', + } as AvailableUpdate, + ], + observedGeneration: 1, + versionHash: 'test-hash', + }, + }); + + // Helper to create mock ClusterOperator + const createMockClusterOperator = ( + name: string, + conditions: K8sResourceCondition[], + ): ClusterOperator => ({ + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterOperator', + metadata: { + name, + resourceVersion: '12345', + uid: `${name}-uid`, + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: {}, + status: { + conditions, + versions: [], + relatedObjects: [], + }, + }); + + describe('Button Appearance Logic - Table Requirements', () => { + describe('Pre-check Button', () => { + it('should appear when Progressing=False and Failing=False', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + createCondition('Available', 'True', 'Available', 'Available'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showPreCheck).toBe(true); + expect(buttons.showStatus).toBe(false); + }); + + it('should NEVER appear when Failing=True (cluster level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'True', 'UpdateFailed', 'Update failed'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showPreCheck).toBe(false); + expect(buttons.showStatus).toBe(true); + }); + + it('should NOT appear when operators are degraded (operator level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const operatorsWithIssues = [ + createMockClusterOperator('test-operator', [ + createCondition('Degraded', 'True', 'OperatorDegraded', 'Operator degraded'), + ]), + ]; + + const buttons = determineWorkflowButtons(cv, operatorsWithIssues); + expect(buttons.showPreCheck).toBe(false); + expect(buttons.showStatus).toBe(true); + }); + + it('should NOT appear when operators are unavailable (operator level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const operatorsWithIssues = [ + createMockClusterOperator('test-operator', [ + createCondition('Available', 'False', 'OperatorUnavailable', 'Operator unavailable'), + ]), + ]; + + const buttons = determineWorkflowButtons(cv, operatorsWithIssues); + expect(buttons.showPreCheck).toBe(false); + expect(buttons.showStatus).toBe(true); + }); + }); + + describe('Update Status Button', () => { + it('should appear when Progressing=True (cluster level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'True', 'UpdateProgressing', 'Update in progress'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should NOT appear when Progressing=False', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(false); + }); + }); + + describe('Troubleshoot Button', () => { + it('should appear when Failing=True (cluster level)', () => { + const cv = createMockClusterVersion([ + createCondition('Failing', 'True', 'UpdateFailed', 'Update failed'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should appear when Degraded=True (operator level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const degradedOperators = [ + createMockClusterOperator('degraded-operator', [ + createCondition('Degraded', 'True', 'OperatorDegraded', 'Operator degraded'), + ]), + ]; + + const buttons = determineWorkflowButtons(cv, degradedOperators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should appear when Available=False (operator level)', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const unavailableOperators = [ + createMockClusterOperator('unavailable-operator', [ + createCondition('Available', 'False', 'OperatorUnavailable', 'Operator unavailable'), + ]), + ]; + + const buttons = determineWorkflowButtons(cv, unavailableOperators); + expect(buttons.showStatus).toBe(true); + expect(buttons.showPreCheck).toBe(false); + }); + + it('should appear for ReleaseAccepted=False', () => { + const cv = createMockClusterVersion([ + createCondition('ReleaseAccepted', 'False', 'ReleaseRejected', 'Release not accepted'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(true); + }); + + it('should appear for RetrievedUpdates=False', () => { + const cv = createMockClusterVersion([ + createCondition( + 'RetrievedUpdates', + 'False', + 'UpdatesNotRetrieved', + 'Updates not retrieved', + ), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(true); + }); + + it('should appear for Invalid=True', () => { + const cv = createMockClusterVersion([ + createCondition('Invalid', 'True', 'InvalidCluster', 'Cluster invalid'), + ]); + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showStatus).toBe(true); + }); + }); + }); + + describe('Button Text - Table Requirements', () => { + it('should have correct pre-check button text', () => { + const buttonText = getUpdateButtonTranslationKey('pre-check'); + expect(buttonText).toBe('public~Pre-check with AI'); + }); + + it('should have correct status button text', () => { + const buttonText = getUpdateButtonTranslationKey('status'); + expect(buttonText).toBe('public~Update status'); + }); + }); + + describe('Prompt Content - Table Requirements', () => { + describe('Pre-check Prompts', () => { + it('should mention update risks, ClusterVersion conditions, OCPSTRAT-2118, and precheck output when updates available', () => { + const cv = createMockClusterVersion([]); + cv.status!.availableUpdates = [{ version: '4.12.3' }]; + + const prompt = generateUpdatePrompt('pre-check', cv, mockT); + + expect(prompt).toContain('Cluster Upgrade Readiness'); + expect(prompt).toContain('ClusterVersion'); + expect(prompt).toContain('Pre-Check Analysis'); + expect(prompt).toContain('pre-upgrade analysis'); + expect(prompt).toContain('condition_checking_guide'); + expect(prompt).toContain('ALWAYS check the status field'); + }); + + it('should show cluster health verification when no updates available', () => { + const cv = createMockClusterVersion([]); + cv.status!.availableUpdates = []; + + const prompt = generateUpdatePrompt('pre-check', cv, mockT); + + expect(prompt).toContain('cluster health'); + expect(prompt).toContain('ClusterVersion'); + expect(prompt).toContain('condition_checking_guide'); + expect(prompt).toContain('ALWAYS check the status field'); + }); + }); + + describe('Update Status Prompts', () => { + it('should mention CVO progress, operator conditions, completion percentage, and estimated time', () => { + const cv = createMockClusterVersion([{ type: 'Progressing', status: 'True' }]); + + const prompt = generateUpdatePrompt('status', cv, mockT); + + expect(prompt).toContain('progress'); + expect(prompt).toContain('ClusterOperator'); + expect(prompt).toContain('completion'); + expect(prompt).toContain('Estimated completion'); + expect(prompt).toContain('Current progress'); + }); + }); + + describe('Troubleshoot Prompts', () => { + it('should mention ClusterOperator analysis and failure detection', () => { + const cv = createMockClusterVersion([{ type: 'Failing', status: 'True' }]); + + const prompt = generateUpdatePrompt('status', cv, mockT); + + expect(prompt).toContain('ClusterOperator Failure Analysis'); + expect(prompt).toContain('Failed ClusterOperators'); + expect(prompt).toContain('Degraded=True'); + expect(prompt).toContain('Available=False'); + }); + }); + }); + + describe('Operator Issues Detection', () => { + it('should detect degraded operators', () => { + const operators = [ + createMockClusterOperator('healthy-operator', [ + createCondition('Degraded', 'False', 'OperatorHealthy', 'Operator healthy'), + createCondition('Available', 'True', 'OperatorAvailable', 'Operator available'), + ]), + createMockClusterOperator('degraded-operator', [ + createCondition('Degraded', 'True', 'OperatorDegraded', 'Operator degraded'), + ]), + ]; + + expect(hasOperatorIssues(operators)).toBe(true); + }); + + it('should detect unavailable operators', () => { + const operators = [ + createMockClusterOperator('healthy-operator', [ + createCondition('Available', 'True', 'OperatorAvailable', 'Operator available'), + ]), + createMockClusterOperator('unavailable-operator', [ + createCondition('Available', 'False', 'OperatorUnavailable', 'Operator unavailable'), + ]), + ]; + + expect(hasOperatorIssues(operators)).toBe(true); + }); + + it('should return false for healthy operators', () => { + const operators = [ + createMockClusterOperator('healthy-operator-1', [ + createCondition('Degraded', 'False', 'OperatorHealthy', 'Operator healthy'), + createCondition('Available', 'True', 'OperatorAvailable', 'Operator available'), + ]), + createMockClusterOperator('healthy-operator-2', [ + createCondition('Available', 'True', 'OperatorAvailable', 'Operator available'), + ]), + ]; + + expect(hasOperatorIssues(operators)).toBe(false); + }); + + it('should handle empty operators array', () => { + expect(hasOperatorIssues([])).toBe(false); + expect(hasOperatorIssues(undefined)).toBe(false); + }); + }); + + describe('Workflow Phase Determination', () => { + it('should prioritize cluster-level failures over operator issues', () => { + const cv = createMockClusterVersion([{ type: 'Failing', status: 'True' }]); + + const degradedOperators = [ + createMockClusterOperator('degraded-operator', [{ type: 'Degraded', status: 'True' }]), + ]; + + const phase = determineWorkflowPhase(cv, degradedOperators); + expect(phase).toBe('status'); + }); + + it('should detect operator issues when cluster level is healthy', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + + const degradedOperators = [ + createMockClusterOperator('degraded-operator', [ + createCondition('Degraded', 'True', 'OperatorDegraded', 'Operator degraded'), + ]), + ]; + + const phase = determineWorkflowPhase(cv, degradedOperators); + expect(phase).toBe('status'); + }); + + it('should return pre-check when everything is healthy', () => { + const cv = createMockClusterVersion([ + createCondition('Progressing', 'False', 'NotProgressing', 'Not progressing'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + createCondition('Available', 'True', 'Available', 'Available'), + ]); + + const healthyOperators = [ + createMockClusterOperator('healthy-operator', [ + createCondition('Degraded', 'False', 'OperatorHealthy', 'Operator healthy'), + createCondition('Available', 'True', 'OperatorAvailable', 'Operator available'), + ]), + ]; + + const phase = determineWorkflowPhase(cv, healthyOperators); + expect(phase).toBe('pre-check'); + }); + }); + + describe('Edge Cases and Scenarios', () => { + it('should handle missing conditions gracefully', () => { + const cv: ClusterVersionKind = { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterVersion', + metadata: { + name: 'version', + resourceVersion: '12345', + uid: 'test-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: { + channel: 'stable-4.12', + clusterID: 'test-cluster-id', + }, + status: { + desired: { + version: '4.12.1', + image: 'registry.redhat.io/openshift4/ose:4.12.1', + url: 'https://example.com', + } as Release, + history: [ + { + version: '4.12.1', + state: 'Completed', + startedTime: '2024-01-01T00:00:00Z', + completionTime: '2024-01-01T01:00:00Z', + image: 'registry.redhat.io/openshift4/ose:4.12.1', + verified: false, + } as UpdateHistory, + ], + observedGeneration: 1, + versionHash: 'test-hash', + }, // No conditions array + }; + + const phase = determineWorkflowPhase(cv, []); + expect(phase).toBe('pre-check'); // Default to pre-check when no conditions + }); + + it('should handle operator without conditions', () => { + const operatorWithoutConditions: ClusterOperator = { + apiVersion: 'config.openshift.io/v1', + kind: 'ClusterOperator', + metadata: { + name: 'test', + resourceVersion: '12345', + uid: 'test-uid', + generation: 1, + creationTimestamp: '2024-01-01T00:00:00Z', + }, + spec: {}, + status: { + versions: [], + relatedObjects: [], + }, // No conditions array + }; + + expect(hasOperatorIssues([operatorWithoutConditions])).toBe(false); + }); + + it('should handle multiple failure conditions correctly', () => { + const cv = createMockClusterVersion([ + createCondition('Failing', 'True', 'UpdateFailed', 'Update failed'), + createCondition('ReleaseAccepted', 'False', 'ReleaseRejected', 'Release not accepted'), + createCondition('Invalid', 'True', 'InvalidCluster', 'Cluster invalid'), + ]); + + const phase = determineWorkflowPhase(cv, []); + expect(phase).toBe('status'); + }); + }); + + describe('Specific Table Scenarios', () => { + describe('Pre-check Scenarios', () => { + it('should show pre-check when no updates available and cluster healthy', () => { + const cv = createMockClusterVersion([ + createCondition('Available', 'True', 'Available', 'Available'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + cv.status!.availableUpdates = []; + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showPreCheck).toBe(true); + }); + + it('should show pre-check when updates available but no version selected yet', () => { + const cv = createMockClusterVersion([ + createCondition('Available', 'True', 'Available', 'Available'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + cv.status!.availableUpdates = [ + { + version: '4.12.3', + image: 'registry.redhat.io/openshift4/ose:4.12.3', + url: 'https://example.com', + } as AvailableUpdate, + ]; + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showPreCheck).toBe(true); + }); + + it('should show pre-check when updates available and specific version selected', () => { + const cv = createMockClusterVersion([ + createCondition('Available', 'True', 'Available', 'Available'), + createCondition('Failing', 'False', 'NotFailing', 'Not failing'), + ]); + cv.status!.availableUpdates = [ + { + version: '4.12.3', + image: 'registry.redhat.io/openshift4/ose:4.12.3', + url: 'https://example.com', + } as AvailableUpdate, + ]; + cv.spec!.desiredUpdate = { + version: '4.12.3', + image: 'registry.redhat.io/openshift4/ose:4.12.3', + url: 'https://example.com', + } as Release; + + const buttons = determineWorkflowButtons(cv, []); + expect(buttons.showPreCheck).toBe(true); + }); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-utils.spec.ts b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-utils.spec.ts new file mode 100644 index 00000000000..8316e97dbfa --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/__tests__/workflow-utils.spec.ts @@ -0,0 +1,101 @@ +import type { ClusterVersionKind, ClusterVersionCondition } from '@console/internal/module/k8s'; +import { determineWorkflowPhase } from '../workflow-utils'; + +describe('determineWorkflowPhase', () => { + const createMockClusterVersion = ( + conditions: ClusterVersionCondition[] = [], + ): ClusterVersionKind => + ({ + status: { conditions }, + } as ClusterVersionKind); + + describe('status phase detection (includes failure conditions)', () => { + it('should return status when Failing condition is True', () => { + const cv = createMockClusterVersion([{ type: 'Failing', status: 'True' }]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + + it('should return status when ReleaseAccepted is False with message', () => { + const cv = createMockClusterVersion([ + { type: 'ReleaseAccepted', status: 'False', message: 'Error occurred' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + + it('should return status when RetrievedUpdates is False with message', () => { + const cv = createMockClusterVersion([ + { type: 'RetrievedUpdates', status: 'False', message: 'Error occurred' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + + it('should return status when Invalid is True', () => { + const cv = createMockClusterVersion([{ type: 'Invalid', status: 'True' }]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + }); + + describe('status phase detection', () => { + it('should return status when Progressing is True and no failure conditions', () => { + const cv = createMockClusterVersion([ + { type: 'Progressing', status: 'True' }, + { type: 'ReleaseAccepted', status: 'True' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + }); + + describe('pre-check phase detection', () => { + it('should return pre-check when cluster is healthy (no failure conditions, not progressing)', () => { + const cv = createMockClusterVersion([ + { type: 'Available', status: 'True' }, + { type: 'Progressing', status: 'False' }, + { type: 'ReleaseAccepted', status: 'True' }, + { type: 'Failing', status: 'False' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('pre-check'); + }); + }); + + describe('condition priority (all return status phase)', () => { + it('should return status for multiple problematic conditions (Failing + ReleaseAccepted)', () => { + const cv = createMockClusterVersion([ + { type: 'Failing', status: 'True' }, + { type: 'ReleaseAccepted', status: 'False', message: 'Error' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + + it('should return status for mixed conditions (ReleaseAccepted + Progressing)', () => { + const cv = createMockClusterVersion([ + { type: 'ReleaseAccepted', status: 'False', message: 'Error' }, + { type: 'Progressing', status: 'True' }, + ]); + + const phase = determineWorkflowPhase(cv); + + expect(phase).toBe('status'); + }); + }); +}); diff --git a/frontend/packages/console-shared/src/components/cluster-updates/cluster-state-detector.ts b/frontend/packages/console-shared/src/components/cluster-updates/cluster-state-detector.ts new file mode 100644 index 00000000000..740bcacfa6c --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/cluster-state-detector.ts @@ -0,0 +1,278 @@ +import type { ClusterVersionKind, ClusterOperator } from '@console/internal/module/k8s'; +import { + isClusterFailing, + isClusterProgressing, + isClusterInvalid, + isClusterAvailable, + isClusterUpgradeable, + hasRecommendedUpdates, + hasConditionalUpdates, + hasUpdateRetrievalFailure, + hasReleaseAcceptanceFailure, + hasAnyOperatorIssues, + hasUnknownConditions, +} from './predicates'; + +/** + * Comprehensive cluster state enumeration + * Maps to the different OLS prompt scenarios + */ +export enum ClusterState { + /** Scenario 1: Cluster ready to update with recommended updates available */ + READY_WITH_UPDATES = 'ready-with-updates', + + /** Scenario 2: Cluster ready to update with conditional updates (have concerns/risks) */ + READY_WITH_CONDITIONAL_UPDATES = 'ready-with-conditional-updates', + + /** Scenario 3: Update in progress, monitoring progress */ + UPDATE_IN_PROGRESS = 'update-in-progress', + + /** Scenario 4: Cluster failing, needs troubleshooting */ + FAILING = 'failing', + + /** Scenario 5: Update completed successfully, cluster up-to-date */ + UP_TO_DATE = 'up-to-date', + + /** Additional states for comprehensive coverage */ + NOT_UPGRADEABLE = 'not-upgradeable', + UPDATE_SERVICE_FAILURE = 'update-service-failure', + OPERATOR_ISSUES = 'operator-issues', + INVALID_CONFIGURATION = 'invalid-configuration', + UNKNOWN_STATE = 'unknown-state', +} + +/** + * Cluster state detection result with detailed context + */ +export interface ClusterStateInfo { + /** Primary cluster state */ + state: ClusterState; + + /** Human-readable state description */ + description: string; + + /** Detailed conditions for this state */ + conditions: { + failing: boolean; + progressing: boolean; + invalid: boolean; + available: boolean; + upgradeable: boolean; + hasRecommendedUpdates: boolean; + hasConditionalUpdates: boolean; + hasUpdateServiceFailure: boolean; + hasOperatorIssues: boolean; + hasUnknownConditions: boolean; + }; + + /** Recommended OLS workflow phase for this state */ + recommendedWorkflow: 'status' | 'pre-check'; +} + +/** + * Detect the current cluster state based on ClusterVersion and ClusterOperator conditions + * + * This implements the comprehensive cluster state matrix required for OLS prompt selection: + * + * 1. Ready to update with recommended updates: + * - Failing=False, Progressing=False, availableUpdates present + * - Prompt: Check readiness for available updates + * + * 2. Ready to update with conditional updates: + * - Failing=False, Progressing=False, conditionalUpdates with risks + * - Prompt: Check readiness and assess conditional update risks + * + * 3. Update in progress: + * - Failing=False, Progressing=True + * - Prompt: Monitor update progress (operators, alerts, etc.) + * + * 4. Cluster failing: + * - Failing=True (or operator issues, or update service failures) + * - Prompt: Troubleshoot failure causes + * + * 5. Up-to-date: + * - Failing=False, Progressing=False, no updates available, version matches desired + * - Prompt: Provide update completion summary and cluster health status + * + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns Comprehensive cluster state information + */ +export const detectClusterState = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): ClusterStateInfo => { + // Gather all condition predicates + const conditions = { + failing: isClusterFailing(cv), + progressing: isClusterProgressing(cv), + invalid: isClusterInvalid(cv), + available: isClusterAvailable(cv), + upgradeable: isClusterUpgradeable(cv), + hasRecommendedUpdates: hasRecommendedUpdates(cv), + hasConditionalUpdates: hasConditionalUpdates(cv), + hasUpdateServiceFailure: hasUpdateRetrievalFailure(cv) || hasReleaseAcceptanceFailure(cv), + hasOperatorIssues: hasAnyOperatorIssues(operators), + hasUnknownConditions: hasUnknownConditions(cv), + }; + + // Priority 1: Invalid configuration + if (conditions.invalid) { + return { + state: ClusterState.INVALID_CONFIGURATION, + description: 'Cluster has invalid configuration', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Priority 2: Operator issues + if (conditions.hasOperatorIssues && !conditions.failing) { + return { + state: ClusterState.OPERATOR_ISSUES, + description: 'Cluster operators have issues (degraded or unavailable)', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Priority 3: Cluster failures (Scenario 4) + if (conditions.failing) { + return { + state: ClusterState.FAILING, + description: 'Cluster has failures that need troubleshooting', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Priority 4: Update in progress (Scenario 3) + if (conditions.progressing) { + return { + state: ClusterState.UPDATE_IN_PROGRESS, + description: 'Cluster update is in progress', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Priority 5: Update service failures + if (conditions.hasUpdateServiceFailure) { + return { + state: ClusterState.UPDATE_SERVICE_FAILURE, + description: 'Cluster cannot retrieve or verify updates', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Priority 6: Not upgradeable + if (!conditions.upgradeable) { + return { + state: ClusterState.NOT_UPGRADEABLE, + description: 'Cluster is marked as not upgradeable', + conditions, + recommendedWorkflow: 'pre-check', + }; + } + + // Priority 7: Ready with conditional updates (Scenario 2) + if (conditions.hasConditionalUpdates && !conditions.hasRecommendedUpdates) { + return { + state: ClusterState.READY_WITH_CONDITIONAL_UPDATES, + description: 'Cluster has conditional updates with risks/concerns', + conditions, + recommendedWorkflow: 'pre-check', + }; + } + + // Priority 8: Ready with recommended updates (Scenario 1) + if (conditions.hasRecommendedUpdates) { + return { + state: ClusterState.READY_WITH_UPDATES, + description: 'Cluster is ready to update with available updates', + conditions, + recommendedWorkflow: 'pre-check', + }; + } + + // Priority 9: Unknown conditions detected + if (conditions.hasUnknownConditions) { + return { + state: ClusterState.UNKNOWN_STATE, + description: 'Cluster has unknown condition states', + conditions, + recommendedWorkflow: 'status', + }; + } + + // Default: Up-to-date (Scenario 5) + return { + state: ClusterState.UP_TO_DATE, + description: 'Cluster is up-to-date with no available updates', + conditions, + recommendedWorkflow: 'pre-check', + }; +}; + +/** + * Check if cluster is in a healthy state + * Healthy means: not failing, not invalid, no operator issues, no unknown conditions + * + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns true if cluster is healthy + */ +export const isClusterHealthy = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): boolean => { + const stateInfo = detectClusterState(cv, operators); + return ( + !stateInfo.conditions.failing && + !stateInfo.conditions.invalid && + !stateInfo.conditions.hasOperatorIssues && + !stateInfo.conditions.hasUpdateServiceFailure && + !stateInfo.conditions.hasUnknownConditions + ); +}; + +/** + * Determine if cluster should show the pre-check workflow + * Pre-check is shown when cluster is healthy and not progressing + * + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns true if pre-check workflow should be shown + */ +export const shouldShowPreCheck = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): boolean => { + const stateInfo = detectClusterState(cv, operators); + return ( + stateInfo.recommendedWorkflow === 'pre-check' && + !stateInfo.conditions.progressing && + isClusterHealthy(cv, operators) + ); +}; + +/** + * Determine if cluster should show the status workflow + * Status is shown when cluster is progressing or has issues + * + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns true if status workflow should be shown + */ +export const shouldShowStatus = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): boolean => { + const stateInfo = detectClusterState(cv, operators); + return ( + stateInfo.recommendedWorkflow === 'status' || + stateInfo.conditions.progressing || + !isClusterHealthy(cv, operators) + ); +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/cluster-version-helpers.ts b/frontend/packages/console-shared/src/components/cluster-updates/cluster-version-helpers.ts new file mode 100644 index 00000000000..daf4aab3e59 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/cluster-version-helpers.ts @@ -0,0 +1,18 @@ +import type { ClusterVersionKind } from '@console/internal/module/k8s'; + +/** + * Individual helper functions for cluster version operations + * These avoid factory patterns which can cause re-render issues in React + */ + +/** + * Extract current version from cluster version history + */ +export const getCurrentVersion = (cv: ClusterVersionKind): string => + cv.status?.history?.find((h) => h.state === 'Completed')?.version ?? ''; + +/** + * Extract desired version from cluster version spec or status + */ +export const getDesiredVersion = (cv: ClusterVersionKind): string => + (cv.spec?.desiredUpdate?.version || cv.status?.desired?.version) ?? ''; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/constants.ts b/frontend/packages/console-shared/src/components/cluster-updates/constants.ts new file mode 100644 index 00000000000..0d220c5f088 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/constants.ts @@ -0,0 +1,49 @@ +/** + * Constants for cluster update workflows + */ + +/** + * ClusterVersion condition types + * See: https://docs.openshift.com/container-platform/latest/rest_api/config_apis/clusterversion-config-openshift-io-v1.html + */ +export const CLUSTER_VERSION_CONDITION_AVAILABLE = 'Available'; +export const CLUSTER_VERSION_CONDITION_FAILING = 'Failing'; +export const CLUSTER_VERSION_CONDITION_PROGRESSING = 'Progressing'; +export const CLUSTER_VERSION_CONDITION_RETRIEVED_UPDATES = 'RetrievedUpdates'; +export const CLUSTER_VERSION_CONDITION_RELEASE_ACCEPTED = 'ReleaseAccepted'; +export const CLUSTER_VERSION_CONDITION_INVALID = 'Invalid'; +export const CLUSTER_VERSION_CONDITION_UPGRADEABLE = 'Upgradeable'; + +/** + * ClusterOperator condition types + */ +export const CLUSTER_OPERATOR_CONDITION_AVAILABLE = 'Available'; +export const CLUSTER_OPERATOR_CONDITION_DEGRADED = 'Degraded'; +export const CLUSTER_OPERATOR_CONDITION_PROGRESSING = 'Progressing'; +export const CLUSTER_OPERATOR_CONDITION_UPGRADEABLE = 'Upgradeable'; + +/** + * K8s resource condition status values + */ +export const CONDITION_STATUS_TRUE = 'True'; +export const CONDITION_STATUS_FALSE = 'False'; +export const CONDITION_STATUS_UNKNOWN = 'Unknown'; + +/** + * Feature flags for cluster update workflows + */ +export const FEATURE_FLAG_LIGHTSPEED_PLUGIN = 'LIGHTSPEED_PLUGIN'; + +/** + * OpenShift Lightspeed extension identifiers + * See: https://github.com/openshift/lightspeed-console/tree/main#example + */ +export const OLS_EXTENSION_TYPE = 'console.action/provider'; +export const OLS_EXTENSION_CONTEXT_ID = 'ols-open-handler'; + +/** + * OLS integration behavior options + */ +export const OLS_SUBMIT_IMMEDIATELY = true; +export const OLS_HIDE_PROMPT = true; +export const OLS_NO_ATTACHMENTS: never[] = []; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/explain-button.tsx b/frontend/packages/console-shared/src/components/cluster-updates/explain-button.tsx new file mode 100644 index 00000000000..5522826c750 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/explain-button.tsx @@ -0,0 +1,197 @@ +import type { FC } from 'react'; +import { useCallback } from 'react'; +import { Button } from '@patternfly/react-core'; +import { RhUiAiInfoIcon } from '@patternfly/react-icons'; +import { useTranslation } from 'react-i18next'; +import { useResolvedExtensions } from '@console/dynamic-plugin-sdk'; +import type { Alert } from '@console/dynamic-plugin-sdk'; +import type { Extension } from '@console/dynamic-plugin-sdk/src/types'; +import type { ClusterVersionKind, ClusterOperator } from '@console/internal/module/k8s'; +import { useFlag } from '@console/shared/src/hooks/useFlag'; +import { useTelemetry } from '@console/shared/src/hooks/useTelemetry'; +import { + FEATURE_FLAG_LIGHTSPEED_PLUGIN, + OLS_EXTENSION_TYPE, + OLS_EXTENSION_CONTEXT_ID, + OLS_SUBMIT_IMMEDIATELY, + OLS_HIDE_PROMPT, + OLS_NO_ATTACHMENTS, +} from './constants'; +import type { UpdateWorkflowPhase, MachineConfigPool } from './types'; +import { generateUpdatePrompt, getUpdateButtonText } from './workflow-utils'; + +/** + * OLS Attachment type based on lightspeed-console plugin API + * See https://github.com/openshift/lightspeed-console/blob/701992fe94c7f8cb97cedddc642788c369e7af7e/src/types.ts#L14-L24 + */ +type OLSAttachment = { + kind: string; + name: string; + contentType: string; + content: string; +}; + +type OpenOLSCallback = ( + prompt: string, + attachments: OLSAttachment[], // MCP tools fetch real-time cluster data, so we pass empty array + enableHistory: boolean, + enableFeedback: boolean, +) => void; + +type UseOpenOLS = () => OpenOLSCallback | null; + +type OLSButtonInnerProps = { + openOLS: OpenOLSCallback | null; +}; + +interface UpdateWorkflowOLSButtonProps { + phase: UpdateWorkflowPhase; + cv: ClusterVersionKind; + clusterOperators?: ClusterOperator[]; + machineConfigPools?: MachineConfigPool[]; + alerts?: Alert[]; + targetVersion?: string; // Optional target version for specific version analysis + className?: string; + onClick?: () => void; + variant?: + | 'primary' + | 'secondary' + | 'tertiary' + | 'danger' + | 'warning' + | 'link' + | 'plain' + | 'control'; + size?: 'sm' | 'lg'; + 'data-test'?: string; +} + +// Internal component that only renders when all conditions are met +const OLSButtonInner: FC = ({ + phase, + cv, + clusterOperators, + machineConfigPools, + alerts, + targetVersion, + className, + onClick, + variant = 'link', + size = 'sm', + 'data-test': dataTest, + openOLS, +}) => { + // CRITICAL: Call ALL hooks before any conditional returns + const { t } = useTranslation('console-shared'); + const fireTelemetryEvent = useTelemetry(); + + const handleClick = useCallback(() => { + if (!openOLS) { + return; + } + + // Track usage by workflow phase + fireTelemetryEvent('OLS Update Workflow Button Clicked', { + source: 'cluster-settings', + workflowPhase: phase, + clusterVersion: cv.status?.desired?.version || 'unknown', + updateChannel: cv.spec?.channel, + clusterId: cv.spec?.clusterID, + }); + + // Call the optional onClick callback + onClick?.(); + + // Generate prompt - MCP tools will fetch real-time cluster data + // Pass machineConfigPools and alerts when available for enhanced context + const prompt = generateUpdatePrompt( + phase, + cv, + t, + clusterOperators, + machineConfigPools, + alerts, + targetVersion, + ); + + // Open OLS with prompt - MCP uses tools to fetch live cluster data + openOLS(prompt, OLS_NO_ATTACHMENTS, OLS_SUBMIT_IMMEDIATELY, OLS_HIDE_PROMPT); + }, [ + openOLS, + fireTelemetryEvent, + onClick, + phase, + cv, + t, + clusterOperators, + machineConfigPools, + alerts, + targetVersion, + ]); + + // Get button text for this phase from workflow configuration + const buttonContent = getUpdateButtonText(phase, t); + + // Early return AFTER all hooks have been called + if (!openOLS) { + return null; + } + + return ( + + ); +}; + +type OpenOLSHandlerExtension = Extension< + 'console.action/provider', + { + contextId: string; + provider: UseOpenOLS; + } +>; + +// Component that calls the OLS hook - separated to ensure hooks are called consistently +const OLSButtonWithHook: FC = ({ + useOpenOLS, + ...props +}) => { + // Call the hook unconditionally - this component only renders when hook is available + const openOLS = useOpenOLS(); + + // Pass to the inner button component + return ; +}; + +export const UpdateWorkflowOLSButton: FC = (props) => { + // CRITICAL: All hooks must be called unconditionally at the top level + const isOLSAvailable = useFlag(FEATURE_FLAG_LIGHTSPEED_PLUGIN); + + // Find the OLS extension using useResolvedExtensions - always call hooks at top level + // Type guard inlined since it's only used here + // See https://github.com/openshift/lightspeed-console/tree/main#example + const [extensions, resolved] = useResolvedExtensions( + (e: Extension): e is OpenOLSHandlerExtension => + e.type === OLS_EXTENSION_TYPE && e.properties?.contextId === OLS_EXTENSION_CONTEXT_ID, + ); + + // Early return after calling all hooks + if (!isOLSAvailable || !resolved || !extensions[0]?.properties?.provider) { + return null; + } + + // Type assertion is safe here because the extension system guarantees the provider is resolved + const useOpenOLS = extensions[0].properties.provider as UseOpenOLS; + + // Render the component that will call the hook + return ; +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/predicates.ts b/frontend/packages/console-shared/src/components/cluster-updates/predicates.ts new file mode 100644 index 00000000000..b2e728abf8b --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/predicates.ts @@ -0,0 +1,265 @@ +import type { ClusterVersionKind, ClusterOperator } from '@console/internal/module/k8s'; +import { + CLUSTER_VERSION_CONDITION_AVAILABLE, + CLUSTER_VERSION_CONDITION_FAILING, + CLUSTER_VERSION_CONDITION_INVALID, + CLUSTER_VERSION_CONDITION_PROGRESSING, + CLUSTER_VERSION_CONDITION_RETRIEVED_UPDATES, + CLUSTER_VERSION_CONDITION_RELEASE_ACCEPTED, + CLUSTER_VERSION_CONDITION_UPGRADEABLE, + CLUSTER_OPERATOR_CONDITION_DEGRADED, + CLUSTER_OPERATOR_CONDITION_AVAILABLE, + CLUSTER_OPERATOR_CONDITION_UPGRADEABLE, + CONDITION_STATUS_TRUE, + CONDITION_STATUS_FALSE, + CONDITION_STATUS_UNKNOWN, +} from './constants'; + +/** + * Testable condition checking predicates for cluster updates + * These pure functions encapsulate condition logic for better testability and reuse + */ + +/** + * Check if cluster is currently failing + * @param cv - ClusterVersion resource + * @returns true if Failing condition status is True + */ +export const isClusterFailing = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_VERSION_CONDITION_FAILING && c.status === CONDITION_STATUS_TRUE, + ); +}; + +/** + * Check if cluster configuration is invalid + * @param cv - ClusterVersion resource + * @returns true if Invalid condition status is True + */ +export const isClusterInvalid = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_VERSION_CONDITION_INVALID && c.status === CONDITION_STATUS_TRUE, + ); +}; + +/** + * Check if cluster is currently progressing through an update + * @param cv - ClusterVersion resource + * @returns true if Progressing condition status is True + */ +export const isClusterProgressing = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_VERSION_CONDITION_PROGRESSING && c.status === CONDITION_STATUS_TRUE, + ); +}; + +/** + * Check if cluster failed to retrieve updates + * @param cv - ClusterVersion resource + * @returns true if RetrievedUpdates condition status is False + */ +export const hasUpdateRetrievalFailure = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => + c.type === CLUSTER_VERSION_CONDITION_RETRIEVED_UPDATES && c.status === CONDITION_STATUS_FALSE, + ); +}; + +/** + * Check if release was not accepted (signature verification failed) + * @param cv - ClusterVersion resource + * @returns true if ReleaseAccepted condition status is False + */ +export const hasReleaseAcceptanceFailure = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => + c.type === CLUSTER_VERSION_CONDITION_RELEASE_ACCEPTED && c.status === CONDITION_STATUS_FALSE, + ); +}; + +/** + * Check if cluster has recommended updates available + * @param cv - ClusterVersion resource + * @returns true if availableUpdates array has entries + */ +export const hasRecommendedUpdates = (cv: ClusterVersionKind): boolean => { + return (cv.status?.availableUpdates?.length || 0) > 0; +}; + +/** + * Check if cluster has conditional updates (with risks/warnings) + * @param cv - ClusterVersion resource + * @returns true if conditionalUpdates array has entries + */ +export const hasConditionalUpdates = (cv: ClusterVersionKind): boolean => { + return (cv.status?.conditionalUpdates?.length || 0) > 0; +}; + +/** + * Check if a specific operator is degraded + * @param operator - ClusterOperator resource + * @returns true if operator has Degraded=True condition + * @public + */ +export const isOperatorDegraded = (operator: ClusterOperator): boolean => { + const conditions = operator?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_OPERATOR_CONDITION_DEGRADED && c.status === CONDITION_STATUS_TRUE, + ); +}; + +/** + * Check if a specific operator is unavailable + * @param operator - ClusterOperator resource + * @returns true if operator has Available=False condition + * @public + */ +export const isOperatorUnavailable = (operator: ClusterOperator): boolean => { + const conditions = operator?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_OPERATOR_CONDITION_AVAILABLE && c.status === CONDITION_STATUS_FALSE, + ); +}; + +/** + * Check if a specific operator has issues (degraded or unavailable) + * @param operator - ClusterOperator resource + * @returns true if operator is degraded or unavailable + */ +export const hasOperatorIssues = (operator: ClusterOperator): boolean => { + return isOperatorDegraded(operator) || isOperatorUnavailable(operator); +}; + +/** + * Check if any operators in the cluster have issues + * @param operators - Array of ClusterOperator resources + * @returns true if any operator is degraded or unavailable + */ +export const hasAnyOperatorIssues = (operators?: ClusterOperator[]): boolean => { + if (!operators || operators.length === 0) { + return false; + } + return operators.some(hasOperatorIssues); +}; + +/** + * Get all operators with issues + * @param operators - Array of ClusterOperator resources + * @returns Array of operators that are degraded or unavailable + * @public + */ +export const getOperatorsWithIssues = (operators?: ClusterOperator[]): ClusterOperator[] => { + if (!operators || operators.length === 0) { + return []; + } + return operators.filter(hasOperatorIssues); +}; + +/** + * Check if cluster has any blocking conditions preventing updates + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns true if cluster has failures, invalid config, or operator issues + * @public + */ +export const hasBlockingConditions = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): boolean => { + return ( + isClusterFailing(cv) || + isClusterInvalid(cv) || + hasUpdateRetrievalFailure(cv) || + hasReleaseAcceptanceFailure(cv) || + hasAnyOperatorIssues(operators) + ); +}; + +/** + * Check if cluster is available + * @param cv - ClusterVersion resource + * @returns true if Available condition status is True + */ +export const isClusterAvailable = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some( + (c) => c.type === CLUSTER_VERSION_CONDITION_AVAILABLE && c.status === CONDITION_STATUS_TRUE, + ); +}; + +/** + * Check if cluster is upgradeable + * @param cv - ClusterVersion resource + * @returns true if Upgradeable condition status is True + */ +export const isClusterUpgradeable = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + const upgradeableCondition = conditions.find( + (c) => c.type === CLUSTER_VERSION_CONDITION_UPGRADEABLE, + ); + // If Upgradeable condition is not present, assume cluster is upgradeable (default behavior) + // If present and status is False, cluster is not upgradeable + return upgradeableCondition ? upgradeableCondition.status === CONDITION_STATUS_TRUE : true; +}; + +/** + * Check if cluster has any updates available (recommended or conditional) + * @param cv - ClusterVersion resource + * @returns true if either availableUpdates or conditionalUpdates are present + * @public + */ +export const hasAnyUpdates = (cv: ClusterVersionKind): boolean => { + return hasRecommendedUpdates(cv) || hasConditionalUpdates(cv); +}; + +/** + * Check if cluster is ready to update + * Cluster is ready when: not failing, not progressing, and upgradeable + * @param cv - ClusterVersion resource + * @param operators - Optional array of ClusterOperator resources + * @returns true if cluster is ready to start an update + * @public + */ +export const isClusterReadyToUpdate = ( + cv: ClusterVersionKind, + operators?: ClusterOperator[], +): boolean => { + return ( + !isClusterFailing(cv) && + !isClusterProgressing(cv) && + !isClusterInvalid(cv) && + isClusterUpgradeable(cv) && + !hasAnyOperatorIssues(operators) + ); +}; + +/** + * Check if an operator is upgradeable + * @param operator - ClusterOperator resource + * @returns true if operator has Upgradeable=True condition (or condition not present) + * @public + */ +export const isOperatorUpgradeable = (operator: ClusterOperator): boolean => { + const conditions = operator?.status?.conditions || []; + const upgradeableCondition = conditions.find( + (c) => c.type === CLUSTER_OPERATOR_CONDITION_UPGRADEABLE, + ); + // If Upgradeable condition is not present, assume operator is upgradeable + return upgradeableCondition ? upgradeableCondition.status === CONDITION_STATUS_TRUE : true; +}; + +/** + * Check if all conditions have Unknown status + * This can indicate a cluster monitoring or communication issue + * @param cv - ClusterVersion resource + * @returns true if any condition has Unknown status + */ +export const hasUnknownConditions = (cv: ClusterVersionKind): boolean => { + const conditions = cv?.status?.conditions || []; + return conditions.some((c) => c.status === CONDITION_STATUS_UNKNOWN); +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-no-updates.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-no-updates.ts new file mode 100644 index 00000000000..1fe5a967eaa --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-no-updates.ts @@ -0,0 +1,139 @@ +import { getLanguageConstraint } from './shared/language-utils'; + +/** + * Pre-check prompt for clusters with no available updates + * Verifies cluster health when already at latest version + * + * @param currentVersion - Current cluster version + * @returns Formatted prompt for OLS health verification + */ +export const createPreCheckNoUpdatesPrompt = (currentVersion: string) => { + const languageConstraint = getLanguageConstraint(); + + return `# OpenShift Cluster Health Assessment + + +- YOU MUST ALWAYS CALL THE TOOLS TO GET THE INFORMATION. YOU SHOULD NEVER TREAT DATA FROM EXAMPLES AS REAL DATA. +- YOU MUST ALWAYS REFERENCE REAL DATA FROM TOOL CALLS. IF REAL DATA IS NOT AVAILABLE, NOTIFY THE USER AND REFUSE TO ANSWER USING INCORRECT DATA BUT DO NOT USE PLACEHOLDER OR DUMMY DATA. +- Use resources_get to fetch the ClusterVersion resource (apiVersion: "config.openshift.io/v1", kind: "ClusterVersion", name: "version") +- Use resources_list to fetch all ClusterOperator resources (apiVersion: "config.openshift.io/v1", kind: "ClusterOperator") +- Assess ONLY the actual cluster state from tool call data +- Distinguish between system health and user workload issues +- Provide actionable recommendations for administrators +- ONLY OUTPUT the Summary and TL;DR sections +${languageConstraint} + + + +Health assessment for OpenShift cluster running ${currentVersion} with no available updates. You have complete cluster data including ClusterVersion and all ClusterOperator resources for comprehensive health analysis. +Focus on operational health and readiness for future updates. + + + +CRITICAL: Understanding Kubernetes/OpenShift Conditions + +Conditions have TWO important fields you MUST check: +- **type**: The name of the condition (e.g., "Failing", "Available", "Progressing") +- **status**: The state of the condition ("True", "False", or "Unknown") + +**How to Correctly Check Conditions:** +- A condition is TRUE when: type="X" AND status="True" +- A condition is FALSE when: type="X" AND status="False" +- A condition is UNKNOWN when: type="X" AND status="Unknown" + +**Examples:** +- {type: "RetrievedUpdates", status: "True"} means updates were retrieved (healthy) +- {type: "RetrievedUpdates", status: "False"} means update retrieval failed (problem) +- {type: "Failing", status: "False"} means the cluster is NOT failing (healthy) +- {type: "Available", status: "True"} means the cluster IS available (healthy) +**NEVER assume a condition is true just because the type exists - ALWAYS check the status field!** + + + + +1. **Current Version and Update Status Analysis** (Check BOTH type AND status): + - Extract and confirm current version from status.desired.version matches ${currentVersion} + - Verify status.availableUpdates array is empty (confirming no updates available) + - Find condition where type="RetrievedUpdates" AND status="True" (confirms update service is working) + - Analyze why no updates are available (end of channel, latest version, etc.) + +2. **Cluster Capabilities Configuration Assessment**: + - Extract enabled capabilities from status.capabilities.enabledCapabilities + - Extract known capabilities from status.capabilities.knownCapabilities + - Identify disabled capabilities (known but not enabled) + - Assess capability configuration health and consistency + - Check spec.capabilities.baselineCapabilitySet and additionalEnabledCapabilities + +3. **Update Service and Channel Health**: + - Check spec.upstream (if configured) or note "using default Red Hat update service" + - Verify status.conditions for type="RetrievedUpdates" status and timestamp + - Confirm update service connectivity is working despite no available updates + - Current channel from spec.channel + - Cluster ID for telemetry (spec.clusterID) + - Signature verification status (spec.signatureStores if present, otherwise default stores) + +4. **Cluster Version History Context**: + - Extract initial cluster version from status.history (first entry) + - Identify upgrade path from history entries + - Last completed upgrade and timeframe + - Total cluster age and upgrade frequency + - Historical upgrade success pattern + +5. **System Component Health** (Check BOTH type AND status for each operator): + For each ClusterOperator, check conditions: + - **Available**: If type="Available" AND status="False" → Requires immediate intervention + - **Degraded**: If type="Degraded" AND status="True" → Degraded state, lower quality of service + - **Progressing**: If type="Progressing" AND status="True" with errors → Component stuck + - **Upgradeable**: If type="Upgradeable" AND status="False" → Blocks minor upgrades + - Verify core platform operators (console, authentication, ingress, etc.) are healthy + - Check ClusterVersion status.conditions for overall cluster health + - Report specific operator names and their condition messages for problematic conditions only + - IMPORTANT: Available=True, Degraded=False, Upgradeable=True are healthy states + +6. **Future Update Readiness Assessment** (Check BOTH type AND status): + - Find condition where type="Upgradeable" (OPTIONAL - may not exist) + * If found AND status="False": This IS an upgrade blocker - report reason + * If status="True", missing, or status="Unknown": Future upgrades are allowed + - Find condition where type="Failing" + * If found AND status="True": Cluster issues that must be resolved + * If status="False" or missing: No failing condition (healthy) + - Review spec.overrides for any unmanaged components that might block future upgrades + - Identify maintenance items to address proactively + - User workload PDB analysis for potential upgrade blockers + +7. **Operational Health and Recommendations**: + - Identify issues that affect user applications + - Focus on problems that cluster administrators can/should address + - Provide specific, actionable guidance for maintaining cluster health + - Distinguish from normal system maintenance activities + - Avoid recommendations for normal system behavior + + + + +## Summary +**Overall Health Status** +[Assessment based on actual cluster state data] +**System Component Status** +- **Core Services**: [List core platform operators and their health status] +- **Degraded Operators**: [Any operators with Available=False or Degraded=True] +- **Progressing Operators**: [Operators currently updating or progressing] +- **Infrastructure**: [Overall cluster-level status and configuration] +**Administrator Action Items** +- **Immediate**: [Issues requiring prompt attention] +- **Maintenance**: [Items to address during maintenance windows] +- **Monitoring**: [Things to watch for trends] +**Future Update Readiness** +[Assessment of readiness for next OpenShift updates] + +## TL;DR +- **Overall Status**: [Healthy | Minor issues | Attention needed] +- **System Health**: [Count of healthy vs degraded operators] +- **Core Platform**: [Status of essential operators: console, authentication, ingress, etc.] +- **Degraded Components**: [Count and names of any unhealthy operators] +- **User Impact**: [Any operator issues affecting workloads] +- **Action Items**: [Count of items needing administrator attention] +- **Update Readiness**: [Ready | Operator issues need resolution] +- **Next Review**: [Recommended reassessment timeframe] +`; +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-specific.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-specific.ts new file mode 100644 index 00000000000..567cd37abba --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck-specific.ts @@ -0,0 +1,334 @@ +import { + PROMPT_TIMEOUT_TOTAL_LIMIT, + PROMPT_TIMEOUT_WARNING_THRESHOLD, + PROMPT_TIMEOUT_MAX_EXECUTION, +} from './shared/constants'; +import { getLanguageConstraint } from './shared/language-utils'; + +/** + * Pre-check prompt for a specific target version + * Validates upgrade path and version-specific requirements + * + * @param currentVersion - Current cluster version + * @param targetVersion - Specific target version selected by user + * @returns Formatted prompt for OLS version-specific assessment + */ +export const createPreCheckSpecificVersionPrompt = ( + currentVersion: string, + targetVersion: string, +) => { + const languageConstraint = getLanguageConstraint(); + + return `# OpenShift Cluster Upgrade Pre-Check Analysis + + +${languageConstraint} + +- YOU MUST ALWAYS CALL THE TOOLS TO GET THE INFORMATION. YOU SHOULD NEVER TREAT DATA FROM EXAMPLES AS REAL DATA. +- YOU MUST ALWAYS REFERENCE REAL DATA FROM TOOL CALLS. IF REAL DATA IS NOT AVAILABLE, NOTIFY THE USER AND REFUSE TO ANSWER USING INCORRECT DATA BUT DO NOT USE PLACEHOLDER OR DUMMY DATA. +**CRITICAL: Timeout and Error Handling** +**Timeout Awareness:** +- You have a ${PROMPT_TIMEOUT_TOTAL_LIMIT}-second timeout - manage your time wisely +- Prioritize essential data (ClusterVersion, ClusterOperators) first +- Track execution time and stop making new tool calls after ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds +- Provide analysis with available data rather than timing out trying to fetch everything +**Error Handling Rules:** +1. **Be specific about which tool failed**- don't give generic "cannot retrieve data" messages +2. **Explain what data you're missing**- e.g., "Unable to fetch ClusterVersion resource" vs "Unable to retrieve data" +3. **Try alternative approaches**: + - If resources_list fails for all ClusterOperators, note this specifically + - If nodes_top fails, continue with other analysis - it's optional + - If get_alerts fails, skip it - alerts are optional + - If events_list fails, continue without event data +4. **Provide partial analysis** - If you get ClusterVersion but not operators, analyze what you have +5. **Give actionable troubleshooting**when tools fail: + - Check if OpenShift MCP server is running: 'oc get pods -n openshift-lightspeed' + - Verify cluster connectivity + - Suggest checking MCP server logs for specific errors +6. **NEVER give up completely**- Always provide SOME analysis even with partial data +**Tool Call Priority to Avoid Timeouts:** +**PHASE 1 - ESSENTIAL (Always fetch):** +1. resources_get: ClusterVersion (apiVersion: "config.openshift.io/v1", kind: "ClusterVersion", name: "version") +2. resources_list: ClusterOperator (apiVersion: "config.openshift.io/v1", kind: "ClusterOperator") +**PHASE 2 - IMPORTANT (Fetch if time permits, under 45 seconds total):** +3. resources_list: Node (apiVersion: "v1", kind: "Node") - Quick check for NotReady nodes +4. events_list: Get recent warning/error events from last 30 minutes - High value for diagnostics +**PHASE 3 - OPTIONAL (Only if under ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds total):** +5. resources_list: MachineConfigPool (apiVersion: "machineconfiguration.openshift.io/v1", kind: "MachineConfigPool") +6. nodes_top: Check node CPU/memory usage +7. resources_list: PodDisruptionBudget (apiVersion: "policy/v1", kind: "PodDisruptionBudget") - Filter out openshift-*, kube-* +8. get_alerts: Check for critical/warning alerts +**CRITICAL EFFICIENCY RULES:** +- If approaching ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds of execution time, STOP making new tool calls and provide analysis with data collected +- NEVER let total execution exceed ${PROMPT_TIMEOUT_MAX_EXECUTION} seconds to avoid timeout +- Prioritize breadth over depth: Get ClusterVersion + ClusterOperators fully before diving into logs/events +- Skip optional data if essential data took longer than expected + +- NEVER use placeholder or dummy data - only reference real data from tool calls +- ONLY report issues that are actually present in the data +- ONLY OUTPUT the Summary and TL;DR sections +- Be specific about the source of any issues identified +- CRITICAL: When counting available updates, count ALL array elements in status.availableUpdates +- CRITICAL: Check status.conditionalUpdates for ALL versions from ${currentVersion} to ${targetVersion} (inclusive) +- CRITICAL: Analyze the COMPLETE upgrade path, not just the target version - intermediate versions matter! + + + +This is a pre-upgrade analysis for OpenShift cluster upgrade from ${currentVersion} to ${targetVersion}. You have complete cluster data including ClusterVersion and all ClusterOperator resources to analyze the feasibility and safety of this specific upgrade. + +**CRITICAL UPGRADE PATH ANALYSIS**: You must analyze ALL conditional update risks for every version between ${currentVersion} and ${targetVersion} (inclusive). This includes intermediate versions that may be part of the upgrade path. For example, if upgrading from 4.21.16 to 4.21.22, you must check for risks at 4.21.17, 4.21.18, 4.21.19, 4.21.20, 4.21.21, and 4.21.22. Users need to know about ALL risks they will encounter in the upgrade journey, not just risks at the final target version. + + + +CRITICAL: Understanding Kubernetes/OpenShift Conditions + +Conditions have TWO important fields you MUST check: +- **type**: The name of the condition (e.g., "Failing", "Available", "Progressing") +- **status**: The state of the condition ("True", "False", or "Unknown") + +**How to Correctly Check Conditions:** +- A condition is TRUE when: type="X" AND status="True" +- A condition is FALSE when: type="X" AND status="False" +- A condition is UNKNOWN when: type="X" AND status="Unknown" + +**Examples:** +- {type: "Failing", status: "False"} means the cluster is NOT failing (healthy) +- {type: "Failing", status: "True"} means the cluster IS failing (problem) +- {type: "Upgradeable", status: "False"} means upgrades are blocked (problem) +- {type: "Upgradeable", status: "True"} means upgrades are allowed (healthy) +**NEVER assume a condition is true just because the type exists - ALWAYS check the status field!** + + + + +1. **Target Version Verification** (PRIORITY): + - Look in status.availableUpdates array for ${targetVersion} + - If found, extract its channels, url, and image information + - If NOT found, report "${targetVersion} is not available for upgrade" + +1a. **Conditional Updates Risk Analysis - All Risks Up to Target Version**: + - **CRITICAL**: Analyze ALL conditional updates from ${currentVersion} up to and including ${targetVersion} + - **Version Range**: Parse version numbers to identify which conditional updates fall between current and target + - **Example**: If current=4.21.16, target=4.21.22, analyze risks for: 4.21.17, 4.21.18, 4.21.19, 4.21.20, 4.21.21, 4.21.22 + - **Why This Matters**: Users may need to upgrade through intermediate versions to reach the target, so ALL risks in the path are relevant + - For each conditional update in the version range, analyze the conditions array: + * Look for conditions with type="Recommended" AND status="False" (indicates risks/concerns) + * Extract risk details from the condition message field + * Parse URLs in the message for documentation links + - **Risk Assessment Process** (for each version in range): + * Identify what triggers the risk (e.g., specific cluster configurations, network plugins) + * Determine if the risk applies to THIS cluster based on current configuration + * Assess severity: Does this block the upgrade or just require careful planning? + * Note which version introduces the risk (important for upgrade path planning) + - **User-Friendly Risk Explanation**: + * Translate technical risk messages into plain language + * Example: "Recommended=False: OVN network disruption" → "This update may cause brief network interruptions (2-5 minutes) if your cluster uses OVN-Kubernetes networking. Plan for a maintenance window." + * Show version where risk appears: "Risk at 4.21.18: [description]" + - **Mitigation Recommendations**: + * If risk applies: Suggest mitigation steps (maintenance window, backup procedures, etc.) + * If risk doesn't apply: Clearly state "This risk does not apply to your cluster" + * Provide decision guidance: "Safe to proceed with caution" vs "Address concerns first" + - **Presentation Order**: List risks in version order (lowest to highest) to show the upgrade path chronologically + +2. **Cluster Upgrade Readiness** (Check BOTH type AND status): + - Find condition where type="Upgradeable" (may not exist) + * If found AND status="False": Report the specific reason - this blocks upgrades + * If status="True" or missing: Upgrades are allowed + - Find condition where type="Failing" + * If found AND status="True": Report details - this indicates problems + * If status="False" or missing: No failing condition (healthy) + - Find condition where type="Available" + * If found AND status="False": Report cluster operational issues + * If status="True": Cluster is available (healthy) + +3. **ClusterOperator Health Check** (Check BOTH type AND status): + For each ClusterOperator, check conditions: + - Available: If type="Available" AND status="False" → Operator unavailable (blocker) + - Degraded: If type="Degraded" AND status="True" → Operator degraded (warning) + - Upgradeable: If type="Upgradeable" AND status="False" → Blocks upgrades (blocker) + - Report specific operator names and their issues for problematic conditions only + - Focus on operators that would block upgrades + +4. **Current Cluster Configuration**: + - Extract spec.channel (current update channel) + - Extract spec.clusterID + - Check if spec.upstream is configured (custom Cincinnati server) + - Note status.conditions RetrievedUpdates condition + +5. **User Workload PDB Analysis** (IMPORTANT - Filter System PDBs): + - Query PodDisruptionBudgets in ALL namespaces EXCEPT these OpenShift system namespaces: + * openshift-* (all openshift- prefixed namespaces) + * kube-* (all kube- prefixed namespaces) + * default, openshift + - ONLY flag user workload PDBs where: + * minAvailable >= 1 AND it covers critical user applications + * maxUnavailable = 0 AND it covers critical user applications + - IGNORE all PDBs in OpenShift system namespaces - these are managed by Red Hat + - If no problematic user workload PDBs exist, state "No problematic user workload PDBs found" + +6. **MachineConfigPool Status** (Check BOTH type AND status): + For each MachineConfigPool: + - Check conditions for Degraded: If type="Degraded" AND status="True" → MCP has issues + - Check conditions for Updated: If type="Updated" AND status="False" → MCP not updated + - Check spec.paused=true → MCP manually paused (blocks node updates) + - Check observedGeneration ≠ metadata.generation → Configuration drift + - Focus on master and worker MCPs which are critical for upgrade success + - Report specific MCP names and their issues + +7. **Node Health and Resource Pressure**: + a) **Node Readiness:** + - Check each Node for Ready condition: If type="Ready" AND status="False" → Node not ready (blocker) + - Check for other node conditions: MemoryPressure, DiskPressure, PIDPressure (status="True" is problem) + - Report NotReady nodes with their conditions and reasons + + b) **Resource Utilization (using nodes_top):** + - Check CPU usage: Flag if any node >90% CPU utilization + - Check memory usage: Flag if any node >90% memory utilization + - Explain impact: High resource usage can slow upgrades or cause failures + - Recommend: Consider scaling down workloads before upgrading to ${targetVersion} if resources are constrained + +8. **Cincinnati Update Service Health**: + - Check spec.upstream (if configured) or note "using default Red Hat update service" + - Verify status.conditions for type="RetrievedUpdates" status and timestamp + - Confirm status.availableUpdates or status.conditionalUpdates contains ${targetVersion} + - Cluster ID for telemetry (spec.clusterID) + - Signature verification status (spec.signatureStores if present, otherwise default stores) + +9. **Recent Events Analysis** (using events_list): + - Query recent events from last 30 minutes + - Focus on Warning and Error type events + - Filter for upgrade-related namespaces: openshift-cluster-version, openshift-* + - Look for patterns: repeated errors, failing pods, configuration issues + - **User-Friendly Explanation**: Translate technical events into plain language + - Report only events that are relevant to upgrade readiness for ${targetVersion} + - Group similar events to avoid overwhelming users + +10. **Active Alerts Assessment** (using get_alerts - if available): + - Query Alertmanager for active alerts + - Focus on Critical and Warning severity alerts + - **Upgrade Impact Analysis**: + * Critical alerts → Likely upgrade blockers, must resolve first + * Warning alerts → May cause issues, recommend resolving + * Info alerts → Monitor but don't block + - **User-Friendly Translation**: Explain what each alert means in simple terms + - Provide actionable recommendations for each alert + - If get_alerts tool not available: Skip this check (gracefully handle tool absence) + + + + +## Summary + +Provide a clear assessment based ONLY on real data from tool calls (resources_get and resources_list). Be specific about: +- **Whether ${targetVersion} is available for upgrade** (found in status.availableUpdates or status.conditionalUpdates) +- **ALL conditional update risks in the upgrade path from ${currentVersion} to ${targetVersion}** (analyze status.conditionalUpdates for ALL versions in range) +- **Current cluster upgrade readiness** (check Upgradeable=False, Failing=True, degraded operators) +- **Any problematic USER WORKLOAD PDBs** (not OpenShift system PDBs) +- **Infrastructure issues**that would prevent the upgrade to ${targetVersion} + +**CRITICAL INSTRUCTION**: Parse version numbers from status.conditionalUpdates and identify which versions fall between ${currentVersion} and ${targetVersion}. Report risks for ALL of these versions, not just ${targetVersion}. + +**Target Version Analysis** +- **Availability**: [Whether ${targetVersion} is in availableUpdates or conditionalUpdates] +- **Channels**: [Channels available for ${targetVersion}] +- **Release Information**: [URL and metadata for ${targetVersion} if available] + +**Conditional Updates Risk Analysis - Upgrade Path from ${currentVersion} to ${targetVersion}**: +- **Version Range Analyzed**: [List all versions between ${currentVersion} and ${targetVersion} that have conditional update risks] +- **Total Risks in Upgrade Path**: [Count of all risk conditions across all versions in the range] +- **Risk Details by Version** (in chronological order from lowest to highest version): + + For each version with risks in the upgrade path: + * **Version**: [e.g., 4.21.18] + * **Risk Conditions**: [List conditions with Recommended=False for this version] + - Risk: [Human-readable risk description from condition message] + - Applies to this cluster: [Yes/No with explanation] + - Severity: [Blocker / Requires Planning / Minor Concern] + - Mitigation: [Specific steps to address this risk] + - Documentation: [URL from message if available] + +- **Cumulative Risk Assessment for Upgrade Path**: + * If no risks in path: "No conditional update risks from ${currentVersion} to ${targetVersion}" + * If risks don't apply: "Conditional updates exist but risks do not apply to this cluster configuration" + * If risks apply but manageable: "Upgrade path has manageable risks - schedule maintenance window and review all mitigations" + * If risks are severe: "Review all risks carefully before proceeding - multiple versions in upgrade path have concerns" + * **Planning Guidance**: "You will encounter [X] risk conditions across [Y] versions in the upgrade path from ${currentVersion} to ${targetVersion}" + +**Upgrade Readiness Assessment** + +YOU MUST explicitly state the status field value for each condition you check: +**ClusterVersion Conditions:** +- **Failing Condition**: [type="Failing" found with status="X"] → [Interpretation: if status="False" then NOT failing/healthy, if status="True" then failing/problem] +- **Upgradeable Condition**: [type="Upgradeable" found with status="X" OR not found] → [Interpretation: if status="False" then upgrades blocked, if status="True" or missing then upgrades allowed] +- **Available Condition**: [type="Available" found with status="X"] → [Interpretation: if status="True" then available/healthy, if status="False" then not available/problem] +**ClusterOperator Health:** +- Verify ClusterOperator resources in config.openshift.io/v1 API group +- For each operator, check status.conditions and explicitly state status field values +- Flag operators with: Available status="False" OR Degraded status="True" OR Upgradeable status="False" +- Include their message and reason fields +**Infrastructure Health:** +- **MachineConfigPools**: [Count and status of MCPs - report Degraded, Paused, or out-of-sync pools] +- **Node Status**: [Count NotReady nodes with their reasons] +- **Resource Pressure**: [From nodes_top - report nodes with >90% CPU or memory usage] +- **User Workload PDBs**: [Count of problematic non-OpenShift PDBs that could block node draining] +**Cincinnati Update Service Health**: +- **Service Configuration**: [spec.upstream URL if configured, otherwise "Default Red Hat update service"] +- **Service Status**: [RetrievedUpdates condition status and message] +- **Last Update Check**: [From RetrievedUpdates condition lastTransitionTime] +- **Update Channel**: [Current spec.channel] +- **Cluster ID**: [spec.clusterID for telemetry] +**Recent Events** (Last 30 minutes): +- **Critical Events**: [Count and description of error events] +- **Warning Events**: [Count and description of warning events] +- **User-Friendly Summary**: [Translate technical events into plain language explanation] +- **Example**: "3 ImagePullBackOff events in openshift-authentication - operator unable to download container images" +- If no concerning events: "No recent errors or warnings detected" +**Active Alerts** (if available): +- **Critical Alerts**: [Count and names of firing critical alerts] +- **Warning Alerts**: [Count and names of firing warning alerts] +- **Impact on Upgrade**: [Explain how these alerts affect upgrade readiness to ${targetVersion}] +- **User-Friendly Explanation**: [Translate alert names into actionable recommendations] +- **Example**: "KubePersistentVolumeFillingUp: Storage volume is 85% full - free up space before upgrading" +- If alerts not available: Skip this section + +**Final Assessment**: +If ${targetVersion} is available and no critical issues are found: +- If no conditional risks in upgrade path: Clearly state the cluster appears ready for upgrade to ${targetVersion} +- If conditional risks exist but don't apply: State the upgrade path is clear despite conditional updates existing +- If conditional risks apply: Explain ALL risks across the upgrade path and provide comprehensive mitigation guidance + +**Upgrade Path Summary**: +- List ALL versions with conditional update risks between ${currentVersion} and ${targetVersion} +- For multi-hop upgrades, clarify that risks at intermediate versions will be encountered +- Provide consolidated recommendation considering ALL risks in the path, not just target version + +If ${targetVersion} is not available, recommend the closest available version and analyze risks for that path instead. + +## TL;DR +- **Current Version**: ${currentVersion} +- **Target Version**: ${targetVersion} +- **Target Available**: [Yes in availableUpdates / Yes in conditionalUpdates with risks / No] +- **Upgrade Path**: [${currentVersion} → ${targetVersion}, list intermediate versions if applicable] +- **Conditional Risks in Upgrade Path**: [Total count of risk conditions across ALL versions from ${currentVersion} to ${targetVersion}, or "None"] +- **Versions with Risks**: [List versions in upgrade path that have conditional update risks, e.g., "4.21.18, 4.21.20" or "None"] +- **Risks Apply to Cluster**: [Yes/No - if ANY conditional risks in upgrade path apply to this cluster configuration] +- **Risk Severity**: [If risks exist: Blocker / Requires Planning / Minor Concern / Does Not Apply] +- **Target Channels**: [Channels for ${targetVersion} if available] +- **Current Channel**: [spec.channel from ClusterVersion] +- **Cincinnati Health**: [Update service status, e.g., "Default service healthy (RetrievedUpdates=True)" or "Custom upstream: URL (status)"] +- **Upgrade Blocked**: [Yes if blocked / No if not blocked - ONLY report "Yes" if: Upgradeable condition has status="False" OR Failing condition has status="True" OR operators have Available status="False" or Upgradeable status="False"] +- **Upgrade Blockers**: [if blockers exist with specific reason - MUST include the actual status field value you read, e.g., "Upgradeable condition status=False: reason message" OR "No blockers - all conditions healthy"] +- **Unhealthy ClusterOperators**: [count and names if any] +- **User Workload PDBs**: [count of problematic NON-OpenShift PDBs] +- **Degraded MCPs**: [count and names if any] +- **Node Issues**: [count of NotReady nodes if any, include Ready=False reason] +- **Resource Pressure**: [nodes with >90% CPU or memory usage] +- **Recent Events**: [count of error/warning events in last 30 min, user-friendly summary] +- **Active Alerts**: [count of critical/warning alerts, skip if tool unavailable] +- **Recommendation**: [Proceed with upgrade to ${targetVersion} | Address risks/warnings first | Blocked - resolve issues | Target not available - use X.X.X instead] +`; +}; + +/** + * Generate health assessment prompt for cluster with no available updates + */ diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck.ts new file mode 100644 index 00000000000..607e6ff0ba8 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/precheck.ts @@ -0,0 +1,487 @@ +import { + PROMPT_TIMEOUT_TOTAL_LIMIT, + PROMPT_TIMEOUT_WARNING_THRESHOLD, + PROMPT_TIMEOUT_MAX_EXECUTION, +} from './shared/constants'; +import { getLanguageConstraint } from './shared/language-utils'; + +/** + * Pre-check prompt for clusters with available updates + * Assesses cluster readiness before initiating an upgrade + * + * @param currentVersion - Current cluster version + * @returns Formatted prompt for OLS pre-update assessment + */ +export const createPreCheckPrompt = (currentVersion: string) => { + const languageConstraint = getLanguageConstraint(); + + return `# OpenShift Cluster Upgrade Pre-Check Analysis + + +${languageConstraint} + +- YOU MUST ALWAYS CALL THE TOOLS TO GET THE INFORMATION. YOU SHOULD NEVER TREAT DATA FROM EXAMPLES AS REAL DATA. +- YOU MUST ALWAYS REFERENCE REAL DATA FROM TOOL CALLS. IF REAL DATA IS NOT AVAILABLE, NOTIFY THE USER AND REFUSE TO ANSWER USING INCORRECT DATA BUT DO NOT USE PLACEHOLDER OR DUMMY DATA. +- NEVER use placeholder or dummy data - only reference real data from tool calls. +- ONLY report issues that are actually present in the data. +- ONLY OUTPUT the Summary and TL;DR sections. +- Be specific about the source of any issues identified. +- CRITICAL: When counting available updates, count ALL array elements in status.availableUpdates AND status.conditionalUpdates separately. + + +**IN SCOPE - Issues that affect OCP cluster updates:** +- ClusterVersion conditions that block or signal upgrade problems +- ClusterOperator health that blocks operator-phase progression during upgrade +- MachineConfigPool state that blocks node-phase rollout +- Node conditions that prevent draining, rebooting, or rejoining during upgrade +- PodDisruptionBudgets that prevent node draining during rolling MCP updates +- Conditional update risks that apply to this specific cluster (Recommended=False) +- Admin-ack gates required before minor-version upgrades +- Deprecated API usage that will break after upgrade +- Pending CSRs that will prevent nodes from rejoining post-reboot +- OLM Subscription health for layered operators that must be compatible with target release +- Update path validity (channel, skip-level, EUS constraints) +- Resource pressure that prevents upgrade surge capacity +- Active alerts directly relevant to upgrade readiness +- Configuration overrides that mask CVO reconciliation + +**OUT OF SCOPE - Do NOT flag these unless they directly affect upgrades:** +- General application performance issues +- User workload errors unrelated to PDBs or node drain +- Non-upgrade-related warnings or events +- Cosmetic issues +- Issues that are recovering on their own and are not blocking +- Anything that does not have a documented effect on oc adm upgrade or the upgrade process + +If you cannot tie an issue to a specific upgrade-blocking or upgrade-disrupting mechanism, do not report it. + +**CRITICAL: Timeout and Error Handling** +**Timeout Awareness:** +- You have a ${PROMPT_TIMEOUT_TOTAL_LIMIT}-second timeout - manage your time wisely +- Prioritize essential data (ClusterVersion, ClusterOperators, admin-acks/admin-gates) first +- Track execution time and stop making new tool calls after ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds +- Provide analysis with available data rather than timing out trying to fetch everything +**Error Handling Rules:** +1. **Be specific about which tool failed** - don't give generic "cannot retrieve data" messages. +2. **Explain what data you're missing** - e.g., "Unable to fetch ClusterVersion resource" vs "Unable to retrieve data". +3. **Try alternative approaches**: + - If resources_list fails for all ClusterOperators, note this specifically. + - If nodes_top fails, continue with other analysis - it's optional. + - If get_alerts fails, skip it - alerts are optional. + - If events_list fails, continue without event data. + - If APIRequestCount fails or is unavailable on the cluster version, note it and skip. +4. **Provide partial analysis** - If you get ClusterVersion but not operators, analyze what you have. +5. **Give actionable troubleshooting** when tools fail: + - Check if OpenShift MCP server is running: 'oc get pods -n openshift-lightspeed' + - Verify cluster connectivity. + - Suggest checking MCP server logs for specific errors. +6. **NEVER give up completely** - Always provide SOME analysis even with partial data. +**Example of good error handling:** +- BAD: "I cannot retrieve necessary data from the cluster" +- GOOD: "Successfully retrieved ClusterVersion (current: 4.21.4, 7 updates available, 2 conditional updates with risks). However, unable to fetch ClusterOperator list (error: connection timeout). Based on ClusterVersion alone, the cluster reports Upgradeable=True and Failing=False. To complete operator health analysis, please verify the OpenShift MCP server is accessible." +**Example of good timeout handling:** +- GOOD: "Retrieved ClusterVersion, all 28 ClusterOperators, admin-acks, and admin-gates (execution time: 38 seconds). Skipping APIRequestCount and alerts to avoid timeout. All admin-ack gates are satisfied; cluster is on track for upgrade." +**Tool Call Priority to Avoid Timeouts:** +**PHASE 1 - ESSENTIAL (Always fetch, target under 25 seconds):** +1. resources_get: ClusterVersion (apiVersion: "config.openshift.io/v1", kind: "ClusterVersion", name: "version") + - Capture full status including: conditions, availableUpdates, conditionalUpdates, history, capabilities, desired +2. resources_list: ClusterOperator (apiVersion: "config.openshift.io/v1", kind: "ClusterOperator") +3. resources_get: ConfigMap "admin-gates" in namespace "openshift-config-managed" + (lists upgrade-blocking gate keys defined by the cluster's components) +4. resources_get: ConfigMap "admin-acks" in namespace "openshift-config" + (lists administrator acknowledgements) +**PHASE 2 - IMPORTANT (Fetch if time permits, under 45 seconds total):** +5. resources_list: Node (apiVersion: "v1", kind: "Node") - Quick check for NotReady nodes and pressure conditions +6. resources_list: MachineConfigPool (apiVersion: "machineconfiguration.openshift.io/v1", kind: "MachineConfigPool") +7. events_list: Get recent warning/error events from last 30 minutes in upgrade-relevant namespaces (openshift-cluster-version, openshift-machine-config-operator, openshift-etcd, openshift-kube-apiserver, openshift-apiserver, openshift-authentication, openshift-network-operator) +**PHASE 3 - OPTIONAL (Only if under ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds total):** +8. nodes_top: Check node CPU/memory usage +9. resources_list: PodDisruptionBudget (apiVersion: "policy/v1", kind: "PodDisruptionBudget") - Filter out openshift-*, kube-* +10. resources_list: APIRequestCount (apiVersion: "apiserver.openshift.io/v1", kind: "APIRequestCount") - Identify deprecated APIs in use +11. resources_list: CertificateSigningRequest (apiVersion: "certificates.k8s.io/v1", kind: "CertificateSigningRequest") - Filter for Pending state +12. resources_list: MachineHealthCheck (apiVersion: "machine.openshift.io/v1beta1", kind: "MachineHealthCheck") - Check for unpaused MHCs +13. resources_list: Subscription (apiVersion: "operators.coreos.com/v1alpha1", kind: "Subscription") - Layered operator health +14. get_alerts: Check for critical/warning alerts +**CRITICAL EFFICIENCY RULES:** +- If approaching ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds of execution time, STOP making new tool calls and provide analysis with data collected +- NEVER let total execution exceed ${PROMPT_TIMEOUT_MAX_EXECUTION} seconds to avoid timeout +- Prioritize breadth over depth: Get ClusterVersion + ClusterOperators + admin-acks fully before diving into logs/events +- Skip optional data if essential data took longer than expected + +BEFORE providing your response, verify: +1. Every word in your response is in the target language (except system identifiers like file paths, URLs, command names). +2. Technical terms are translated or explained in the target language. +3. No English phrases or mixed language content exists in your explanations. +4. All section headers and content follow the target language requirements. + + + + +This is a pre-upgrade analysis for OpenShift cluster version ${currentVersion}. You have complete cluster data including ClusterVersion, all ClusterOperator resources, admin-acks/admin-gates ConfigMaps, and supporting infrastructure resources. Focus on identifying real blockers and risks that would prevent or disrupt cluster upgrades. Stay strictly within the upgrade-impact scope defined above. + + + +CRITICAL: Understanding Kubernetes/OpenShift Conditions + +Conditions have TWO important fields you MUST check: +- **type**: The name of the condition (e.g., "Failing", "Available", "Progressing", "Upgradeable", "Recommended") +- **status**: The state of the condition (ONLY these values: "True", "False", or "Unknown") +**MANDATORY CHECKING PROCESS:** +For EVERY condition you analyze, you MUST: +1. First, locate the condition by its type field. +2. Second, read the EXACT value of the status field. +3. Third, interpret based ONLY on the status field value: + - If status="True" → The condition IS active/present. + - If status="False" → The condition is NOT active/NOT present. + - If status="Unknown" → The condition state is uncertain. +**DO NOT report a problem unless status="True" for negative conditions OR status="False" for positive conditions!** +**Critical Examples - MEMORIZE THESE:** +ClusterVersion / ClusterOperator / general: +- {type: "Failing", status: "False"} → Cluster is NOT failing → NO PROBLEM +- {type: "Failing", status: "True"} → Cluster IS failing → PROBLEM +- {type: "Available", status: "True"} → Component IS available → NO PROBLEM +- {type: "Available", status: "False"} → Component is NOT available → PROBLEM +- {type: "Degraded", status: "False"} → NOT degraded → NO PROBLEM +- {type: "Degraded", status: "True"} → IS degraded → PROBLEM +- {type: "Upgradeable", status: "True"} or absent → Upgrades allowed → NO PROBLEM +- {type: "Upgradeable", status: "False"} → Upgrades BLOCKED → PROBLEM (read message/reason) +- {type: "Progressing", status: "True"} → Currently changing state. Only a problem if stuck (check lastTransitionTime and message for errors). +- {type: "RetrievedUpdates", status: "True"} → Update service healthy → NO PROBLEM +- {type: "RetrievedUpdates", status: "False"} → Cannot reach update service → PROBLEM +- {type: "ReleaseAccepted", status: "True"} → Release image accepted → NO PROBLEM +- {type: "ReleaseAccepted", status: "False"} → Release image rejected (signature/manifest issue) → PROBLEM +- {type: "ImplicitlyEnabledCapabilities", status: "False"} → No capability surprise → NO PROBLEM +- {type: "ImplicitlyEnabledCapabilities", status: "True"} → Disabled capability was implicitly enabled → INFORMATIONAL/WARNING +Conditional update entries (status.conditionalUpdates[].conditions[]): +- {type: "Recommended", status: "True"} → Update IS recommended for this cluster → SAFE +- {type: "Recommended", status: "False"} → Update has KNOWN RISK matching this cluster → REPORT RISK (name, message, url) +- {type: "Recommended", status: "Unknown"} → CVO still evaluating → INFORMATIONAL +Node conditions: +- {type: "Ready", status: "True"} → Node is ready → NO PROBLEM +- {type: "Ready", status: "False"} or "Unknown" → Node NotReady → PROBLEM (will block drain/upgrade on that node) +- {type: "MemoryPressure", status: "True"} → Memory pressure → PROBLEM +- {type: "DiskPressure", status: "True"} → Disk pressure → PROBLEM (often blocks image pulls during upgrade) +- {type: "PIDPressure", status: "True"} → PID pressure → PROBLEM +- {type: "NetworkUnavailable", status: "True"} → Network unavailable → PROBLEM +MachineConfigPool conditions: +- {type: "Updated", status: "True"} → Pool is at desired config → NO PROBLEM +- {type: "Updated", status: "False"} → Pool not yet updated. Only a problem if stuck or paused inappropriately. +- {type: "Updating", status: "True"} → Pool currently rolling. Acceptable mid-upgrade; problem if stuck for hours. +- {type: "Degraded", status: "True"} → Pool degraded → PROBLEM (blocks further node updates) +- {type: "NodeDegraded", status: "True"} → A node in pool failed config apply → PROBLEM +- {type: "RenderDegraded", status: "True"} → Could not render config → PROBLEM (blocks any update for the pool) +**VERIFICATION REQUIREMENT:** +Before making ANY conclusion about a condition, you MUST internally state: +"Condition type='X' has status='Y'" and then interpret it correctly. +**NEVER assume a condition is true just because the type exists - ALWAYS check the status field!** +**The presence of a condition type does NOT mean it is active - check the status field!** + + + +### 1. Available Updates and Conditional Updates Analysis +**Available updates (status.availableUpdates):** +- Count EXACTLY how many items are in status.availableUpdates array. +- For each entry, extract: version, image, channels[], url (errata). +- Identify the latest recommended z-stream and the latest recommended y-stream (if any). +**Conditional updates (status.conditionalUpdates) — REQUIRED:** +- Count EXACTLY how many items are in status.conditionalUpdates array. +- For each conditional update, locate the conditions[] entry with type="Recommended": + - If status="False": the cluster matches a known risk for this target. Extract: + - Target release.version and release.image + - The reason and message from the Recommended condition + - All risks[] entries: name, message, url (Red Hat KCS or bug link) + - If status="Unknown": CVO has not finished evaluating risks yet — note as informational. + - If status="True": cluster does NOT match any risk for this target — treat as effectively recommended. +- Conditional update presence with Recommended=False is NOT itself an upgrade blocker, but it IS a risk the administrator must explicitly accept; surface it prominently. + +### 2. ClusterVersion Conditions - VERIFICATION REQUIRED +For each of the following, locate the condition in status.conditions[], read the status field, and interpret per . Quote the actual reason and message if reporting a problem. +a) **Failing**: status="True" → reconciliation failure, report reason and message. +b) **Upgradeable**: status="False" → upgrades are explicitly blocked; report reason and message verbatim. Common reasons include AdminAckRequired, MultipleReasons, operator-specific reasons. +c) **Available**: status="False" → cluster operationally impaired. +d) **Progressing**: status="True" AND not currently in an admin-initiated upgrade → may indicate a stuck reconciliation. +e) **RetrievedUpdates**: status="False" → Cincinnati/update service unreachable; cluster cannot discover updates. +f) **ReleaseAccepted**: status="False" → desired release image was rejected (signature verification, manifest validation, or image pull failure). +g) **ImplicitlyEnabledCapabilities**: status="True" → a capability disabled in spec was implicitly enabled; surface as informational warning. + +### 3. Admin-Ack Gate Analysis (CRITICAL FOR MINOR UPGRADES) +OpenShift requires administrators to acknowledge specific upgrade gates before minor-version upgrades. CVO sets Upgradeable=False with reason=AdminAckRequired until all applicable gates are acknowledged. +Procedure: +- Read keys from ConfigMap admin-gates in namespace openshift-config-managed. These are the gate keys the cluster's components have declared (example key shape: ack-4.13-kube-1.27-api-removals-in-4.14). +- Read keys from ConfigMap admin-acks in namespace openshift-config. An acknowledgement is valid only if the value is the literal string "true". +- For each key in admin-gates: + - If the same key exists in admin-acks with value "true" → ACKNOWLEDGED. + - If missing OR value is anything other than "true" → NOT ACKNOWLEDGED, and minor upgrade is blocked until administrator runs: + 'oc -n openshift-config patch cm admin-acks --patch '{"data":{"":"true"}}' --type=merge' +- Report each unacknowledged gate by its exact key name. Do NOT invent gate keys; only report what is actually present in admin-gates. +- If admin-gates is empty or absent → no current admin-ack gates apply. +- If either ConfigMap is unreadable, note it explicitly and indicate that gate state cannot be confirmed. + +### 4. ClusterOperator Health Check (per-operator condition matrix) +For each ClusterOperator, check status.conditions[]. Report as upgrade-impacting only when status fields match these patterns: +- Available=False → BLOCKER. Operator is down, will block its phase of the upgrade. +- Degraded=True → WARNING/POTENTIAL BLOCKER. Operator is reconciling with errors. If Available=True, upgrade may still proceed but with risk; if Available=False as well, treat as blocker. +- Upgradeable=False → BLOCKER for minor (and sometimes z-stream) upgrades. Report exact reason and message. +- Progressing=True for an extended period (no admin-initiated upgrade in flight) with error messages → POTENTIAL BLOCKER (stuck reconciliation). +**Pay special attention to these critical operators** (failures here are upgrade-blocking by nature): +- cluster-version (the CVO itself) +- etcd — quorum and member health gate the entire control plane upgrade +- kube-apiserver, kube-controller-manager, kube-scheduler — revision rollouts must converge before next phase +- openshift-apiserver +- machine-config — drives node-side updates +- machine-api — provisions/replaces nodes +- authentication +- network — SDN/OVN health is required for any rolling reboot +- dns +- ingress +- monitoring +- image-registry +- storage and any CSI driver operators +For each problematic operator, report: name, the failing condition (type and status), the reason, and the message. + +### 5. MachineConfigPool Status (Node Rollout Readiness) +For each MachineConfigPool (focus on master and worker, plus any custom pools): +- spec.paused == true → Pool is paused. Paused master pool is almost always wrong. Paused worker pool is acceptable only as part of a documented EUS upgrade workflow; flag it for administrator awareness because paused pools block node-level updates and inhibit certificate rotation. +- status.conditions[]: + - Degraded=True → BLOCKER for that pool's node updates. Report message. + - NodeDegraded=True → BLOCKER. A node in the pool failed to apply config; identify the node from the message. + - RenderDegraded=True → BLOCKER. The MCO cannot render a valid config for the pool. + - Updated=False AND Updating=False → Pool has pending changes but is not progressing — investigate. +- Configuration drift: If status.observedGeneration != metadata.generation, the pool is behind; mention if the gap is significant. +- status.machineCount, status.readyMachineCount, status.updatedMachineCount, status.degradedMachineCount — report if degradedMachineCount > 0 or if readyMachineCount < machineCount outside an active upgrade. + +### 6. Node Health and Resource Pressure +a) **Node Readiness and Pressure** (per Node, from status.conditions): + - Ready=False or Ready=Unknown → BLOCKER. The node cannot drain, reboot, and rejoin during a rolling update. Report node name and the condition's reason and message. + - MemoryPressure=True → BLOCKER/WARNING. Pods will be evicted and reschedule may not converge during upgrade surge; flag node name. + - DiskPressure=True → BLOCKER. Image pulls for new release content will fail. Flag node name and check /var/lib/containers if message indicates. + - PIDPressure=True → BLOCKER. + - NetworkUnavailable=True → BLOCKER. + - spec.unschedulable=true (cordoned) outside an active drain → flag for administrator awareness. +b) **Resource Utilization** (using nodes_top, optional): + - Flag any node with CPU usage > 90% or memory usage > 90%. + - Explain impact: Rolling upgrades require surge capacity (control-plane revisions roll one node at a time; worker pools drain one node at a time). Saturated nodes can prevent successful drain and pod rescheduling. + - For control-plane nodes, memory pressure is especially impactful because etcd is sensitive to I/O contention. + +### 7. PodDisruptionBudget Analysis (User Workload Drain Blockers) +PDBs become upgrade-relevant because the MachineConfigOperator drains worker nodes one at a time during rolling updates. A PDB that does not allow eviction will block the drain indefinitely. +Procedure: +- Query PDBs in ALL namespaces EXCEPT OpenShift system namespaces: + - All namespaces with prefix openshift- + - All namespaces with prefix kube- + - Namespaces default and openshift +- For each remaining (user workload) PDB, evaluate as a drain blocker if ANY of the following are true: + - status.disruptionsAllowed == 0 AND status.currentHealthy <= status.desiredHealthy (eviction blocked right now) + - spec.minAvailable equals 100% (or equals status.expectedPods) — no pod can be evicted + - spec.maxUnavailable == 0 — explicitly forbids any disruption + - The PDB selector matches zero pods (status.expectedPods == 0) AND minAvailable >= 1 — misconfigured, will block drain +- For each problematic PDB, report: namespace, name, the offending field, and status.disruptionsAllowed. +- Ignore all PDBs in OpenShift system namespaces — these are managed by Red Hat. +- If no problematic user workload PDBs exist, state "No problematic user workload PDBs found". + +### 8. Update Path Validation +a) **Channel correctness**: + - Read spec.channel (e.g., stable-4.21, fast-4.21, eus-4.18). + - Check status.desired.channels[] for channels available for the current version. + - If spec.channel is not present in status.desired.channels AND RetrievedUpdates=False, the channel may be invalid for this version — flag it. +b) **Skip-level detection**: + - Examine status.history[0].version (current) vs any administrator-mentioned target. + - OpenShift does NOT support skipping minor versions (e.g., 4.18 → 4.20 directly). Upgrades must go through each intermediate minor, except via the EUS-to-EUS path where worker pools are paused. + - If the latest available update or conditional update is more than one minor ahead of the current version, surface this as an informational note about path constraints. +c) **EUS path indicators**: + - If spec.channel starts with eus-, note that the cluster is on the EUS path and that worker MCP pause/unpause is part of the workflow. + +### 9. Deprecated API Usage (Affects Minor Upgrades) +If the APIRequestCount resource is available on this cluster (apiserver.openshift.io/v1): +- List APIRequestCount objects. +- For each, read status.removedInRelease. If a removal release is set AND the upcoming minor target matches or exceeds it: + - Read status.currentHour.byUser[] and status.last24h[].byUser[] to identify which clients are still calling the API. + - Report: API name (e.g., flowschemas.v1beta2.flowcontrol.apiserver.k8s.io), removedInRelease, and a deduplicated list of top callers (username and userAgent). +- If no deprecated APIs are in use OR none are removed by the target release → state so explicitly. +- If APIRequestCount is unavailable, skip with a note. + +### 10. Pending CertificateSigningRequests +During upgrades, nodes that reboot must have their kubelet client and serving certificates approved. A backlog of pending CSRs can prevent nodes from rejoining the cluster. +- List CertificateSigningRequest objects. +- Filter to those with no Approved condition AND no Denied condition (i.e., still pending) OR with status.certificate empty. +- Group by spec.signerName (e.g., kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/kubelet-serving). +- If 5 or more node-related CSRs are pending, flag as a node rejoin risk and report counts and signer names. +- Pending CSRs unrelated to nodes (custom signers) can be ignored unless explicitly tied to upgrade workflows. + +### 11. MachineHealthCheck Status +Active MachineHealthChecks can interfere with upgrades by remediating nodes that are intentionally drained or rebooted as part of the upgrade. Red Hat documentation recommends pausing MHCs during upgrades. +- List MachineHealthChecks. +- For each, check metadata.annotations["cluster.x-k8s.io/paused"] or metadata.annotations["machine.openshift.io/cluster-api-cluster"] paused-style annotations. Different OCP versions use different paused annotations; if any pause annotation is present, treat as paused. +- Report MHCs that are NOT paused and target node sets that will roll during the upgrade — surface as a recommendation, not a blocker. + +### 12. OLM Subscription Health (Layered Operators) +Layered operators installed via OLM must be on a channel/version compatible with the target OpenShift release before upgrade. +- List Subscriptions. +- For each Subscription, examine status.conditions[]: + - CatalogSourcesUnhealthy=True → operator catalog cannot be reached (will block any operator updates) + - InstallPlanFailed=True or ResolutionFailed=True → operator cannot install/update; flag the operator + - InstallPlanPending=True AND not progressing → manual approval may be required before upgrade +- Report by namespace and Subscription name. Do not flag healthy Subscriptions. + +### 13. Cluster Capabilities Assessment +- Extract enabled capabilities from status.capabilities.enabledCapabilities. +- Extract known capabilities from status.capabilities.knownCapabilities. +- Disabled capabilities = known minus enabled. Note these. +- If ImplicitlyEnabledCapabilities=True, surface that the upgrade target implicitly enables a capability that was disabled in spec.capabilities. +- Capabilities themselves are rarely upgrade blockers, but capability transitions can change which operators are reconciled. + +### 14. Cincinnati Update Service Health +- spec.upstream: if set, the cluster uses a custom update service; if unset, default Red Hat update service is used. +- Verify RetrievedUpdates condition: status, lastTransitionTime, message. +- If status.availableUpdates is empty AND RetrievedUpdates=True → cluster is on the latest known version in its channel. +- If status.availableUpdates is empty AND RetrievedUpdates=False → update discovery is broken. +- spec.clusterID: report for telemetry context. +- spec.signatureStores: if present, custom signature stores are configured (relevant for disconnected clusters and ReleaseAccepted failures). + +### 15. Cluster Version History Context +- Initial version: status.history[] last entry (oldest). +- Most recent completed upgrade: most recent status.history[] entry with state="Completed". +- Any state="Partial" entries indicate failed or interrupted upgrades — surface them. +- Cluster age: derive from oldest history entry's startedTime or completionTime. + +### 16. Configuration Overrides +- Review spec.overrides[]. Each entry with unmanaged=true means the CVO will not reconcile that resource. +- Overrides are not upgrade blockers per se, but they can mask drift and cause post-upgrade inconsistencies. Surface any overrides as informational. + +### 17. Recent Events Analysis (Upgrade-Relevant Only) +- Query events from last 30 minutes, type Warning or higher. +- Restrict to upgrade-relevant namespaces: openshift-cluster-version, openshift-machine-config-operator, openshift-etcd, openshift-kube-apiserver, openshift-apiserver, openshift-authentication, openshift-network-operator, openshift-monitoring. +- Group by reason and involvedObject to avoid noise. +- Translate technical reasons into plain language: + - ImagePullBackOff → "Operator pod cannot pull its container image — check registry connectivity or pull secrets" + - FailedScheduling → "Operator pod cannot be scheduled — check node taints, resources, or selectors" + - Unhealthy (for etcd) → "etcd member health check failing — investigate before upgrading" +- Skip events unrelated to upgrade readiness. + +### 18. Active Alerts (Optional) +If get_alerts is available: +- Focus on severity=critical and severity=warning. +- Prioritize alerts whose names indicate upgrade impact, including but not limited to: ClusterNotUpgradeable, ClusterOperatorDown, ClusterOperatorDegraded, KubeAPIDown, etcdMembersDown, etcdInsufficientMembers, KubePersistentVolumeFillingUp, NodeFilesystemSpaceFillingUp, KubeNodeNotReady, MachineConfigDaemonReboot-style alerts. +- Translate each fired alert into an actionable recommendation. +- If get_alerts is unavailable, skip this section. + + + +## Summary +**Update Service Health** +- **Cincinnati Service**: [spec.upstream URL if configured, otherwise "Default Red Hat update service"] +- **Service Status**: [RetrievedUpdates condition status and message] +- **Last Update Check**: [From RetrievedUpdates condition lastTransitionTime] +- **Update Channel**: [Current spec.channel] +- **Channel Validity**: [Confirmed valid for current version, or flagged as not in status.desired.channels] +- **Cluster ID**: [spec.clusterID] +**Cluster History Context** +- **Initial Version**: [First entry from status.history with date] +- **Upgrade Path**: [Recent version progression from history] +- **Last Completed Upgrade**: [Most recent Completed entry with timeframe] +- **Partial/Failed Upgrade History**: [Any Partial entries, otherwise "None"] +- **Cluster Age**: [Time since initial installation] +**Available Updates** +- **Recommended Updates**: [Count from status.availableUpdates with versions] +- **Conditional Updates**: [Count from status.conditionalUpdates] +- **Conditional Update Risk Analysis**: For each conditional update with Recommended=False, list: + - Target version, risk name, risk message, reference URL + - Otherwise: "No conditional update risks apply to this cluster" +**Upgrade Readiness Assessment** + +YOU MUST explicitly state the status field value for each condition you check. +**ClusterVersion Conditions:** +- **Failing**: [type="Failing" found with status="X"] → [interpretation] +- **Upgradeable**: [type="Upgradeable" found with status="X" OR not found] → [interpretation, including reason and message if status="False"] +- **Available**: [type="Available" found with status="X"] → [interpretation] +- **Progressing**: [type="Progressing" found with status="X"] → [interpretation; only flag if stuck and not in admin-initiated upgrade] +- **RetrievedUpdates**: [status="X"] → [interpretation] +- **ReleaseAccepted**: [status="X"] → [interpretation] +- **ImplicitlyEnabledCapabilities**: [status="X"] → [interpretation] +**Admin-Ack Gates (Minor Upgrade Prerequisite):** +- **Defined Gates** (from openshift-config-managed/admin-gates): [list of keys, or "None"] +- **Acknowledged** (from openshift-config/admin-acks with value "true"): [list of keys] +- **Outstanding Gates Blocking Minor Upgrade**: [list of keys not acked, or "None — all gates satisfied"] +- **Action**: For each outstanding gate, provide the exact oc patch command using the actual key name. +**ClusterOperator Health:** +- **Total Operators**: [count] +- **Operators With Issues**: For each problematic operator, report: + - Name + - Failing condition (type and status) + - Reason and message +- If none: "All ClusterOperators report Available=True, Degraded=False, Upgradeable=True" +**Infrastructure Health:** +- **MachineConfigPools**: For each pool, report state. Flag Degraded=True, NodeDegraded=True, RenderDegraded=True, paused=true, or readyMachineCount < machineCount outside active upgrade. +- **Node Status**: Count of NotReady nodes with names and reasons. Count of nodes with MemoryPressure/DiskPressure/PIDPressure/NetworkUnavailable. +- **Resource Pressure**: From nodes_top, list nodes with >90% CPU or memory. +- **Pending CSRs**: Count and signer names if 5 or more pending node-related CSRs. +- **MachineHealthChecks**: Count of unpaused MHCs (informational recommendation). +- **User Workload PDBs**: Count of problematic non-OpenShift PDBs that could block node draining, with namespace/name and the offending field. +**Deprecated API Usage:** +- **Deprecated APIs Removed In Target**: For each, report API name, removedInRelease, and top callers (username/userAgent). +- If none or APIRequestCount unavailable: state explicitly. +**Layered Operator Health (OLM):** +- **Subscriptions With Issues**: For each, namespace and Subscription name with the failing condition (CatalogSourcesUnhealthy, InstallPlanFailed, ResolutionFailed, etc.). +- If none: "All Subscriptions healthy" +**Recent Events** (Last 30 minutes, upgrade-relevant namespaces): +- **Critical Events**: Count and grouped descriptions. +- **Warning Events**: Count and grouped descriptions. +- **User-Friendly Summary**: Translate technical events into plain language. +- If none: "No recent errors or warnings detected in upgrade-relevant components" +**Active Alerts** (if available): +- **Critical Alerts**: Count and names. +- **Warning Alerts**: Count and names. +- **Impact on Upgrade**: For each, explain effect on upgrade readiness. +- If unavailable: skip section. +**Configuration:** +- **Overrides**: Any spec.overrides entries with unmanaged=true. +- **Capabilities**: Enabled count, disabled-but-known count with names, ImplicitlyEnabledCapabilities note if applicable. +**Final Assessment:** +Based ONLY on issues identified above: +- If no upgrade-blocking conditions and no unaccepted conditional risks: "Cluster appears ready for upgrade." +- If only conditional update risks or warnings (no hard blockers): "Cluster can upgrade after administrator review of: [list]. No hard blockers." +- If hard blockers present: "Upgrade blocked — must resolve [list of specific blockers] first." +A "hard blocker" means at least one of: +- ClusterVersion Upgradeable=False +- ClusterVersion Failing=True +- ClusterVersion ReleaseAccepted=False +- Any ClusterOperator with Available=False or Upgradeable=False +- Any MachineConfigPool with Degraded=True, NodeDegraded=True, or RenderDegraded=True +- Any node with Ready=False (other than transient and self-recovering) +- Any user-workload PDB blocking eviction (disruptionsAllowed=0 with no surge capacity) +- Outstanding admin-ack gate (only blocks minor upgrades, not z-stream) +- Deprecated API in active use that is removed in target minor +## TL;DR +- **Current Version**: ${currentVersion} +- **Available Updates**: [count from status.availableUpdates] +- **Latest Recommended Update**: [version with channels] +- **Conditional Updates**: [count] ([N with Recommended=False risks applying to this cluster]) +- **Update Channel**: [current spec.channel] ([valid / not in status.desired.channels]) +- **Channel Options**: [available channels for current version] +- **Capabilities**: [enabled count / disabled count with names] +- **Initial Version**: [from history with date] +- **Last Upgrade**: [most recent completed upgrade with date] +- **Cincinnati Health**: [service status with timestamp] +- **Admin-Ack Gates**: [satisfied | N outstanding: list of keys] +- **Upgrade Blocked**: [Yes | No — only "Yes" if a hard blocker per definition above is present] +- **Upgrade Blockers**: [specific list with status field values, or "No blockers"] +- **Conditional Risks To Acknowledge**: [risk names with target versions, or "None"] +- **Unhealthy ClusterOperators**: [count and names with the failing condition] +- **Degraded MCPs**: [count and names with failing condition] +- **Paused MCPs**: [names if any] +- **Node Issues**: [count of NotReady or pressure-affected nodes with names] +- **Resource Pressure**: [nodes with >90% CPU or memory] +- **User Workload PDBs Blocking Drain**: [count with namespace/name] +- **Pending Node CSRs**: [count if >= 5, else omit or "None significant"] +- **Deprecated APIs In Use**: [count removed in target, with API names] +- **Layered Operator Issues**: [count of unhealthy Subscriptions] +- **Recent Events**: [count of upgrade-relevant errors/warnings in last 30 min] +- **Active Alerts**: [count of critical/warning, skip if unavailable] +- **Configuration Issues**: [overrides or capability concerns] +- **Recommendation**: [Proceed with upgrade | Address warnings first | Blocked — resolve listed issues] +`; +}; + +/** + * Generate precheck prompt for specific target version + */ diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/progress.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/progress.ts new file mode 100644 index 00000000000..db47fc57b46 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/progress.ts @@ -0,0 +1,220 @@ +import { + PROMPT_TIMEOUT_TOTAL_LIMIT, + PROMPT_TIMEOUT_WARNING_THRESHOLD, + PROMPT_TIMEOUT_MAX_EXECUTION, +} from './shared/constants'; +import { getLanguageConstraint } from './shared/language-utils'; + +export interface OperatorStatusCounts { + total: number; + updated: number; + updating: number; + pending: number; + failed: number; +} +export const createProgressPrompt = ( + currentVersion: string, + desiredVersion: string, + operatorCounts: OperatorStatusCounts, +) => { + const languageConstraint = getLanguageConstraint(); + + return `# OpenShift Cluster Upgrade Progress Monitor + + +- YOU MUST ALWAYS CALL THE TOOLS TO GET THE INFORMATION. YOU SHOULD NEVER TREAT DATA FROM EXAMPLES AS REAL DATA. +- YOU MUST ALWAYS REFERENCE REAL DATA FROM TOOL CALLS. IF REAL DATA IS NOT AVAILABLE, NOTIFY THE USER AND REFUSE TO ANSWER USING INCORRECT DATA BUT DO NOT USE PLACEHOLDER OR DUMMY DATA. +**CRITICAL: Timeout and Error Handling** + +**Timeout Awareness (${PROMPT_TIMEOUT_TOTAL_LIMIT} second limit):** +- Progress monitoring needs to be FAST - users expect quick updates +- ClusterVersion + ClusterOperators gives you operator progress (X of Y) - sufficient for basic progress +- Events and other data add context but aren't required +- Target: Complete analysis in under 40 seconds for responsive UX +- If approaching ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds, provide progress summary immediately +**Error Handling for Tool Failures:** +1. **Core data is essential**- ClusterVersion and ClusterOperators are required for progress tracking +2. **If core resources fail** - Explain specifically what failed and provide troubleshooting +3. **Optional data can be skipped**- nodes_top, events_list, get_alerts are nice-to-have +4. **Provide progress with available data**- Even without events, you can show operator progress +5. **Never give up**- Always show some progress information, even if incomplete +**Tool Call Priority to Avoid Timeouts:** +**PHASE 1 - ESSENTIAL (Always fetch - target: 25 seconds):** +1. resources_get: ClusterVersion (apiVersion: "config.openshift.io/v1", kind: "ClusterVersion", name: "version") +2. resources_list: ClusterOperator (apiVersion: "config.openshift.io/v1", kind: "ClusterOperator") +**PHASE 2 - HELPFUL CONTEXT (Only if under 45 seconds):** +3. events_list: Get recent events (last 30 minutes) - Quick way to spot warnings +4. resources_list: MachineConfigPool - Shows node update progress +**PHASE 3 - NICE-TO-HAVE (Only if under ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds):** +5. nodes_top: Monitor node resource usage during upgrade +6. get_alerts: Check for warning alerts (if available) +**CRITICAL EFFICIENCY RULES:** +- Progress monitoring is time-sensitive - provide fast updates +- ClusterVersion + ClusterOperators is sufficient for basic progress (X of Y operators) +- Events and MCPs add context but aren't required +- NEVER exceed ${PROMPT_TIMEOUT_MAX_EXECUTION} seconds - better to show quick progress than timeout +- Users can refresh for updated progress - speed > completeness + +- Monitor ONLY actual upgrade progress from tool call data +- Report specific progress indicators and timelines using EXACT operator counts from the data +- Use the format "X of Y operators" consistently throughout the output +- Calculate precise percentages: (${operatorCounts.updated} / ${operatorCounts.total}) * 100 +- Format durations in human-readable terms (e.g., "Approximately 1 hour and 20 minutes") +- Use specific operator counts in all sections, not generic descriptions +- Identify potential issues early with conservative recommendations +- ONLY OUTPUT the Summary and TL;DR sections exactly as specified in the output format +${languageConstraint} + + + +Monitor upgrade progress from ${currentVersion} to ${desiredVersion}. You have complete cluster data including ClusterVersion and all ClusterOperator resources to analyze upgrade progress and detect issues. +Focus on detecting issues early while avoiding false alarms. + + + +CRITICAL: Understanding Kubernetes/OpenShift Conditions + +Conditions have TWO important fields you MUST check: +- **type**: The name of the condition (e.g., "Failing", "Available", "Progressing") +- **status**: The state of the condition ("True", "False", or "Unknown") + +**How to Correctly Check Conditions:** +- A condition is TRUE when: type="X" AND status="True" +- A condition is FALSE when: type="X" AND status="False" +- A condition is UNKNOWN when: type="X" AND status="Unknown" + +**Examples:** +- {type: "Progressing", status: "True"} means the cluster IS progressing (upgrading) +- {type: "Progressing", status: "False"} means the cluster is NOT progressing (stable) +- {type: "Failing", status: "False"} means the cluster is NOT failing (healthy) +- {type: "Failing", status: "True"} means the cluster IS failing (problem) +**NEVER assume a condition is true just because the type exists - ALWAYS check the status field!** + + + + +1. **Upgrade State Verification** (Check BOTH type AND status): + - Confirm spec.desiredUpdate.version matches ${desiredVersion} + - Find condition where type="Progressing" AND status="True" - extract progress details + - Verify no conditions where type="Failing" AND status="True" are present + +2. **Component Progress Tracking** (CRITICAL - Use Provided Operator Counts): + - You are provided with pre-calculated operator counts: ${operatorCounts.total} total, ${operatorCounts.updated} updated, ${operatorCounts.updating} updating, ${operatorCounts.pending} pending, ${operatorCounts.failed} failed + - ALWAYS use the "X of Y operators" format consistently: + * "**Updated Operators**: ${operatorCounts.updated} of ${operatorCounts.total} operators at target version ${desiredVersion}" + * "**Updating Operators**: ${operatorCounts.updating} of ${operatorCounts.total} operators progressing toward target" + * "**Pending Operators**: ${operatorCounts.pending} of ${operatorCounts.total} operators waiting to start" + * "**Failed Operators**: ${operatorCounts.failed} of ${operatorCounts.total} operators with issues" + - Calculate upgrade completion percentage using the exact formula: (${operatorCounts.updated} / ${operatorCounts.total}) * 100 + - In TL;DR section, use format: "${operatorCounts.updated} of ${operatorCounts.total} operators at target version ([X% complete])" + - For pending components, combine counts: "${operatorCounts.updating} updating + ${operatorCounts.pending} pending operators" + - NEVER use vague terms like "several" or "most" - always use exact counts provided + +3. **Timeline and ETA Analysis - CRITICAL INSTRUCTIONS**: +**FINDING THE CORRECT START TIME:** + - Look in status.history array - it's ordered with MOST RECENT first (index 0) + - The CURRENT upgrade is the FIRST entry where state="Partial" (in-progress upgrade) + - Use the startedTime field from that Partial entry ONLY + - Example: If history[0].state="Partial" and history[0].startedTime="2026-05-04T16:59:26Z", use "2026-05-04T16:59:26Z" + - DO NOT use startedTime from older entries with state="Completed" - those are PREVIOUS upgrades! +**FORMATTING AND CALCULATIONS:** + - Format the startedTime as human-readable (e.g., "May 4, 2026, 4:59:26 PM UTC") + - Calculate elapsed time from startedTime to current time + - Format elapsed time as human-readable duration (e.g., "Approximately 1 hour and 20 minutes") + - Extract progress details from Progressing condition message if available + - Calculate progress percentage: (${operatorCounts.updated} / ${operatorCounts.total}) * 100 + - Calculate ETA based on current progress rate +**OUTPUT FORMAT:** + * "Upgrade started: [human-readable start time from the Partial entry]" + * "Elapsed time: [Human-readable duration since startedTime]" + * "Current progress: [X% complete]" + * "Estimated completion: [Time remaining]" + * "Progress rate: [On track | Ahead of schedule | Behind schedule]" + +4. **Upgrade Target Analysis**: + - Current upgrade target from status.desired.version + - Target release metadata from status.desired (url, channels) + - Upgrade path validation from current to target version + - Any upgrade risks or compatibility notes + +5. **Cluster History Context During Upgrade**: + - Previous completed upgrade and duration for comparison + - Upgrade frequency pattern analysis + - Any historical upgrade failures or issues + - Progress comparison with typical upgrade patterns + +6. **Early Issue Detection**: + - Look for warning signs in status.conditions + - Check for stalled progress indicators in cluster conditions + - Report specific issues using exact operator counts: "${operatorCounts.failed} operators with issues" + - If no issues: "No problems requiring immediate attention" + - Use format in TL;DR: "**Issues**: [${operatorCounts.failed} operators with issues if any, otherwise "No problems requiring immediate attention"]" + - Monitor for unexpected delays compared to historical patterns and report as "On track", "Delayed", or "Issues detected" + + + + +## Summary +**Upgrade Status** +- **Current Phase**: [Extract from Progressing condition message, e.g., "Progressing (Working towards 4.21.7: X of Y done (Z% complete))"] +- **Elapsed Time**: [Human-readable duration from upgrade start to current time] +- **Progress Indicators**: [Specific progress details and any operators currently updating] +**Component Status** (Total: ${operatorCounts.total} ClusterOperators) +- **Updated Operators**: ${operatorCounts.updated} of ${operatorCounts.total} operators at target version ${desiredVersion} +- **Updating Operators**: ${operatorCounts.updating} of ${operatorCounts.total} operators progressing toward target +- **⏸ Pending Operators**: ${operatorCounts.pending} of ${operatorCounts.total} operators waiting to start +- **Failed Operators**: ${operatorCounts.failed} of ${operatorCounts.total} operators with issues +**Upgrade Target Details** +- **Target Version**: [${desiredVersion} from status.desired.version] +- **Target Release Info**: [Errata URL from status.desired.url if available, format as markdown link] +- **Target Channels**: [List available channels from status.desired.channels, comma-separated] +- **Upgrade Path**: Current version [${currentVersion}] → Target version [${desiredVersion}] +**Historical Context** +- **Previous Upgrade**: [Most recent completed upgrade version and completion timestamp from status.history] +- **Upgrade Pattern**: [Upgrade frequency analysis and historical success pattern] +- **Duration Comparison**: [Current upgrade timeline compared to previous upgrade durations and typical patterns] +**Infrastructure Health During Upgrade** +- **MachineConfigPool Progress**: [Status of MCPs - are they updating, stuck, or complete?] +- **Node Resource Pressure**: [From nodes_top - any nodes with high CPU/memory usage?] + - Example: "All nodes healthy - CPU usage 45-60%, memory usage 55-70%" + - Example: " Warning: master-0 at 92% memory - monitor for slowdowns" +**Recent Progress Events** (Last 30 minutes): +- **Event Summary**: [Count of events related to upgrade progress] +- **Warning Signs**: [Any warning events that might slow progress] + - Example: "ImagePullBackOff in 3 operators - image download issues may slow upgrade" + - Example: "No concerning events - upgrade progressing normally" +- **Positive Indicators**: [Events showing healthy progress] + - Example: "12 operators successfully updated to target version" +**Health Indicators** +- **Issues Detected**: [Any warning signs, delays, or specific operator issues requiring attention] +- **Cluster Status**: [Overall cluster condition health based on ClusterVersion conditions] +- **Active Alerts**: [Warning/critical alerts during upgrade, if available] +- **Timeline Analysis**: + * Upgrade started: [Find the FIRST entry in status.history where state="Partial" - this is the CURRENT upgrade. Use ONLY its startedTime field. Convert from ISO timestamp (e.g., "2026-05-04T16:59:26Z") to human-readable (e.g., "May 4, 2026, 4:59:26 PM UTC"). DO NOT use startedTime from Completed entries!] + * Elapsed time: [Calculate duration from the Partial entry's startedTime to current time in human-readable format] + * Current progress: [X% complete based on operator completion ratio] + * Estimated completion: [Time remaining calculation based on progress rate] + * Progress rate: [Assessment: "On track", "Ahead of schedule", or "Behind schedule" compared to typical upgrade window] + +## TL;DR +- **Progress**: [X% complete - (${operatorCounts.updated} Updated Operators / ${operatorCounts.total} Total Operators) * 100] +- **Target Version**: [${desiredVersion} with release info if available] +- **Target Channels**: [Available channels for target release] +- **Upgrade Duration**: [Elapsed time from upgrade start] +- **Status**: [On track | Delayed | Issues detected] +- **Updated Components**: ${operatorCounts.updated} of ${operatorCounts.total} operators at target version ([X% complete]) +- **Pending Components**: ${operatorCounts.updating} updating + ${operatorCounts.pending} pending operators +- **Historical Comparison**: [How current upgrade compares to previous ones] +- **Issues**: [${operatorCounts.failed} operators with issues if any, otherwise "No problems requiring immediate attention"] +- **Resource Pressure**: [Node CPU/memory status - any nodes >90% usage?] +- **MCP Status**: [MachineConfigPool progress - all updating normally?] +- **Recent Events**: [Count of warning events in last 30 min, user-friendly summary] +- **Alerts**: [Warning/critical alerts during upgrade, if available] +- **ETA**: [Estimated time remaining based on current progress rate] +- **Action Required**: [Continue monitoring | Investigate delays | Address operator issues] +`; +}; + +/** + * Generate precheck prompt for cluster with available updates + */ diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/constants.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/constants.ts new file mode 100644 index 00000000000..ff6e2363c94 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/constants.ts @@ -0,0 +1,30 @@ +/** + * Prompt-specific constants + * These values control timing recommendations and behavior in OLS prompts + */ + +/** + * Timeout constants for OLS prompt execution + * These values control the timing recommendations in troubleshooting prompts + */ + +/** Total timeout limit for OLS tool execution (seconds) */ +export const PROMPT_TIMEOUT_TOTAL_LIMIT = 60; + +/** Warning threshold - stop and analyze if approaching this (seconds) */ +export const PROMPT_TIMEOUT_WARNING_THRESHOLD = 50; + +/** Phase 2 execution threshold (seconds) */ +export const PROMPT_TIMEOUT_PHASE_2_THRESHOLD = 35; + +/** Phase 1 target completion time (seconds) */ +export const PROMPT_TIMEOUT_PHASE_1_TARGET = 20; + +/** Maximum execution time before forced completion (seconds) */ +export const PROMPT_TIMEOUT_MAX_EXECUTION = 55; + +/** Number of log lines to fetch per pod */ +export const PROMPT_TIMEOUT_LOG_TAIL_LINES = 50; + +/** Deprecated log tail size (not recommended) */ +export const PROMPT_TIMEOUT_LOG_TAIL_LINES_FULL = 100; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/language-utils.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/language-utils.ts new file mode 100644 index 00000000000..c2104e83763 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/shared/language-utils.ts @@ -0,0 +1,90 @@ +import i18next from 'i18next'; +import { supportedLocales } from '@console/app/src/components/user-preferences/language/const'; + +/** + * Language constraint utilities for OLS prompts + * Ensures prompts are generated in the user's selected language + */ + +/** + * Parse language display name from supportedLocales format + * Format: "Native Name - English Name" (e.g., "Español - Spanish") + * Special case: English is just "English" + * + * @param displayName - Display name from supportedLocales + * @returns Object with nativeName and englishName, or null if parsing fails + */ +const parseLanguageDisplayName = ( + displayName: string, +): { nativeName: string; englishName: string } | null => { + // Handle English special case (no delimiter) + if (displayName === 'English') { + return { nativeName: 'English', englishName: 'English' }; + } + + // Parse "Native Name - English Name" format + const delimiterIndex = displayName.indexOf(' - '); + if (delimiterIndex === -1) { + return null; // Invalid format + } + + const nativeName = displayName.slice(0, delimiterIndex).trim(); + const englishName = displayName.slice(delimiterIndex + 3).trim(); + + if (!nativeName || !englishName) { + return null; // Empty parts + } + + return { nativeName, englishName }; +}; + +/** + * Get the current language constraint for prompts + * Uses supportedLocales as the single source of truth for language configuration + * Always uses the current UI language from i18next + * + * @returns Language constraint string to be included in prompts + * + * @example + * ```ts + * const constraint = getLanguageConstraint(); + * // For English: "- LANGUAGE REQUIREMENT: Respond in English..." + * // For Spanish: "- CRITICAL LANGUAGE REQUIREMENT: You MUST respond ENTIRELY in Spanish..." + * ``` + */ +export const getLanguageConstraint = (): string => { + const targetLang = i18next.language || 'en'; + + // English constraint used for default, unsupported languages, and fallbacks + const englishConstraint = + '- LANGUAGE REQUIREMENT: Respond in English. All analysis, explanations, recommendations, and text must be in English.'; + + // Short-circuit for English + if (targetLang === 'en') { + return englishConstraint; + } + + // Use supportedLocales as the authoritative source + const languageDisplayName = supportedLocales[targetLang]; + if (!languageDisplayName) { + // Unsupported language - fall back to English + return englishConstraint; + } + + // Parse the display name to extract native and English names + const parsed = parseLanguageDisplayName(languageDisplayName); + if (!parsed) { + // Invalid format - fall back to English + return englishConstraint; + } + + const { nativeName, englishName } = parsed; + + // Generate critical language requirement for non-English languages + return `- CRITICAL LANGUAGE REQUIREMENT: You MUST respond ENTIRELY in ${englishName} (${nativeName}). Every explanation, recommendation, and analysis must be in ${englishName}. However, you MUST preserve the following in English: + * Kubernetes/OpenShift resource types (Pod, Node, ClusterVersion, ClusterOperator, MachineConfigPool, etc.) + * API field names and condition types (status, spec, metadata, Progressing, Available, Degraded, etc.) + * Technical identifiers (file paths, URLs, command names, resource names, etc.) + * Error messages and log output from the cluster +Only translate your explanatory text and recommendations. Do NOT translate technical terminology that has specific meaning in Kubernetes/OpenShift.`; +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/prompts/troubleshoot.ts b/frontend/packages/console-shared/src/components/cluster-updates/prompts/troubleshoot.ts new file mode 100644 index 00000000000..b556eeae989 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/prompts/troubleshoot.ts @@ -0,0 +1,266 @@ +import { + PROMPT_TIMEOUT_TOTAL_LIMIT, + PROMPT_TIMEOUT_WARNING_THRESHOLD, + PROMPT_TIMEOUT_PHASE_2_THRESHOLD, + PROMPT_TIMEOUT_PHASE_1_TARGET, + PROMPT_TIMEOUT_MAX_EXECUTION, + PROMPT_TIMEOUT_LOG_TAIL_LINES, + PROMPT_TIMEOUT_LOG_TAIL_LINES_FULL, +} from './shared/constants'; +import { getLanguageConstraint } from './shared/language-utils'; + +/** + * Troubleshoot prompt for failing or stalled cluster updates + * Used when upgrade failures or component degradation is detected + * + * @param currentVersion - Current cluster version + * @param desiredVersion - Target version for the failed upgrade + * @returns Formatted prompt for OLS troubleshooting assistance + */ +export const createTroubleshootPrompt = (currentVersion: string, desiredVersion: string) => { + const languageConstraint = getLanguageConstraint(); + + return `# OpenShift Cluster Upgrade Troubleshoot Analysis + + +- YOU MUST ALWAYS CALL THE TOOLS TO GET THE INFORMATION. YOU SHOULD NEVER TREAT DATA FROM EXAMPLES AS REAL DATA. +- YOU MUST ALWAYS REFERENCE REAL DATA FROM TOOL CALLS. IF REAL DATA IS NOT AVAILABLE, NOTIFY THE USER AND REFUSE TO ANSWER USING INCORRECT DATA BUT DO NOT USE PLACEHOLDER OR DUMMY DATA. +**CRITICAL: Timeout and Error Handling** + +**Timeout Awareness (${PROMPT_TIMEOUT_TOTAL_LIMIT} second limit):** +- Prioritize ClusterVersion + ClusterOperators first (essential for failure diagnosis) +- Fetch events_list early - often explains failures quickly without needing logs +- Limit pod log fetching - logs are SLOW, only fetch 1-2 critical operators +- If approaching ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds, STOP and analyze what you have +- Partial diagnosis is better than timeout + +**Error Handling for Tool Failures:** +1. **Try core resources first** - ClusterVersion and ClusterOperators are essential +2. **If core resources fail** - Provide specific error and troubleshooting steps +3. **If optional tools fail** (pods_log, events_list, get_alerts) - Continue with available data +4. **Provide partial analysis** - Analyze whatever data you successfully retrieved +5. **Be specific** - "Unable to fetch operator pod logs from openshift-authentication namespace" NOT "cannot retrieve data" +6. **Give troubleshooting steps**: + - Verify MCP server is running: 'oc get pods -n openshift-lightspeed' + - Check if operator namespaces exist + - Suggest manual log checking: 'oc logs -n openshift-authentication ' +**Tool Call Priority to Avoid Timeouts:** +**PHASE 1 - ESSENTIAL (Always fetch - target: ${PROMPT_TIMEOUT_PHASE_1_TARGET} seconds):** +1. resources_get: ClusterVersion (apiVersion: "config.openshift.io/v1", kind: "ClusterVersion", name: "version") +2. resources_list: ClusterOperator (apiVersion: "config.openshift.io/v1", kind: "ClusterOperator") +**PHASE 2 - HIGH-VALUE DIAGNOSTICS (If under ${PROMPT_TIMEOUT_PHASE_2_THRESHOLD} seconds):** +3. events_list: Get events from last 1 hour - Often explains failures quickly +4. For THE MOST CRITICAL failed operator only (not all): + * pods_list_in_namespace: Find operator pods + * pods_log: Get last ${PROMPT_TIMEOUT_LOG_TAIL_LINES} lines only (not ${PROMPT_TIMEOUT_LOG_TAIL_LINES_FULL}) - Logs can be slow! +**PHASE 3 - OPTIONAL CONTEXT (Only if under ${PROMPT_TIMEOUT_WARNING_THRESHOLD} seconds):** +5. resources_list: Node - Check for NotReady nodes +6. get_alerts: Critical alerts (if available) +7. Additional operator logs (only if time permits) +**CRITICAL EFFICIENCY RULES:** +- LIMIT pod log fetching to 1-2 critical operators max - logs are SLOW +- Use tail=${PROMPT_TIMEOUT_LOG_TAIL_LINES} for logs, not tail=${PROMPT_TIMEOUT_LOG_TAIL_LINES_FULL} - faster retrieval +- If events_list provides the error, SKIP pod logs - events are faster +- NEVER exceed ${PROMPT_TIMEOUT_MAX_EXECUTION} seconds total execution time +- Provide analysis with partial data rather than timing out + +- Analyze ONLY the actual data from tool calls +- Report SPECIFIC failure details with actual error messages from logs and events +- Provide conservative, investigation-focused remediation +- Focus on root cause identification using real error messages, not aggressive fixes +- ONLY OUTPUT the Summary and TL;DR sections +${languageConstraint} + + + +Troubleshoot upgrade issues for cluster attempting to go from ${currentVersion} to ${desiredVersion}. You have complete cluster data including ClusterVersion and all ClusterOperator resources to diagnose upgrade failures. +This prompt is used when upgrade failures or component degradation is detected. + + + + CRITICAL: Understanding Kubernetes/OpenShift Conditions + +Conditions have TWO important fields you MUST check: +- **type**: The name of the condition (e.g., "Failing", "Available", "Progressing") +- **status**: The state of the condition (ONLY these values: "True", "False", or "Unknown") +**MANDATORY CHECKING PROCESS:** +For EVERY condition you analyze, you MUST: +1. First, locate the condition by its type field +2. Second, read the EXACT value of the status field +3. Third, interpret based ONLY on the status field value: + - If status="True" → The condition IS active/present + - If status="False" → The condition is NOT active/NOT present + - If status="Unknown" → The condition state is uncertain +**DO NOT report a problem unless status="True" for negative conditions OR status="False" for positive conditions!** +**Critical Examples - MEMORIZE THESE:** +- {type: "Failing", status: "False"} → Cluster is NOT failing → NO PROBLEM +- {type: "Failing", status: "True"} → Cluster IS failing → PROBLEM +- {type: "Available", status: "True"} → Cluster IS available → NO PROBLEM +- {type: "Available", status: "False"} → Cluster is NOT available → PROBLEM +- {type: "Degraded", status: "False"} → Cluster is NOT degraded → NO PROBLEM +- {type: "Degraded", status: "True"} → Cluster IS degraded → PROBLEM +**VERIFICATION REQUIREMENT:** +Before making ANY conclusion about a condition, you MUST explicitly state: +"Condition type='X' has status='Y'" and then interpret it correctly. +**NEVER assume a condition is true just because the type exists - ALWAYS check the status field!** +**The presence of a condition type does NOT mean it is active - check the status field!** + + + + +1. **Upgrade Failure Root Cause**: + - Find condition where type="Failing" AND status="True" + - Extract the EXACT reason and message from the Failing condition + - Check status.history for failed upgrade attempts and their specific errors + - Identify which component or process is actually failing + +2. **ClusterOperator Failure Analysis with Pod Logs** (Check BOTH type AND status): + - For each ClusterOperator, check conditions: + * Available: If type="Available" AND status="False" → Operator unavailable (blocker) + * Degraded: If type="Degraded" AND status="True" → Operator degraded (issue) + * Progressing: If type="Progressing" AND status="True" with error messages → Operator stuck + - Report SPECIFIC operator names and their condition messages for problematic conditions only +**For each failing/degraded operator, fetch pod logs:** + - Use pods_list_in_namespace to find operator's pods (usually in openshift-[operator-name] namespace) + - Use pods_log with tail=${PROMPT_TIMEOUT_LOG_TAIL_LINES} to get recent logs from failing pods + - If pod has restarted, also get previous container logs + - **Extract actual error messages from logs**- don't just say "check logs" + - **Translate technical errors into user-friendly explanations** + - Example: "Error: dial tcp 10.0.0.1:6443: i/o timeout" → "Operator cannot connect to API server - network connectivity issue" + +3. **Cluster-Level Failure Analysis** (Check BOTH type AND status): + - Find condition where type="Failing" AND status="True" - extract specific error messages + - Find condition where type="Degraded" AND status="True" - review degradation reasons + - Find condition where type="Invalid" AND status="True" - check invalid configuration + - Look for specific failure reasons in condition messages and status + - IMPORTANT: Only report as failing if status="True" + +4. **Node and Infrastructure Issues**: + - Check Node resources for NotReady conditions + - Identify nodes with scheduling issues or resource constraints + - Look for infrastructure problems affecting the upgrade + +5. **MachineConfigPool Issues**: + - Check for Degraded=True, spec.paused=true, or observedGeneration ≠ metadata.generation + - These can cause upgrade failures and node configuration problems + +6. **Historical Failure Context**: + - Previous upgrade attempts from status.history + - Compare current failure with historical upgrade patterns + - Identify recurring issues or new problems + - Duration and frequency of past upgrade attempts + +7. **Update Target Analysis for Failures**: + - Failed target version from status.desired.version + - Release metadata and known issues from status.desired.url + - Target channel information from status.desired.channels + - Validate if target version is still available and supported + +8. **Cincinnati and Update Service Analysis**: + - Update service configuration (spec.upstream if custom, otherwise default Red Hat service) + - Recent update retrieval status from RetrievedUpdates condition + - Verify availableUpdates is populated (indicates service connectivity) + - Signature verification status (spec.signatureStores if custom, otherwise default Red Hat stores) + - Network connectivity issues affecting update process + +9. **Failure Events Timeline** (using events_list): + - Query events from last 1 hour (upgrade failures develop over time) + - Focus on Error and Warning events in openshift-* namespaces + - Look for event patterns that explain the failure: + * CrashLoopBackOff → Operator pod keeps restarting + * ImagePullBackOff → Cannot download container images + * OOMKilled → Pod ran out of memory + * FailedScheduling → Cannot place pods on nodes + - **Build a timeline**: Show sequence of events leading to failure + - **User-friendly translation**: Explain technical events in plain language + - **Example**: "10 minutes ago: authentication operator pod started crashing (CrashLoopBackOff). 5 minutes ago: authentication unavailable. Now: upgrade blocked" + +10. **Active Critical Alerts** (using get_alerts - if available): + - Query critical alerts that might explain upgrade failure + - Focus on infrastructure and operator alerts + - **Correlation**: Connect alerts to failing operators + - **Example**: "KubeAPIDown alert firing - explains why operators can't communicate" + - If get_alerts not available: Skip this check + +11. **Conservative Remediation Approach**: + - Focus on investigation and monitoring first + - Suggest checking logs and status before taking action + - Avoid aggressive suggestions like "restart operators" unless clearly needed + - Recommend escalation paths for complex issues + - Consider rollback strategies based on failure severity + + + + +## Summary +**Root Cause Analysis** +Based on the ClusterVersion data: +- **Current Version**: ${currentVersion} +- **Target Version**: ${desiredVersion} +- **Failure Type**: [Extract from actual Failing condition reason] +- **Specific Error**: [Quote the actual failure message from conditions] +**Component Analysis** +- **Failed ClusterOperators**: [List specific operators with Available=False, Degraded=True, or failing conditions] +- **Operator Error Details**: [Actual error messages from pod logs - be specific!] + - Example: "authentication operator pod logs show: 'Error: certificate expired at 2026-04-15 12:00:00 UTC'" +- **Stuck ClusterOperators**: [List operators stuck in Progressing=True with error messages] +- **Affected Services**: [Impact on cluster functionality based on failed operators] +**Failed Upgrade Context** +- **Target Version**: [From status.desired.version with metadata] +- **Release Information**: [Target release details and known issues from status.desired.url] +- **Upgrade Path**: [Source → Target version progression] +- **Target Availability**: [Verify target version is still in available updates] +**Historical Failure Analysis** +- **Previous Attempts**: [Recent upgrade attempts from status.history] +- **Failure Pattern**: [Recurring vs new failure based on history] +- **Last Successful Upgrade**: [Most recent completed upgrade for comparison] +- **Cluster Stability**: [Overall upgrade success rate and patterns] +**Update Service Health** +- **Service Configuration**: [spec.upstream if custom, otherwise "Default Red Hat service"] +- **Cincinnati Status**: [RetrievedUpdates condition status and message] +- **Last Update Check**: [Recent update retrieval timestamp from RetrievedUpdates] +- **Available Updates**: [Confirm availableUpdates array is populated] +- **Connectivity Issues**: [Network or authentication problems affecting updates] +**Failure Events Timeline** (Last hour): +- **Event Summary**: [Count of error vs warning events] +- **Timeline of Key Events**: [Chronological sequence showing how failure developed] + - Example: "60 min ago: Started upgrade to 4.21.7" + - Example: "45 min ago: authentication operator pod started failing (CrashLoopBackOff)" + - Example: "30 min ago: authentication operator marked Degraded" + - Example: "Now: Upgrade stuck, authentication unavailable" +- **Technical Errors Found**: [Specific error types: ImagePullBackOff, OOMKilled, etc.] +- **User-Friendly Explanation**: [What these events mean in plain language] +**Active Critical Alerts** (if available): +- **Alert Count**: [Number of critical/warning alerts] +- **Key Alerts**: [Names and descriptions of alerts related to failure] +- **Correlation**: [How alerts connect to failing operators] +- **Example**: "KubeAPIDown alert + authentication operator failure → API server connectivity issue" +- If alerts not available: "Alert monitoring unavailable" +**Investigation Steps** +1. [First diagnostic step based on actual failure type] +2. [Second diagnostic step] +3. [Log locations to check] +**Recovery Actions** (Conservative Approach) +1. [Investigation-focused first step] +2. [Monitoring and validation steps] +3. [When to escalate to support] + +## TL;DR +- **Failure Type**: [Specific failure reason from conditions] +- **Target Version**: [Failed upgrade target with release info] +- **Root Cause**: [Primary component or process failing - with actual error from logs] +- **Failed Components**: [Count and names of failed ClusterOperators] +- **Error Messages**: [Key errors from pod logs - be specific!] +- **Event Summary**: [Count of error events in last hour, key patterns] +- **Alert Status**: [Critical alerts related to failure, if available] +- **Historical Pattern**: [Recurring failure vs new issue] +- **Last Success**: [Most recent completed upgrade for context] +- **Update Service**: [Cincinnati health, e.g., "Default service working (RetrievedUpdates=True)" or "Custom upstream failing"] +- **Node Issues**: [Count of NotReady nodes if any] +- **Infrastructure Problems**: [Any detected infrastructure issues] +- **MCP Issues**: [Count of degraded MachineConfigPools if any] +- **Next Steps**: [Conservative investigation approach based on actual errors found] +- **Escalation**: [When to contact Red Hat support] +- **Recovery Time**: [Realistic estimate based on failure type] +`; +}; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/types.ts b/frontend/packages/console-shared/src/components/cluster-updates/types.ts new file mode 100644 index 00000000000..41f66eb8f12 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/types.ts @@ -0,0 +1,78 @@ +import type { TFunction } from 'i18next'; +import type { Alert } from '@console/dynamic-plugin-sdk'; +import type { + K8sResourceCommon, + ClusterVersionKind, + ClusterOperator, +} from '@console/internal/module/k8s'; + +/** + * Cluster Update specific OLS workflow types + */ + +/** + * Machine Config Pool resource type + * Subset of MachineConfigPoolKind with fields used by cluster update workflows + * Extends K8sResourceCommon to inherit standard metadata, apiVersion, and kind fields + */ +export type MachineConfigPool = K8sResourceCommon & { + spec?: { + configuration?: { + name?: string; + }; + nodeSelector?: { + matchLabels?: Record; + }; + paused?: boolean; + }; + status?: { + conditions?: { + type: string; + status: string; + reason?: string; + message?: string; + lastTransitionTime?: string; + }[]; + machineCount?: number; + readyMachineCount?: number; + updatedMachineCount?: number; + degradedMachineCount?: number; + unavailableMachineCount?: number; + observedGeneration?: number; + }; +}; + +// OLS workflow context - specific to cluster update workflows +interface OLSWorkflowContext { + t: TFunction; + data: T; +} + +/** + * Update workflow phases for OLS integration + * + * The workflow has 2 primary phases: + * - 'status': Provides real-time update progress monitoring. Automatically adapts + * its prompt based on cluster state (troubleshooting for failures, progress + * monitoring for in-progress updates). + * - 'pre-check': Pre-update validation and readiness assessment before initiating + * an update. Helps users understand prerequisites and requirements. + * + * Note: While the 'status' phase dynamically handles multiple scenarios (failure + * analysis, progress tracking, success validation), it is still a single phase + * from a type system perspective. + */ +export type UpdateWorkflowPhase = 'status' | 'pre-check'; + +export interface UpdateWorkflowContext extends OLSWorkflowContext { + phase: UpdateWorkflowPhase; + cv: ClusterVersionKind; + clusterOperators?: ClusterOperator[]; + machineConfigPools?: MachineConfigPool[]; + alerts?: Alert[]; +} + +export interface UpdateWorkflowConfig { + prompt: (context: UpdateWorkflowContext) => string; + buttonText: (t: TFunction) => string; +} diff --git a/frontend/packages/console-shared/src/components/cluster-updates/workflow-configs.ts b/frontend/packages/console-shared/src/components/cluster-updates/workflow-configs.ts new file mode 100644 index 00000000000..75201274f6e --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/workflow-configs.ts @@ -0,0 +1,179 @@ +import type { ClusterOperator } from '@console/internal/module/k8s'; +import { semVerComparator } from '@console/shared/src/utils/comparators'; +import { getCurrentVersion, getDesiredVersion } from './cluster-version-helpers'; +import { CLUSTER_OPERATOR_CONDITION_PROGRESSING, CONDITION_STATUS_TRUE } from './constants'; +import { + isClusterFailing, + isClusterInvalid, + hasUpdateRetrievalFailure, + hasReleaseAcceptanceFailure, + hasAnyOperatorIssues, + hasOperatorIssues, +} from './predicates'; +import { createPreCheckPrompt } from './prompts/precheck'; +import { createPreCheckNoUpdatesPrompt } from './prompts/precheck-no-updates'; +import { createPreCheckSpecificVersionPrompt } from './prompts/precheck-specific'; +import { createProgressPrompt } from './prompts/progress'; +import { createTroubleshootPrompt } from './prompts/troubleshoot'; +import type { UpdateWorkflowPhase, UpdateWorkflowConfig, UpdateWorkflowContext } from './types'; + +/** + * Update workflow configurations for different phases + * + * The status workflow intelligently chooses between progress monitoring and troubleshooting: + * - Uses createProgressPrompt when upgrade is progressing without failures + * - Uses createTroubleshootPrompt when failures are detected (cluster-level or operator-level) + * + * Decision Logic: + * - Cluster-level failures: Failing=True, Invalid=True, RetrievedUpdates=False, ReleaseAccepted=False + * - Operator-level failures: Any operator with Available=False OR Degraded=True + * - Priority: Failures take precedence over progress (troubleshoot even if Progressing=True) + * + * See __tests__/cluster-state-matrix.spec.ts for complete state matrix and test coverage. + */ + +const createStatusWorkflow = (): UpdateWorkflowConfig => ({ + buttonText: (t) => t('public~Update status'), + + prompt: ({ cv, clusterOperators }: UpdateWorkflowContext) => { + const currentVersion = getCurrentVersion(cv); + const desiredVersion = getDesiredVersion(cv); + + // Check if there are failure conditions that should trigger troubleshoot prompt + // IMPORTANT: Always check the status field, not just the condition type! + // Example: {type: "Failing", status: "False"} means NOT failing (healthy) + const failing = isClusterFailing(cv); + const invalid = isClusterInvalid(cv); + const retrievedUpdates = hasUpdateRetrievalFailure(cv); + const releaseAccepted = hasReleaseAcceptanceFailure(cv); + + // Operator-level failure detection + // Check for operators that are unavailable (Available=False) or degraded (Degraded=True) + const operatorIssues = hasAnyOperatorIssues(clusterOperators); + + // Determine if troubleshooting is needed + // Failures take precedence: even if Progressing=True, we troubleshoot if failures detected + const hasFailures = failing || invalid || retrievedUpdates || releaseAccepted || operatorIssues; + + if (hasFailures) { + // Use troubleshoot prompt for any failure scenario: + // - Upgrade failing (Progressing=True + failures) + // - Cluster failing (Failing=True, not progressing) + // - Operator issues (degraded/unavailable operators) + // - Update service issues (cannot retrieve updates or verify releases) + return createTroubleshootPrompt(currentVersion, desiredVersion); + } + + // Helper function to extract operator version + const getOperatorVersion = (operator: ClusterOperator): string | null => { + const versions = operator.status?.versions || []; + // Find the "operator" version entry first + const operatorVersion = versions.find((v) => v.name === 'operator'); + if (operatorVersion?.version) { + return operatorVersion.version; + } + // Fallback: find the highest version among all entries using semantic version comparison + const sortedVersions = versions + .filter((v) => v.version) + .sort((a, b) => semVerComparator(b.version || '', a.version || '')); // Descending order (highest first) + return sortedVersions[0]?.version || null; + }; + + // Calculate operator status counts + const total = clusterOperators?.length || 0; + + // Failed operators: Available=False OR Degraded=True + const failed = clusterOperators?.filter(hasOperatorIssues).length || 0; + + // Updated operators: Current version equals target version + const updated = + clusterOperators?.filter((operator) => { + const operatorVersion = getOperatorVersion(operator); + return operatorVersion === desiredVersion; + }).length || 0; + + // Updating operators: Current version < target AND Progressing=True + const updating = + clusterOperators?.filter((operator) => { + const operatorVersion = getOperatorVersion(operator); + const operatorConditions = operator.status?.conditions || []; + const progressing = operatorConditions.find( + (c) => + c.type === CLUSTER_OPERATOR_CONDITION_PROGRESSING && c.status === CONDITION_STATUS_TRUE, + ); + return operatorVersion && operatorVersion !== desiredVersion && progressing; + }).length || 0; + + // Pending operators: Current version < target AND Progressing=False + const pending = + clusterOperators?.filter((operator) => { + const operatorVersion = getOperatorVersion(operator); + const operatorConditions = operator.status?.conditions || []; + const progressing = operatorConditions.find( + (c) => + c.type === CLUSTER_OPERATOR_CONDITION_PROGRESSING && c.status === CONDITION_STATUS_TRUE, + ); + return operatorVersion && operatorVersion !== desiredVersion && !progressing; + }).length || 0; + + const operatorCounts = { total, updated, updating, pending, failed }; + + // Otherwise use normal progress prompt + return createProgressPrompt(currentVersion, desiredVersion, operatorCounts); + }, +}); + +/** + * Pre-check workflow intelligently chooses the appropriate readiness assessment: + * - createPreCheckNoUpdatesPrompt: When no updates available (cluster fully updated) + * - createPreCheckSpecificVersionPrompt: When user has selected a specific target version + * - createPreCheckPrompt: General readiness check when updates are available + * + * The pre-check workflow is shown ONLY when cluster is healthy: + * - Failing=False (cluster not failing) + * - Progressing=False (no upgrade in progress) + * - No operator issues (all operators Available=True, Degraded=False) + * - No service issues (RetrievedUpdates working, ReleaseAccepted working) + */ +const createPreCheckWorkflow = (): UpdateWorkflowConfig => ({ + buttonText: (t) => t('public~Pre-check with AI'), + + prompt: ({ cv }: UpdateWorkflowContext) => { + const currentVersion = getCurrentVersion(cv); + const hasAvailableUpdates = (cv.status?.availableUpdates?.length || 0) > 0; + + // Check if a specific version is selected for update + const desiredVersion = cv.status?.desired?.version; + const currentDesiredVersion = cv.status?.history?.[0]?.version; + const hasSpecificVersionSelected = desiredVersion && desiredVersion !== currentDesiredVersion; + + if (!hasAvailableUpdates) { + // No updates available - perform health assessment + // Verifies cluster is up-to-date and operationally healthy + return createPreCheckNoUpdatesPrompt(currentVersion); + } + if (hasSpecificVersionSelected) { + // Specific version selected - assess readiness for that version + // Validates version is available and checks version-specific compatibility + return createPreCheckSpecificVersionPrompt(currentVersion, desiredVersion); + } + // Updates available - general readiness assessment + // Lists available updates and checks for any upgrade blockers + return createPreCheckPrompt(currentVersion); + }, +}); + +/** + * Registry of update workflow configurations + * @public + */ +export const updateWorkflowConfigs: Record = { + status: createStatusWorkflow(), + 'pre-check': createPreCheckWorkflow(), +}; + +/** + * Get workflow configuration for a specific phase + */ +export const getUpdateWorkflowConfig = (phase: UpdateWorkflowPhase): UpdateWorkflowConfig => + updateWorkflowConfigs[phase]; diff --git a/frontend/packages/console-shared/src/components/cluster-updates/workflow-utils.ts b/frontend/packages/console-shared/src/components/cluster-updates/workflow-utils.ts new file mode 100644 index 00000000000..d523c7dfbb6 --- /dev/null +++ b/frontend/packages/console-shared/src/components/cluster-updates/workflow-utils.ts @@ -0,0 +1,168 @@ +import type { TFunction } from 'i18next'; +import type { Alert } from '@console/dynamic-plugin-sdk'; +import type { ClusterVersionKind, ClusterOperator } from '@console/internal/module/k8s'; +import { getDesiredClusterVersion } from '@console/internal/module/k8s'; +import { + isClusterFailing, + isClusterInvalid, + isClusterProgressing, + hasUpdateRetrievalFailure, + hasReleaseAcceptanceFailure, + hasAnyOperatorIssues, +} from './predicates'; +import { createPreCheckSpecificVersionPrompt } from './prompts/precheck-specific'; +import type { UpdateWorkflowPhase, UpdateWorkflowContext, MachineConfigPool } from './types'; +import { getUpdateWorkflowConfig } from './workflow-configs'; + +/** + * Utility functions for cluster update workflows + * + * This module provides the core decision logic for determining which OLS workflow + * and prompt to use based on cluster state. + */ + +/** + * Generate OLS prompt for a specific update workflow phase + */ +export const generateUpdatePrompt = ( + phase: UpdateWorkflowPhase, + cv: ClusterVersionKind, + t: TFunction, + clusterOperators?: ClusterOperator[], + machineConfigPools?: MachineConfigPool[], + alerts?: Alert[], + targetVersion?: string, +): string => { + // For pre-check phase with target version, use specific version prompt + if (phase === 'pre-check' && targetVersion) { + const currentVersion = getDesiredClusterVersion(cv); + return createPreCheckSpecificVersionPrompt(currentVersion, targetVersion); + } + + // Otherwise use the default workflow configuration + const context: UpdateWorkflowContext = { + phase, + cv, + clusterOperators, + machineConfigPools, + alerts, + t, + data: cv, + }; + const config = getUpdateWorkflowConfig(phase); + return config.prompt(context); +}; + +/** + * Get button text for a specific update workflow phase + */ +export const getUpdateButtonText = (phase: UpdateWorkflowPhase, t: TFunction): string => { + const config = getUpdateWorkflowConfig(phase); + return config.buttonText(t); +}; + +/** + * Get button translation key for a specific update workflow phase + * Extracts the translation key from workflow configurations for use with OLSButton + */ +export const getUpdateButtonTranslationKey = (phase: UpdateWorkflowPhase): string => { + // Translation keys that match the keys used in workflow-configs.ts buttonText functions + const keys: Record = { + status: 'public~Update status', + 'pre-check': 'public~Pre-check with AI', + }; + return keys[phase]; +}; + +/** + * Check if cluster has available updates + */ +export const hasAvailableUpdates = (cv: ClusterVersionKind): boolean => + (cv.status?.availableUpdates?.length || 0) > 0; + +/** + * Check if there are any degraded or unavailable cluster operators + * Re-exported from predicates for convenience + */ +export const hasOperatorIssues = hasAnyOperatorIssues; + +/** + * Determine the appropriate workflow phase based on cluster version status and operator conditions + */ +export const determineWorkflowPhase = ( + cv: ClusterVersionKind, + clusterOperators?: ClusterOperator[], +): UpdateWorkflowPhase => { + // Check for failure conditions or progressing condition - both show status button + // The status workflow will automatically use troubleshoot prompt for failures + const failing = isClusterFailing(cv); + const invalid = isClusterInvalid(cv); + const retrievedUpdates = hasUpdateRetrievalFailure(cv); + const releaseAccepted = hasReleaseAcceptanceFailure(cv); + const progressing = isClusterProgressing(cv); + + // Check for operator-level issues + const operatorIssues = hasAnyOperatorIssues(clusterOperators); + + // Show status button for any of these conditions: + // - Cluster is failing (will auto-switch to troubleshoot prompt) + // - Cluster is progressing (will use progress prompt) + // - There are operator issues (will auto-switch to troubleshoot prompt) + if (failing || invalid || retrievedUpdates || releaseAccepted || operatorIssues || progressing) { + return 'status'; + } + + // If cluster is healthy (not failing, not progressing), show pre-check + return 'pre-check'; +}; + +/** + * Determine which workflow buttons to show based on cluster state and operator conditions + */ +export const determineWorkflowButtons = ( + cv: ClusterVersionKind, + clusterOperators?: ClusterOperator[], +): { + showStatus: boolean; + showPreCheck: boolean; +} => { + const phase = determineWorkflowPhase(cv, clusterOperators); + + // Show status button when cluster is failing or progressing + // The status workflow will automatically switch between progress and troubleshoot prompts + if (phase === 'status') { + return { showStatus: true, showPreCheck: false }; + } + + // Show pre-check button when cluster is healthy (not failing, not progressing) + if (phase === 'pre-check') { + return { showStatus: false, showPreCheck: true }; + } + + // Default behavior: No buttons shown + return { showStatus: false, showPreCheck: false }; +}; + +/** + * Extract risk information from conditional updates + * Parses the conditions to extract human-readable risk descriptions + */ +export const getConditionalUpdateRisks = ( + cv: ClusterVersionKind, +): { + version: string; + risks: { reason: string; message: string }[]; +}[] => { + const conditionalUpdates = cv.status?.conditionalUpdates || []; + + return conditionalUpdates.map((update) => ({ + version: update.release.version || 'Unknown', + risks: + update.conditions + ?.filter((c) => c.status === 'False' && c.type === 'Recommended') + .map((c) => ({ + reason: c.reason || 'UnknownRisk', + message: c.message || 'No details available', + })) || [], + })); +}; diff --git a/frontend/packages/console-shared/src/components/dashboard/inventory-card/InventoryItem.tsx b/frontend/packages/console-shared/src/components/dashboard/inventory-card/InventoryItem.tsx index 40b4fae4a18..4857dd8270b 100644 --- a/frontend/packages/console-shared/src/components/dashboard/inventory-card/InventoryItem.tsx +++ b/frontend/packages/console-shared/src/components/dashboard/inventory-card/InventoryItem.tsx @@ -9,9 +9,8 @@ import { import { InProgressIcon, QuestionCircleIcon } from '@patternfly/react-icons'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; -import type { DashboardsInventoryItemGroup } from '@console/dynamic-plugin-sdk'; import { useResolvedExtensions, isDashboardsInventoryItemGroup } from '@console/dynamic-plugin-sdk'; -import type { ResolvedExtension } from '@console/dynamic-plugin-sdk/dist/core/lib/types'; +import type { DashboardsInventoryItemGroup, ResolvedExtension } from '@console/dynamic-plugin-sdk'; import type { ResourceInventoryItemProps } from '@console/dynamic-plugin-sdk/src/api/internal-types'; import { pluralize } from '@console/internal/components/utils/details-page'; import { resourcePathFromModel } from '@console/internal/components/utils/resource-link'; diff --git a/frontend/packages/console-shared/src/utils/comparators.ts b/frontend/packages/console-shared/src/utils/comparators.ts index 74676d61232..c43ac7c435f 100644 --- a/frontend/packages/console-shared/src/utils/comparators.ts +++ b/frontend/packages/console-shared/src/utils/comparators.ts @@ -1,4 +1,4 @@ -import * as SemVer from 'semver'; +import * as semver from 'semver'; /** * A null safe wrapper for String.localeCompare. Sorts strings alphabetically in ascending order, @@ -12,17 +12,22 @@ export const localeComparator: Comparator = (a, b) => (a || '').localeCo export const boolComparator: Comparator = (a, b) => (a ? 0 : 1) - (b ? 0 : 1); /** - * Wrapper for SemVer.compare function. Sorts semver strings in ascending order. Invalid semver - * strings will be sorted last. + * Compares two semantic version strings. Sorts in ascending order. + * Returns -1 if a < b, 0 if a === b, 1 if a > b. + * Falls back to string comparison if semver parsing fails. */ -export const semVerComparator: Comparator = (a, b) => SemVer.compare(a, b); +export const semVerComparator: Comparator = (a, b) => { + const aVersion = semver.parse(a); + const bVersion = semver.parse(b); -/** - * Same as semVerCompare, but is more forgiving for not-quite-valid semver strings. Sorts strings in - * ascending order based on the loosely interpreted semver value. - */ -export const looseSemVerComparator: Comparator = (a, b) => - SemVer.compare(a, b, true); + if (!aVersion && !bVersion) { + return (a || '').localeCompare(b || ''); + } + if (!aVersion) return 1; + if (!bVersion) return -1; + + return semver.compare(aVersion, bVersion); +}; /** * A null safe function that can be passed directly to Array.prototype.sort. diff --git a/frontend/public/components/cluster-settings/cluster-settings.tsx b/frontend/public/components/cluster-settings/cluster-settings.tsx index 8319f04be78..33f5d4ffce9 100644 --- a/frontend/public/components/cluster-settings/cluster-settings.tsx +++ b/frontend/public/components/cluster-settings/cluster-settings.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import type { FC, ReactNode } from 'react'; -import { useEffect, useRef, useMemo } from 'react'; +import { useEffect, useRef, useMemo, useState } from 'react'; import * as _ from 'lodash'; import { css } from '@patternfly/react-styles'; import * as semver from 'semver'; @@ -8,6 +8,11 @@ import { Alert, AlertActionLink, Button, + Card, + CardHeader, + CardTitle, + CardBody, + CardExpandableContent, Flex, FlexItem, Label, @@ -21,11 +26,26 @@ import { DescriptionListTerm, DescriptionListDescription, DescriptionListGroup, + Stack, + StackItem, } from '@patternfly/react-core'; import { Link } from 'react-router'; import { useTranslation } from 'react-i18next'; -import { AddCircleOIcon, PauseCircleIcon, PencilAltIcon } from '@patternfly/react-icons'; +import { + RhUiAddCircleIcon, + RhUiPauseCircleIcon, + RhUiEditIcon, + RhUiInProgressIcon, +} from '@patternfly/react-icons'; + +import { UpdateWorkflowOLSButton } from '@console/shared/src/components/cluster-updates/explain-button'; +import { + hasAvailableUpdates, + hasOperatorIssues, + determineWorkflowButtons, +} from '@console/shared/src/components/cluster-updates/workflow-utils'; +import { FEATURE_FLAG_LIGHTSPEED_PLUGIN } from '@console/shared/src/components/cluster-updates/constants'; import { useQueryParamsMutator } from '@console/shared/src/hooks/useQueryParamsMutator'; import { MarkdownView } from '@console/shared/src/components/markdown/MarkdownView'; @@ -33,7 +53,7 @@ import { ClusterServiceVersionKind, ClusterServiceVersionModel, } from '@console/operator-lifecycle-manager'; -import { WatchK8sResource } from '@console/dynamic-plugin-sdk'; +import { WatchK8sResource, useAccessReview } from '@console/dynamic-plugin-sdk'; import PaneBody from '@console/shared/src/components/layout/PaneBody'; import PaneBodyGroup from '@console/shared/src/components/layout/PaneBodyGroup'; @@ -56,6 +76,7 @@ import { clusterIsUpToDateOrUpdateAvailable, ClusterOperator, ClusterUpdateStatus, + ClusterVersionConditionType, ClusterVersionKind, clusterVersionReference, getClusterID, @@ -99,7 +120,7 @@ import { ResourceLink, resourcePathFromModel } from '../utils/resource-link'; import { SectionHeading } from '../utils/headings'; import { togglePaused } from '../utils/workload-pause'; import { UpstreamConfigDetailsItem } from '../utils/details-page'; -import { useAccessReview } from '../utils/rbac'; + import { Timestamp } from '@console/shared/src/components/datetime/Timestamp'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; import { @@ -119,10 +140,11 @@ import { ServiceLevelText, ServiceLevelLoading, } from '../utils/service-level'; -import { hasAvailableUpdates, hasNotRecommendedUpdates } from '../../module/k8s/cluster-settings'; +import { hasNotRecommendedUpdates } from '../../module/k8s/cluster-settings'; import { UpdateStatus } from './cluster-status'; import { ErrorModal } from '../modals/error-modal'; import { useOverlay } from '@console/dynamic-plugin-sdk/src/app/modal-support/useOverlay'; +import { getGroupVersionKindForModel } from '@console/dynamic-plugin-sdk/src/utils/k8s/k8s-ref'; export const clusterAutoscalerReference = referenceForModel(ClusterAutoscalerModel); @@ -161,20 +183,13 @@ const getUpdatedOperatorsCount = ( ); }; -const getReleaseImageVersion = (obj: K8sResourceKind): string => { - return obj?.metadata?.annotations?.['machineconfiguration.openshift.io/release-image-version']; -}; - -const calculatePercentage = (numerator: number, denominator: number): number => - Math.round((numerator / denominator) * 100); - -export const CurrentChannel: FC = ({ cv, canUpgrade }) => { - const { t } = useTranslation(); +const CurrentChannel: FC = ({ cv, canUpgrade }) => { + const { t } = useTranslation('public'); const launchModal = useOverlay(); const label = cv.spec.channel || t('public~Not configured'); return canUpgrade ? ( + + ); + } + return canUpgrade && (hasAvailableUpdates(cv) || hasNotRecommended) && - (status === ClusterUpdateStatus.ErrorRetrieving || - status === ClusterUpdateStatus.Failing || - status === ClusterUpdateStatus.UpdatesAvailable || + (status === ClusterUpdateStatus.UpdatesAvailable || status === ClusterUpdateStatus.Updating || + status === ClusterUpdateStatus.Failing || (status === ClusterUpdateStatus.UpToDate && hasNotRecommended)) && workerMachineConfigPoolIsEditable ? (
@@ -481,7 +515,10 @@ export const NodesUpdatesGroup: FC = ({ }); const isMaster = isMCPMaster(machineConfigPool); const isPaused = isMCPPaused(machineConfigPool); - const renderedConfigIsUpdated = getReleaseImageVersion(renderedConfig) === desiredVersion; + const renderedConfigIsUpdated = + renderedConfig?.metadata?.annotations?.[ + 'machineconfiguration.openshift.io/release-image-version' + ] === desiredVersion; const MCOIsUpdated = getClusterOperatorVersion(machineConfigOperator) === desiredVersion; const MCPisUpdated = machineConfigPool?.status?.conditions?.some( (c) => c.type === 'Updated' && c.status === K8sResourceConditionStatus.True, @@ -493,7 +530,7 @@ export const NodesUpdatesGroup: FC = ({ updatedMachineCountReady || (MCPUpdatingTime > updateStartedTime && renderedConfigIsUpdated) ? machineConfigPool?.status?.updatedMachineCount : 0; - const percentMCPNodes = calculatePercentage(updatedMCPNodes, totalMCPNodes); + const percentMCPNodes = Math.round((updatedMCPNodes / totalMCPNodes) * 100); const isUpdated = percentMCPNodes === 100; const nodeRoleFilterValue = isMaster ? 'control-plane' : mcpName; const { t } = useTranslation(); @@ -690,7 +727,7 @@ export const UpdateInProgress: FC = ({ const [clusterOperators] = useK8sWatchResource(ClusterOperatorsResource); const totalOperatorsCount = clusterOperators?.length || 0; const updatedOperatorsCount = getUpdatedOperatorsCount(clusterOperators, desiredVersion); - const percentOperators = calculatePercentage(updatedOperatorsCount, totalOperatorsCount); + const percentOperators = Math.round((updatedOperatorsCount / totalOperatorsCount) * 100); const masterMachinePoolConfig = getMCPByName(machineConfigPools, NodeTypes.master); const { t } = useTranslation(); @@ -745,6 +782,321 @@ const ClusterServiceVersionResource: WatchK8sResource = { kind: referenceForModel(ClusterServiceVersionModel), }; +// Helper function to get a condition by type from cluster version +const getConditionOfType = (cv: ClusterVersionKind, type: ClusterVersionConditionType) => + cv.status?.conditions?.find((c) => c.type === type); + +// Helper function to detect operator issues (degraded or unavailable) +// Returns array of operator details for operators with problems +const detectOperatorIssues = ( + clusterOperators?: ClusterOperator[], +): Array<{ name: string; issue: string; condition: any }> => { + if (!clusterOperators || clusterOperators.length === 0) { + return []; + } + + return clusterOperators + .map((operator) => { + const operatorConditions = operator.status?.conditions || []; + const degraded = operatorConditions.find((c) => c.type === 'Degraded' && c.status === 'True'); + const available = operatorConditions.find( + (c) => c.type === 'Available' && c.status === 'False', + ); + + if (degraded) { + return { + name: operator.metadata?.name || 'unknown', + issue: 'degraded', + condition: degraded, + }; + } + if (available) { + return { + name: operator.metadata?.name || 'unknown', + issue: 'not available', + condition: available, + }; + } + return null; + }) + .filter(Boolean); +}; + +// Helper function to parse and improve error messages for better user experience +const parseUpdateFailureMessage = ( + rawMessage: string, + t: (key: string, options?: { [key: string]: string | number }) => string, + cv?: ClusterVersionKind, + clusterOperators?: ClusterOperator[], +): { title: string; message: string; operatorDetails?: string } => { + // Check for operator-specific failures first, regardless of rawMessage + // This ensures we always provide detailed operator information when available + if (cv && clusterOperators) { + const operatorIssueDetails = detectOperatorIssues(clusterOperators); + + // If we have operator issues, return operator-specific message with technical details + if (operatorIssueDetails.length > 0) { + const operatorList = operatorIssueDetails + .map((detail) => `${detail.name} (${detail.issue})`) + .join(', '); + + const baseMessage = t( + 'public~{{count}} cluster operators are experiencing issues and need to be healthy before the cluster can be updated.', + { count: operatorIssueDetails.length }, + ); + + // Build detailed operator condition information for technical details + const operatorDetailsMarkdown = operatorIssueDetails + .map((detail) => { + const conditionInfo = detail.condition.message + ? `\n **Message**: ${detail.condition.message}` + : ''; + const conditionReason = detail.condition.reason + ? `\n **Reason**: ${detail.condition.reason}` + : ''; + return `**${detail.name}**\n **Status**: ${detail.condition.type}=${detail.condition.status}${conditionReason}${conditionInfo}`; + }) + .join('\n\n'); + + return { + title: t('public~Cluster operators are experiencing issues'), + message: t( + 'public~{{baseMessage}}\n\nAffected operators: {{operators}}\n\nCheck the operator status and ensure they have sufficient resources and network connectivity.', + { baseMessage, operators: operatorList }, + ), + operatorDetails: operatorDetailsMarkdown, + }; + } + } + + // If rawMessage is empty AND no operator failures, check if cluster actually has issues + if (!rawMessage) { + // Check ClusterVersion conditions to see if there are real failures + const conditions = cv?.status?.conditions || []; + const hasFailing = conditions.some((c) => c.type === 'Failing' && c.status === 'True'); + const isNotUpgradeable = conditions.some( + (c) => c.type === 'Upgradeable' && c.status === 'False', + ); + const hasRetrievalFailure = conditions.some( + (c) => c.type === 'RetrievedUpdates' && c.status === 'False', + ); + const hasReleaseIssue = conditions.some( + (c) => c.type === 'ReleaseAccepted' && c.status === 'False', + ); + + // If cluster has actual issues but no message, return generic fallback + if (hasFailing || isNotUpgradeable || hasRetrievalFailure || hasReleaseIssue) { + return { + title: t('public~Cluster has issues preventing updates'), + message: t( + 'public~The cluster is not ready to update. Check the cluster operator status and resolve any issues before attempting to update.', + ), + }; + } + + // Cluster is healthy and ready to upgrade - no message needed + return { + title: '', + message: '', + }; + } + + // Pattern: Update already in progress (informational, not an error) + // This message comes from Upgradeable condition during updates + if ( + rawMessage.includes('An update is already in progress') || + rawMessage.includes('UpdateInProgress') + ) { + // Return empty - this is normal during updates, not a blocking issue + // The isProgressing check above will show the "Updating" status + return { + title: '', + message: '', + }; + } + + // Pattern: ClusterVersionOverridesSet + if ( + rawMessage.includes('ClusterVersionOverridesSet') || + rawMessage.includes('cluster version overrides prevents upgrades') + ) { + return { + title: t('public~Update blocked by cluster version overrides'), + message: t( + 'public~The cluster has version overrides configured that prevent automatic updates. Remove the overrides from the ClusterVersion object to continue with the update.', + ), + }; + } + + // Pattern: ClusterOperatorsDegraded + if ( + rawMessage.includes('ClusterOperatorsDegraded') || + rawMessage.includes('ClusterOperatorNotAvailable') + ) { + return { + title: t('public~Update blocked by degraded cluster operators'), + message: t( + 'public~Some cluster operators are in a degraded or unavailable state. Fix the operator issues before attempting to update the cluster.', + ), + }; + } + + // Pattern: Validation failures + if (rawMessage.includes('validation failed') || rawMessage.includes('Validation error')) { + return { + title: t('public~Update validation failed'), + message: t( + 'public~The update payload failed validation checks. This may indicate issues with the update manifest or cluster configuration.', + ), + }; + } + + // Pattern: Network/connectivity issues + if ( + rawMessage.includes('unable to retrieve') || + rawMessage.includes('connection refused') || + rawMessage.includes('timeout') + ) { + return { + title: t('public~Update failed due to connectivity issues'), + message: t( + 'public~Unable to download or validate the update payload. Check network connectivity and registry access.', + ), + }; + } + + // Pattern: Insufficient resources + if (rawMessage.includes('insufficient resources') || rawMessage.includes('out of disk space')) { + return { + title: t('public~Update failed due to insufficient resources'), + message: t( + 'public~The cluster does not have enough resources to complete the update. Ensure adequate disk space and memory are available.', + ), + }; + } + + // Pattern: Update blocked by policy + if (rawMessage.includes('blocked by policy') || rawMessage.includes('not permitted')) { + return { + title: t('public~Update blocked by cluster policy'), + message: t( + 'public~The update is blocked by cluster policies or governance rules. Contact your cluster administrator for assistance.', + ), + }; + } + + // Pattern: Precondition failures (general) + if (rawMessage.includes('Preconditions failed') || rawMessage.includes('Precondition')) { + // Try to extract actionable advice (sentences that start with action words) + const adviceMatch = rawMessage.match(/\.\s*(Please [^.]+\.)/); + const advice = adviceMatch ? adviceMatch[1] : ''; + + return { + title: t('public~Update preconditions not met'), + message: + advice || + t( + 'public~The cluster does not meet the required conditions for updating. Check the cluster status and resolve any blocking issues.', + ), + }; + } + + // Pattern: Signatures/verification failures + if (rawMessage.includes('signature') || rawMessage.includes('verification failed')) { + return { + title: t('public~Update signature verification failed'), + message: t( + 'public~The update payload could not be verified. This may indicate issues with release signatures or registry certificates.', + ), + }; + } + + // Check for broader operator issues (matching troubleshoot conditions) + if (cv && clusterOperators) { + const conditions = cv.status?.conditions || []; + + // Check for cluster-level failure conditions + const failing = conditions.find((c) => c.type === 'Failing' && c.status === 'True'); + const invalid = conditions.find((c) => c.type === 'Invalid' && c.status === 'True'); + const retrievedUpdates = conditions.find( + (c) => c.type === 'RetrievedUpdates' && c.status === 'False', + ); + const releaseAccepted = conditions.find( + (c) => c.type === 'ReleaseAccepted' && c.status === 'False', + ); + + // Check for operator issues using same logic as troubleshoot conditions + const operatorIssueDetails = detectOperatorIssues(clusterOperators); + + // If we have operator issues, show appropriate banner with details + if (operatorIssueDetails.length > 0) { + const operatorList = operatorIssueDetails + .map((detail) => `${detail.name} (${detail.issue})`) + .join(', '); + + const baseMessage = t( + 'public~{{count}} cluster operators are experiencing issues and need to be healthy before the cluster can be updated.', + { count: operatorIssueDetails.length }, + ); + + return { + title: t('public~Cluster operators are experiencing issues'), + message: t( + 'public~{{baseMessage}}\n\nAffected operators: {{operators}}\n\nCheck the operator status and ensure they have sufficient resources and network connectivity.', + { baseMessage, operators: operatorList }, + ), + }; + } + + // If we have other failure conditions (no operator issues but other problems) + const hasOtherFailures = + failing || + invalid || + (retrievedUpdates && retrievedUpdates.message) || + (releaseAccepted && releaseAccepted.message); + + if (hasOtherFailures) { + return { + title: t('public~Cluster update conditions need attention'), + message: t( + 'public~The cluster has conditions that prevent updates. Check the cluster status and resolve any issues before attempting to update.', + ), + }; + } + } + + // Default: try to extract meaningful parts from technical messages + if (rawMessage.length > 200) { + // For very long technical messages, try to extract the last sentence which often contains actionable advice + const sentences = rawMessage.split(/[.!?]+/).filter((s) => s.trim()); + const lastSentence = sentences[sentences.length - 1]?.trim(); + + if ( + lastSentence && + (lastSentence.includes('Please ') || + lastSentence.includes('remove ') || + lastSentence.includes('Check ')) + ) { + return { + title: t('public~Update failed'), + message: `${lastSentence}.`, + }; + } + } + + // Fallback: return cleaned up original message + const cleanMessage = rawMessage + .replace(/Preconditions failed for payload loaded version="[^"]*" image="[^"]*":\s*/, '') // Remove technical payload info + .replace(/Precondition "[^"]*" failed because of "[^"]*":\s*/, '') // Remove precondition technical details + .replace(/sha256:[a-f0-9]{64}/g, '[image digest]') // Replace long SHA digests + .trim(); + + return { + title: t('public~Update failed'), + message: cleanMessage || t('public~An error occurred during the update process.'), + }; +}; + export const ClusterNotUpgradeableAlert: FC = ({ cv, onCancel, @@ -836,7 +1188,7 @@ export const MachineConfigPoolsArePausedAlert: FC} + customIcon={} actionLinks={ workerMachineConfigPoolIsEditable && ( = ({ +// Alert content component for cluster update status +interface AlertContentProps { + failingCondition: boolean; + progressingCondition: boolean; + hasOperatorProblems: boolean; + retrievedUpdatesFailure: boolean; + message: string; + rawFailureMessage?: string; + operatorDetails?: string; + currentVersion: string; + desiredVersion: string; + showPreCheck: boolean; + cv: ClusterVersionKind; + t: (key: string, options?: { [key: string]: string | number }) => string; +} + +const UpdateAlertContent: FC = ({ + failingCondition, + progressingCondition, + hasOperatorProblems, + retrievedUpdatesFailure, + message, + rawFailureMessage, + operatorDetails, + currentVersion, + desiredVersion, + showPreCheck, cv, - machineConfigPools, + t, }) => { - const { t } = useTranslation(); + // Unified blocking condition predicate that covers all blocking states + // This ensures title and body rendering stay in sync + const isBlockingCondition = + !!failingCondition || hasOperatorProblems || retrievedUpdatesFailure || rawFailureMessage; + const isProgressing = !!progressingCondition; + + // Memoize expensive operations + const hasUpdates = useMemo(() => hasAvailableUpdates(cv), [cv]); + const availableUpdates = useMemo(() => getSortedAvailableUpdates(cv), [cv]); + + const updatesDisplayText = useMemo(() => { + if (!hasUpdates) { + return t('public~Cluster {{currentVersion}} - Up to Date', { currentVersion }); + } + + if (availableUpdates.length === 1) { + return t('public~Update Available: {{updateVersion}}', { + currentVersion, + updateVersion: availableUpdates[0]?.version, + }); + } + + if (availableUpdates.length > 1) { + return t('public~Available Updates (latest: {{latestVersion}})', { + currentVersion, + latestVersion: availableUpdates[0]?.version, + }); + } + return ''; + }, [hasUpdates, availableUpdates, currentVersion, t]); + + if (isBlockingCondition && message) { + return ( + + + + + +
{message}
+
+ {(operatorDetails || (rawFailureMessage && rawFailureMessage !== message)) && ( + +
+ + {t('public~View technical details')} + +
+ +
+
+
+ )} +
+ ); + } + + if (isProgressing) { + return ( + + + + + + + {currentVersion !== desiredVersion && ( + <> + + {currentVersion} + + + + {desiredVersion} + + + )} + + + +
+ {currentVersion !== desiredVersion + ? t('public~Cluster is updating to {{desiredVersion}}', { desiredVersion }) + : t('public~Cluster update is in progress')} +
+
+
+ ); + } + + if (showPreCheck) { + return ( + + + + {hasUpdates && availableUpdates.length > 0 && ( + + + + )} + + + +
{updatesDisplayText}
+
+ +
+ {hasUpdates + ? t('public~Check cluster health and update prerequisites.') + : t('public~Verify cluster health and operational status.')} +
+
+
+ ); + } + + return ( + + +
+ {updatesDisplayText || t('public~Cluster {{currentVersion}}', { currentVersion })} +
+
+ +
+ {t('public~Review cluster status.')} +
+
+
+ ); +}; + +const UpdateAssessmentCard: FC<{ + cv: ClusterVersionKind; + clusterOperators?: ClusterOperator[]; + machineConfigPools?: MachineConfigPoolKind[]; +}> = ({ cv, clusterOperators, machineConfigPools }) => { + const { t } = useTranslation('public'); + const isOLSAvailable = useFlag(FEATURE_FLAG_LIGHTSPEED_PLUGIN); + const [assessmentExpanded, setAssessmentExpanded] = useState(true); + + // Memoize expensive computations (call all hooks before any returns) + const conditions = useMemo(() => cv.status?.conditions || [], [cv.status?.conditions]); + const currentVersion = useMemo(() => getLastCompletedUpdate(cv), [cv]); + const desiredVersion = useMemo(() => getDesiredClusterVersion(cv), [cv]); + + // Check cluster and operator conditions for alert display + const progressingCondition = useMemo( + () => conditions.find((c) => c.type === 'Progressing' && c.status === 'True'), + [conditions], + ); + const failingCondition = useMemo( + () => conditions.find((c) => c.type === 'Failing' && c.status === 'True'), + [conditions], + ); + const hasOperatorProblems = useMemo(() => hasOperatorIssues(clusterOperators), [ + clusterOperators, + ]); + + // Determine button visibility using the new unified logic + const { showStatus, showPreCheck } = useMemo( + () => determineWorkflowButtons(cv, clusterOperators), + [cv, clusterOperators], + ); + + // Get failure details for display when issues exist + const upgradeableCondition = useMemo( + () => getConditionOfType(cv, ClusterVersionConditionType.Upgradeable), + [cv], + ); + const releaseAccepted = useMemo( + () => getConditionOfType(cv, ClusterVersionConditionType.ReleaseAccepted), + [cv], + ); + const retrievedUpdates = useMemo( + () => getConditionOfType(cv, ClusterVersionConditionType.RetrievedUpdates), + [cv], + ); + const invalid = useMemo(() => getConditionOfType(cv, ClusterVersionConditionType.Invalid), [cv]); + + const rawFailureMessage = useMemo( + () => + failingCondition?.message || + // Only use Upgradeable message when status is False (not upgradeable) + (upgradeableCondition?.status === 'False' ? upgradeableCondition.message : '') || + // Only use ReleaseAccepted message when status is False (not accepted) + (releaseAccepted?.status === 'False' ? releaseAccepted.message : '') || + // Only use RetrievedUpdates message when status is False (failed to retrieve) + (retrievedUpdates?.status === 'False' ? retrievedUpdates.message : '') || + // Only use Invalid message when status is True (cluster version is invalid) + (invalid?.status === 'True' ? invalid.message : '') || + '', + [failingCondition?.message, upgradeableCondition, releaseAccepted, retrievedUpdates, invalid], + ); + + const { message, operatorDetails } = useMemo( + () => parseUpdateFailureMessage(rawFailureMessage, t, cv, clusterOperators), + [rawFailureMessage, t, cv, clusterOperators], + ); + + // Memoize alert title determination + const alertTitle = useMemo(() => { + const hasFailures = !!failingCondition || hasOperatorProblems; + const isProgressing = !!progressingCondition; + + if (hasFailures && isProgressing) { + return t('public~Update issues detected'); + } + if (hasFailures) { + return t('public~Cluster issues detected'); + } + if (isProgressing) { + return t('public~Cluster updating'); + } + if (showPreCheck) { + return t('public~Cluster health'); + } + return t('public~Cluster status'); + }, [failingCondition, hasOperatorProblems, progressingCondition, showPreCheck, t]); + + // Don't render if OLS is not available + if (!isOLSAvailable) { + return null; + } + + // Don't render if no buttons should show + if (!showPreCheck && !showStatus) { + return null; + } + + return ( + + setAssessmentExpanded(!assessmentExpanded)} + toggleButtonProps={{ + id: 'update-assessment-toggle', + 'aria-expanded': assessmentExpanded, + }} + > + {t('public~AI Assessment')} + + + + } + isInline + title={alertTitle} + actionLinks={ + isOLSAvailable && (showPreCheck || showStatus) ? ( + + {/* Pre-check button: appears when cluster is healthy and ready for updates */} + {showPreCheck ? ( + + ) : null} + {/* Status button: appears when cluster is progressing or has issues */} + {showStatus ? ( + + ) : null} + + ) : null + } + > + + + + + + ); +}; + +const ClusterSettingsAlerts: FC = ({ cv, machineConfigPools }) => { + const { t } = useTranslation('public'); + const isOLSAvailable = useFlag(FEATURE_FLAG_LIGHTSPEED_PLUGIN); + + // Gate cluster operator watching behind OLS availability to prevent unnecessary API calls + const [clusterOperators] = useK8sWatchResource( + isOLSAvailable ? ClusterOperatorsResource : null, + ); if (isClusterExternallyManaged()) { return ( @@ -874,6 +1571,11 @@ export const ClusterSettingsAlerts: FC = ({ <> {!!getConditionUpgradeableFalse(cv) && } + ); }; @@ -896,6 +1598,7 @@ export const ClusterVersionDetailsTable: FC = ( const [machineConfigPools] = useK8sWatchResource( MachineConfigPoolsResource, ); + const serviceLevelTitle = useServiceLevelTitle(); const desiredVersion = getDesiredClusterVersion(cv); @@ -1003,12 +1706,14 @@ export const ClusterVersionDetailsTable: FC = ( )} {(status === ClusterUpdateStatus.UpdatingAndFailing || status === ClusterUpdateStatus.Updating) && ( - + <> + + )}
@@ -1073,7 +1778,10 @@ export const ClusterVersionDetailsTable: FC = ( {t('public~Cluster version configuration')} - + @@ -1085,14 +1793,14 @@ export const ClusterVersionDetailsTable: FC = ( {_.isEmpty(autoscalers) ? ( - + {t('public~Create autoscaler')} ) : ( autoscalers.map((autoscaler) => (
diff --git a/frontend/public/components/modals/cluster-update-modal.tsx b/frontend/public/components/modals/cluster-update-modal.tsx index 330cb0327ce..cd2396ef3c7 100644 --- a/frontend/public/components/modals/cluster-update-modal.tsx +++ b/frontend/public/components/modals/cluster-update-modal.tsx @@ -15,8 +15,10 @@ import { ModalVariant, Radio, } from '@patternfly/react-core'; +import { RhUiAiInfoIcon } from '@patternfly/react-icons'; import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook'; -import { DropdownWithSwitch } from '@console/shared/src/components/dropdown'; +import { DropdownWithSwitch } from '@console/shared/src/components/dropdown/dropdown-with-switch/DropdownWithSwitch'; +import { useFlag } from '@console/shared/src/hooks/useFlag'; import { ClusterVersionModel, MachineConfigPoolModel, NodeModel } from '../../models'; import { FieldLevelHelp } from '../utils/field-level-help'; @@ -47,6 +49,7 @@ import { } from '../cluster-settings/cluster-settings'; import { MachineConfigPoolsSelector } from '../machine-config-pools-selector'; import { ModalFooterWithAlerts } from '@console/shared/src/components/modals/ModalFooterWithAlerts'; +import { UpdateWorkflowOLSButton } from '@console/shared/src/components/cluster-updates/explain-button'; enum upgradeTypes { Full = 'Full', @@ -78,7 +81,9 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { const [machineConfigPoolsToPause, setMachineConfigPoolsToPause] = useState([]); const [upgradeType, setUpgradeType] = useState(upgradeTypes.Full); const [includeNotRecommended, setIncludeNotRecommended] = useState(false); - const { t } = useTranslation(); + const { t } = useTranslation('public'); + const isLightspeedAvailable = useFlag('LIGHTSPEED_PLUGIN'); + useEffect(() => { const initialMCPPausedValues = machineConfigPools .filter((mcp) => !isMCPMaster(mcp) && isMCPPaused(mcp)) @@ -210,17 +215,17 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { return ( <>
{clusterUpgradeableFalse && } - -

{currentVersion}

+ + {currentVersion} - + { data-test="update-cluster-modal-partial-update-radio" /> + {/* OLS Update Precheck Section */} + {isLightspeedAvailable && desiredVersion && ( + + } + isInline + title={ + <> + {t('Update Prerequisites')} + + {t('Updating from {{currentVersion}} to {{desiredVersion}}', { + currentVersion, + desiredVersion, + })} + + + } + actionLinks={ + { + // Close modal when OLS opens + close(); + }} + /> + } + data-test="update-cluster-modal-ols-precheck" + /> + )}
@@ -354,7 +394,7 @@ const ClusterUpdateModal = (props: ClusterUpdateModalProps) => { (upgradeType === upgradeTypes.Partial && machineConfigPoolsToPause.length === 0) } > - {t('public~Update')} + {t('Update cluster')}