diff --git a/tests/e2e/constants/TIMEOUT_CONSTANTS.ts b/tests/e2e/constants/TIMEOUT_CONSTANTS.ts index 9a44e84486a..a07a29ba98c 100644 --- a/tests/e2e/constants/TIMEOUT_CONSTANTS.ts +++ b/tests/e2e/constants/TIMEOUT_CONSTANTS.ts @@ -27,6 +27,7 @@ export const TIMEOUT_CONSTANTS: { TS_SELENIUM_WAIT_FOR_URL: number; TS_WAIT_LOADER_ABSENCE_TIMEOUT: number; TS_WAIT_LOADER_PRESENCE_TIMEOUT: number; + TS_WAIT_BACKUP_STATUS_TIMEOUT: number; } = { /** * default amount of tries, "5" by default. @@ -88,10 +89,15 @@ export const TIMEOUT_CONSTANTS: { TS_CLICK_DASHBOARD_ITEM_TIMEOUT: Number(process.env.TS_CLICK_DASHBOARD_ITEM_TIMEOUT) || 2_000, /** - * timeout for workspace stopped status, "30 000" by default + * timeout for workspace stopped status, "60 000" by default */ TS_DASHBOARD_WORKSPACE_STOP_TIMEOUT: Number(process.env.TS_DASHBOARD_WORKSPACE_STOP_TIMEOUT) || 60_000, + /** + * timeout for waiting for backup status, "360 000" by default + */ + TS_WAIT_BACKUP_STATUS_TIMEOUT: Number(process.env.TS_WAIT_BACKUP_STATUS_TIMEOUT) || 360_000, + // -------------------------------------------- PROJECT TREE -------------------------------------------- /** @@ -102,7 +108,7 @@ export const TIMEOUT_CONSTANTS: { // -------------------------------------------- EDITOR -------------------------------------------- /** - * timeout for interactions with editor tab - wait, click, select, "8 000" by default. + * timeout for interactions with editor tab - wait, click, select, "20 000" by default. */ TS_EDITOR_TAB_INTERACTION_TIMEOUT: Number(process.env.TS_OPEN_PROJECT_TREE_TIMEOUT) || 20_000, diff --git a/tests/e2e/pageobjects/dashboard/Workspaces.ts b/tests/e2e/pageobjects/dashboard/Workspaces.ts index f6be55064bd..c7b35ff42a9 100644 --- a/tests/e2e/pageobjects/dashboard/Workspaces.ts +++ b/tests/e2e/pageobjects/dashboard/Workspaces.ts @@ -32,6 +32,15 @@ export class Workspaces { private static readonly CONFIRMATION_WINDOW: By = By.xpath('//div[@aria-label="Delete workspaces confirmation window"]'); private static readonly LEARN_MORE_DOC_LINK: By = By.xpath('//div/p/a'); + private static readonly BACKUPS_BUTTON: By = By.id('view-backups'); + private static readonly BACKUP_IMAGE: By = By.css('input[aria-label="Copyable input"]'); + private static readonly CREATE_FROM_BACKUP_BUTTON: By = By.xpath('//span[text()="Create from Backup"]'); + private static readonly RESTORE_WORKSPACE_BUTTON: By = By.xpath('//span[text()="Restore Workspace"]'); + private static readonly RESTORE_CONFIRM_BUTTON: By = By.xpath('//span[text()="Restore"]'); + private static readonly EXTERNAL_REGISTRY_MODE_BUTTON: By = By.xpath('//span[text()="External registry"]'); + private static readonly BACKUP_IMAGE_URL_INPUT: By = By.css('input[aria-label="Backup image URL"]'); + private static readonly WORKSPACE_NAME_INPUT: By = By.css('input[aria-label="Workspace name"]'); + constructor( @inject(CLASSES.DriverHelper) private readonly driverHelper: DriverHelper @@ -214,6 +223,68 @@ export class Workspaces { return await this.driverHelper.waitAndGetElementAttribute(Workspaces.LEARN_MORE_DOC_LINK, 'href'); } + async waitBackupStatus( + workspaceName: string, + status: string, + timeout: number = TIMEOUT_CONSTANTS.TS_WAIT_BACKUP_STATUS_TIMEOUT + ): Promise { + Logger.debug(`Backup status for the "${workspaceName}" list item`); + + await this.driverHelper.waitVisibility(this.getBackupStatusLocator(workspaceName, status), timeout); + } + + async openBackupsPage(timeout: number = TIMEOUT_CONSTANTS.TS_CLICK_DASHBOARD_ITEM_TIMEOUT): Promise { + Logger.debug(); + + await this.driverHelper.waitAndClick(Workspaces.BACKUPS_BUTTON, timeout); + } + + async clickCreateFromBackupButton( + workspaceName: string, + timeout: number = TIMEOUT_CONSTANTS.TS_COMMON_DASHBOARD_WAIT_TIMEOUT + ): Promise { + Logger.debug(`"${workspaceName}"`); + + await this.driverHelper.waitAndClick(this.getActionsRestoreWorkspaceButtonLocator(workspaceName), timeout); + await this.driverHelper.waitAndClick(Workspaces.CREATE_FROM_BACKUP_BUTTON, timeout); + } + + async restoreWorkspaceFromDefaultRegistry(): Promise { + Logger.debug(); + + await this.driverHelper.waitAndClick(Workspaces.RESTORE_WORKSPACE_BUTTON); + await this.driverHelper.waitAndClick(Workspaces.RESTORE_CONFIRM_BUTTON); + } + + async restoreWorkspaceFromExternalRegistry(backupImageUrl: string, workspaceName: string): Promise { + Logger.debug(); + + await this.driverHelper.waitAndClick(Workspaces.EXTERNAL_REGISTRY_MODE_BUTTON); + await this.setBackupImageUrlValue(backupImageUrl); + await this.setWorkspaceNameValue(workspaceName); + await this.driverHelper.waitAndClick(Workspaces.RESTORE_WORKSPACE_BUTTON); + } + + async getBackupImageUrlValue(): Promise { + Logger.debug(); + + return await this.driverHelper.waitAndGetValue(Workspaces.BACKUP_IMAGE, TIMEOUT_CONSTANTS.TS_COMMON_DASHBOARD_WAIT_TIMEOUT); + } + + async setBackupImageUrlValue(backupImageUrl: string): Promise { + Logger.debug(); + + await this.driverHelper.clear(Workspaces.BACKUP_IMAGE_URL_INPUT); + await this.driverHelper.type(Workspaces.BACKUP_IMAGE_URL_INPUT, backupImageUrl); + } + + async setWorkspaceNameValue(workspaceName: string): Promise { + Logger.debug(); + + await this.driverHelper.clear(Workspaces.WORKSPACE_NAME_INPUT); + await this.driverHelper.type(Workspaces.WORKSPACE_NAME_INPUT, workspaceName); + } + private getWorkspaceListItemLocator(workspaceName: string): By { return By.xpath(`//tr[td//span[text()='${workspaceName}']]`); } @@ -247,4 +318,12 @@ export class Workspaces { private getOpenWorkspaceDetailsLinkLocator(workspaceName: string): By { return By.xpath(`${this.getWorkspaceListItemLocator(workspaceName).value}//span[text()='${workspaceName}']`); } + + private getActionsRestoreWorkspaceButtonLocator(workspaceName: string): By { + return By.xpath(`//tr[td//span[text()='${workspaceName}']]//button[@aria-label='Actions for ${workspaceName}']`); + } + + private getBackupStatusLocator(workspaceName: string, status: string): By { + return By.xpath(`//tr[td//span[text()='${workspaceName}']]//span[@aria-label="Backup status: ${status}"]`); + } } diff --git a/tests/e2e/specs/miscellaneous/WorkspaceBackupRestore.spec.ts b/tests/e2e/specs/miscellaneous/WorkspaceBackupRestore.spec.ts new file mode 100644 index 00000000000..7073fadc75d --- /dev/null +++ b/tests/e2e/specs/miscellaneous/WorkspaceBackupRestore.spec.ts @@ -0,0 +1,224 @@ +/** ******************************************************************* + * copyright (c) 2026 Red Hat, Inc. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + **********************************************************************/ +import { e2eContainer } from '../../configs/inversify.config'; +import { ViewSection } from 'monaco-page-objects'; +import { CLASSES, TYPES } from '../../configs/inversify.types'; +import { expect } from 'chai'; +import { WorkspaceHandlingTests } from '../../tests-library/WorkspaceHandlingTests'; +import { ProjectAndFileTests } from '../../tests-library/ProjectAndFileTests'; +import { LoginTests } from '../../tests-library/LoginTests'; +import { registerRunningWorkspace } from '../MochaHooks'; +import { BrowserTabsUtil } from '../../utils/BrowserTabsUtil'; +import { BASE_TEST_CONSTANTS } from '../../constants/BASE_TEST_CONSTANTS'; +import { Dashboard } from '../../pageobjects/dashboard/Dashboard'; +import { Workspaces } from '../../pageobjects/dashboard/Workspaces'; +import { FACTORY_TEST_CONSTANTS } from '../../constants/FACTORY_TEST_CONSTANTS'; +import { ITestWorkspaceUtil } from '../../utils/workspace/ITestWorkspaceUtil'; +import { Logger } from '../../utils/Logger'; +import { StringUtil } from '../../utils/StringUtil'; +import { TIMEOUT_CONSTANTS } from '../../constants/TIMEOUT_CONSTANTS'; +import { ContainerTerminal, KubernetesCommandLineToolsExecutor } from '../../utils/KubernetesCommandLineToolsExecutor'; +import { ShellString } from 'shelljs'; +import { WorkspaceDetails } from '../../pageobjects/dashboard/workspace-details/WorkspaceDetails'; + +const factoryUrl: string = + FACTORY_TEST_CONSTANTS.TS_SELENIUM_FACTORY_GIT_REPO_URL || 'https://gh.crw-qe.com/test-automation-only/python-hello-world.git'; +const testFileName: string = 'backup-test.txt'; +const testFileContent: string = 'This is a test file for backup verification'; + +suite(`"Restore workspace from backup" test ${BASE_TEST_CONSTANTS.TEST_ENVIRONMENT}`, function (): void { + const workspaceHandlingTests: WorkspaceHandlingTests = e2eContainer.get(CLASSES.WorkspaceHandlingTests); + const projectAndFileTests: ProjectAndFileTests = e2eContainer.get(CLASSES.ProjectAndFileTests); + const loginTests: LoginTests = e2eContainer.get(CLASSES.LoginTests); + const browserTabsUtil: BrowserTabsUtil = e2eContainer.get(CLASSES.BrowserTabsUtil); + const dashboard: Dashboard = e2eContainer.get(CLASSES.Dashboard); + const workspaces: Workspaces = e2eContainer.get(CLASSES.Workspaces); + const testWorkspaceUtil: ITestWorkspaceUtil = e2eContainer.get(TYPES.WorkspaceUtil); + const workspaceDetails: WorkspaceDetails = e2eContainer.get(CLASSES.WorkspaceDetails); + const kubernetesCommandLineToolsExecutor: KubernetesCommandLineToolsExecutor = e2eContainer.get( + CLASSES.KubernetesCommandLineToolsExecutor + ); + const containerTerminal: ContainerTerminal = e2eContainer.get(CLASSES.ContainerTerminal); + const workspaceName2: string = 'test-workspace-2'; + + let projectSection: ViewSection; + let workspaceName: string; + let backupImageUrl: string; + let projectName: string; + + async function openWorkspaceDetailsBackup(workspaceName: string): Promise { + await workspaces.clickWorkspaceListItemLink(workspaceName); + await workspaceDetails.waitWorkspaceTitle(workspaceName); + await workspaceDetails.waitLoaderDisappearance(); + await workspaceDetails.selectTab('Backup'); + } + + suiteSetup('Login', async function (): Promise { + await loginTests.loginIntoChe(); + }); + test(`Create and open new workspace from factory:${factoryUrl}`, async function (): Promise { + await workspaceHandlingTests.createAndOpenWorkspaceFromGitRepository(factoryUrl); + await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage(); + registerRunningWorkspace(WorkspaceHandlingTests.getWorkspaceName()); + }); + + test('Wait workspace readiness and project folder has been created', async function (): Promise { + await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor(); + }); + test('Check a project folder has been created', async function (): Promise { + projectName = FACTORY_TEST_CONSTANTS.TS_SELENIUM_PROJECT_NAME || StringUtil.getProjectNameFromGitUrl(factoryUrl); + projectSection = await projectAndFileTests.getProjectViewSession(); + expect(await projectAndFileTests.getProjectTreeItem(projectSection, projectName), 'Project folder was not imported').not.undefined; + await projectAndFileTests.performTrustDialogs(); + }); + test('Setup workspace context for API operations', function (): void { + workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + kubernetesCommandLineToolsExecutor.workspaceName = workspaceName; + kubernetesCommandLineToolsExecutor.loginToOcp(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + test('Create test file via API', function (): void { + Logger.debug(`Creating test file: /projects/${projectName}/${testFileName}`); + const createFileCommand: string = `echo "${testFileContent}" > /projects/${projectName}/${testFileName}`; + const output: ShellString = containerTerminal.execInContainerCommand(createFileCommand); + Logger.debug(`File creation output: ${output.stdout}`); + expect(output.code).to.equal(0); + }); + test('Verify test file content via API', function (): void { + Logger.debug(`Verifying test file content: /projects/${projectName}/${testFileName}`); + const readFileCommand: string = `cat /projects/${projectName}/${testFileName}`; + const output: ShellString = containerTerminal.execInContainerCommand(readFileCommand); + Logger.debug(`File content: ${output.stdout}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + test('Stop the workspace', async function (): Promise { + expect(workspaceName, 'Workspace name not available').not.empty; + await dashboard.openDashboard(); + await dashboard.waitPage(); + await dashboard.stopWorkspaceByUI(workspaceName); + await browserTabsUtil.closeAllTabsExceptCurrent(); + await workspaces.waitWorkspaceWithStoppedStatus(workspaceName); + await workspaces.waitBackupStatus(workspaceName, 'Never'); + }); + test('Wait for backup completion', async function (): Promise { + await workspaces.waitWorkspaceListItem(workspaceName); + await workspaces.waitBackupStatus(workspaceName, 'Success'); + }); + test('Get backup image URL', async function (): Promise { + await openWorkspaceDetailsBackup(workspaceName); + backupImageUrl = await workspaces.getBackupImageUrlValue(); + Logger.info(`Retrieved backup image URL: ${backupImageUrl}`); + }); + test('Delete the workspace', async function (): Promise { + await dashboard.deleteStoppedWorkspaceByUI(workspaceName); + }); + test('Open backups page', async function (): Promise { + await workspaces.openBackupsPage(); + await workspaces.waitWorkspaceListItem(workspaceName); + await workspaces.waitBackupStatus(workspaceName, 'Success'); + }); + test('Restore workspace from default registry', async function (): Promise { + const parentGUID: string = await browserTabsUtil.getCurrentWindowHandle(); + await workspaces.clickCreateFromBackupButton(workspaceName); + await workspaces.restoreWorkspaceFromDefaultRegistry(); + await browserTabsUtil.waitAndSwitchToAnotherWindow(parentGUID, TIMEOUT_CONSTANTS.TS_IDE_LOAD_TIMEOUT); + }); + test('Obtain workspace name after restore', async function (): Promise { + await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage(); + const obtainedName: string = WorkspaceHandlingTests.getWorkspaceName(); + Logger.info(`Obtained workspace name after first restore: '${obtainedName}'`); + }); + test('Register restored workspace', function (): void { + registerRunningWorkspace(WorkspaceHandlingTests.getWorkspaceName()); + }); + test('Wait workspace readiness after restore', async function (): Promise { + await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor(); + await projectAndFileTests.performTrustDialogs(); + projectSection = await projectAndFileTests.getProjectViewSession(); + }); + test('Setup workspace context for API operations after restore', function (): void { + workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + Logger.info(`Workspace name for API setup after first restore: '${workspaceName}'`); + expect(workspaceName, 'Workspace name should not be empty after restore').not.empty; + kubernetesCommandLineToolsExecutor.workspaceName = workspaceName; + kubernetesCommandLineToolsExecutor.loginToOcp(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + test('Verify project folder exists after restore', async function (): Promise { + await projectAndFileTests.expandProjectTreeItem(projectSection, projectName); + expect(await projectAndFileTests.getProjectTreeItem(projectSection, projectName), 'Project folder was not restored').not.undefined; + }); + test('Verify test file content is intact after restore via API', function (): void { + Logger.debug(`Verifying restored file content: /projects/${projectName}/${testFileName}`); + const readFileCommand: string = `cat /projects/${projectName}/${testFileName}`; + const output: ShellString = containerTerminal.execInContainerCommand(readFileCommand); + Logger.debug(`Restored file content: ${output.stdout}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + test('Delete the workspace', async function (): Promise { + await dashboard.openDashboard(); + await dashboard.waitPage(); + await dashboard.deleteStoppedWorkspaceByUI(workspaceName); + await browserTabsUtil.closeAllTabsExceptCurrent(); + }); + test('Open backups page', async function (): Promise { + await workspaces.openBackupsPage(); + await workspaces.waitWorkspaceListItem(workspaceName); + await workspaces.waitBackupStatus(workspaceName, 'Success'); + }); + test('Restore workspace from backup image URL ', async function (): Promise { + const parentGUID: string = await browserTabsUtil.getCurrentWindowHandle(); + await workspaces.clickCreateFromBackupButton(workspaceName); + await workspaces.restoreWorkspaceFromExternalRegistry(backupImageUrl, workspaceName2); + await browserTabsUtil.waitAndSwitchToAnotherWindow(parentGUID, TIMEOUT_CONSTANTS.TS_IDE_LOAD_TIMEOUT); + }); + test('Obtain workspace name after second restore', async function (): Promise { + await workspaceHandlingTests.obtainWorkspaceNameFromStartingPage(); + const obtainedName: string = WorkspaceHandlingTests.getWorkspaceName(); + Logger.info(`Obtained workspace name after second restore: '${obtainedName}'`); + }); + test('Register second restored workspace', function (): void { + registerRunningWorkspace(WorkspaceHandlingTests.getWorkspaceName()); + }); + test('Wait workspace readiness after second restore', async function (): Promise { + await projectAndFileTests.waitWorkspaceReadinessForCheCodeEditor(); + await projectAndFileTests.performTrustDialogs(); + projectSection = await projectAndFileTests.getProjectViewSession(); + }); + test('Setup workspace context for API operations after second restore', function (): void { + workspaceName = WorkspaceHandlingTests.getWorkspaceName(); + Logger.info(`Workspace name for API setup after second restore: '${workspaceName}'`); + expect(workspaceName, 'Workspace name should not be empty after second restore').not.empty; + kubernetesCommandLineToolsExecutor.workspaceName = workspaceName; + kubernetesCommandLineToolsExecutor.loginToOcp(); + kubernetesCommandLineToolsExecutor.getPodAndContainerNames(); + }); + test('Verify project folder exists after second restore', async function (): Promise { + await projectAndFileTests.expandProjectTreeItem(projectSection, projectName); + expect(await projectAndFileTests.getProjectTreeItem(projectSection, projectName), 'Project folder was not restored').not.undefined; + }); + test('Verify test file content is intact after second restore via API', function (): void { + Logger.debug(`Verifying restored file content: /projects/${projectName}/${testFileName}`); + const readFileCommand: string = `cat /projects/${projectName}/${testFileName}`; + const output: ShellString = containerTerminal.execInContainerCommand(readFileCommand); + Logger.debug(`Restored file content: ${output.stdout}`); + expect(output.stdout.trim()).to.equal(testFileContent); + }); + suiteTeardown('Open dashboard and close all other tabs', async function (): Promise { + await dashboard.openDashboard(); + await browserTabsUtil.closeAllTabsExceptCurrent(); + }); + suiteTeardown('Stop and delete workspace by API', async function (): Promise { + await testWorkspaceUtil.stopAndDeleteWorkspaceByName(workspaceName); + }); + suiteTeardown('Unregister running workspace', function (): void { + registerRunningWorkspace(''); + }); +}); diff --git a/tests/e2e/tests-library/ProjectAndFileTests.ts b/tests/e2e/tests-library/ProjectAndFileTests.ts index ccebfe8e7a5..1538ea52222 100644 --- a/tests/e2e/tests-library/ProjectAndFileTests.ts +++ b/tests/e2e/tests-library/ProjectAndFileTests.ts @@ -243,4 +243,14 @@ export class ProjectAndFileTests { throw new Error(`File "${fileName}" was not opened in the editor after ${maxAttempts} attempts`); } + + async expandProjectTreeItem(projectSection: ViewSection, projectName: string): Promise { + Logger.debug(`${projectName}`); + const projectTreeItem: ViewItem | undefined = await projectSection.findItem(projectName, 2); + if (!projectTreeItem) { + throw new Error(`Project tree item "${projectName}" not found`); + } + await projectTreeItem.click(); + await this.driverHelper.wait(TIMEOUT_CONSTANTS.TS_EXPAND_PROJECT_TREE_ITEM_TIMEOUT); + } } diff --git a/tests/e2e/utils/KubernetesCommandLineToolsExecutor.ts b/tests/e2e/utils/KubernetesCommandLineToolsExecutor.ts index 8a788c92822..6a716d59689 100644 --- a/tests/e2e/utils/KubernetesCommandLineToolsExecutor.ts +++ b/tests/e2e/utils/KubernetesCommandLineToolsExecutor.ts @@ -1,5 +1,5 @@ /** ******************************************************************* - * copyright (c) 2023-2025 Red Hat, Inc. + * copyright (c) 2023-2026 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -78,21 +78,68 @@ export class KubernetesCommandLineToolsExecutor implements IKubernetesCommandLin getContainerName(): string { Logger.debug(`${this.kubernetesCommandLineTool} - get container name.`); + Logger.debug(`Getting container name for pod: '${KubernetesCommandLineToolsExecutor.pod}' in namespace: ${this.namespace}`); const output: ShellString = this.shellExecutor.executeCommand( - `${this.kubernetesCommandLineTool} get ${KubernetesCommandLineToolsExecutor.pod} -o jsonpath='{.spec.containers[*].name}' -n ${this.namespace}` + `${this.kubernetesCommandLineTool} get pod ${KubernetesCommandLineToolsExecutor.pod} -o jsonpath='{.spec.containers[*].name}' -n ${this.namespace}` ); echo('\n'); - return output.stderr ? output.stderr : output.stdout; + const containerName: string = output.stderr ? output.stderr : output.stdout; + Logger.debug(`Found container name: '${containerName}'`); + return containerName; } getWorkspacePodName(): string { Logger.debug(`${this.kubernetesCommandLineTool} - get workspace pod name.`); + Logger.debug( + `Looking for pod with label: controller.devfile.io/devworkspace_name=${this.workspaceName} in namespace: ${this.namespace}` + ); const output: ShellString = this.shellExecutor.executeCommand( `${this.kubernetesCommandLineTool} get pod -l controller.devfile.io/devworkspace_name=${this.workspaceName} -n ${this.namespace} -o name` ); - return output.stderr ? output.stderr : output.stdout.replace('\n', ''); + let podName: string = output.stderr ? output.stderr : output.stdout.replace('\n', ''); + Logger.debug(`Found pod name (before removing prefix): '${podName}'`); + + // if pod not found, try to find by workspace name prefix (for restored workspaces with suffix) + if (!podName || podName.trim() === '') { + Logger.warn(`No pod found with exact label controller.devfile.io/devworkspace_name=${this.workspaceName}`); + Logger.debug(`Trying to find pod with workspace name starting with: ${this.workspaceName}`); + + // get all pods and filter by label prefix + const allPodsOutput: ShellString = this.shellExecutor.executeCommand( + `${this.kubernetesCommandLineTool} get pods -n ${this.namespace} -o jsonpath='{range .items[*]}{.metadata.name}{"\\t"}{.metadata.labels.controller\\.devfile\\.io/devworkspace_name}{"\\n"}{end}'` + ); + + if (allPodsOutput.stdout && this.workspaceName) { + const lines: string[] = allPodsOutput.stdout.split('\n').filter((line: string): boolean => line.trim() !== ''); + for (const line of lines) { + const [name, label] = line.split('\t'); + if (label && this.workspaceName && label.startsWith(this.workspaceName)) { + Logger.info(`Found pod by prefix match: ${name} with label ${label}`); + podName = name; + // update workspace name to the actual one with suffix + this._workspaceName = label; + Logger.info(`Updated workspace name to: ${this._workspaceName}`); + break; + } + } + } + + // if still not found, list all pods for debugging + if (!podName || podName.trim() === '') { + Logger.warn('Listing all pods in namespace for debugging:'); + const debugPods: ShellString = this.shellExecutor.executeCommand( + `${this.kubernetesCommandLineTool} get pods -n ${this.namespace} --show-labels` + ); + Logger.warn(`All pods:\n${debugPods.stdout}`); + } + } + + // remove 'pod/' prefix if present (oc get -o name returns 'pod/podname') + const cleanPodName: string = podName.replace(/^pod\//, ''); + Logger.debug(`Clean pod name (after removing prefix): '${cleanPodName}'`); + return cleanPodName; } deleteDevWorkspace(devfileName?: string): void { diff --git a/tests/e2e/utils/workspace/ApiUrlResolver.ts b/tests/e2e/utils/workspace/ApiUrlResolver.ts index 6a8fe31b2fd..1a2c2019fd9 100644 --- a/tests/e2e/utils/workspace/ApiUrlResolver.ts +++ b/tests/e2e/utils/workspace/ApiUrlResolver.ts @@ -1,5 +1,5 @@ /** ******************************************************************* - * copyright (c) 2019-2023 Red Hat, Inc. + * copyright (c) 2019-2026 Red Hat, Inc. * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -26,7 +26,8 @@ export class ApiUrlResolver { ) {} async getWorkspaceApiUrl(workspaceName: string): Promise { - return `${await this.getWorkspacesApiUrl()}/${workspaceName}`; + const actualWorkspaceName: string = await this.resolveWorkspaceName(workspaceName); + return `${await this.getWorkspacesApiUrl()}/${actualWorkspaceName}`; } async getWorkspacesApiUrl(): Promise { @@ -34,6 +35,66 @@ export class ApiUrlResolver { return `${ApiUrlResolver.DASHBOARD_API_URL}/${namespace}/devworkspaces`; } + /** + * resolves the actual DevWorkspace name from the API. + * If the exact workspace name exists, returns it as is. + * If not found, searches for a workspace whose name matches the pattern: workspaceName + '-' + random suffix + * (to handle cases where DevWorkspace has a random suffix like '-4fnq' after backup/restore). + * @param workspaceName - The workspace name to search for + * @returns The actual DevWorkspace name from the API + * @throws Error if no matching workspace is found + */ + private async resolveWorkspaceName(workspaceName: string): Promise { + Logger.debug(`Resolving workspace name: ${workspaceName}`); + + try { + // first, try to get the workspace directly by the provided name + const directUrl: string = `${await this.getWorkspacesApiUrl()}/${workspaceName}`; + const directResponse: AxiosResponse = await this.processRequestHandler.get(directUrl); + if (directResponse.status === 200) { + Logger.debug(`Found exact match: ${workspaceName}`); + return workspaceName; + } + } catch (error) { + // workspace not found by exact name, will search by prefix with suffix pattern + Logger.debug(`Exact match not found for ${workspaceName}, searching by prefix with suffix`); + } + + // if exact match not found, get all workspaces and search by prefix + dash + suffix pattern + const allWorkspacesResponse: AxiosResponse = await this.processRequestHandler.get(await this.getWorkspacesApiUrl()); + if (allWorkspacesResponse.status !== 200) { + throw new Error(`Cannot get workspaces list. Code: ${allWorkspacesResponse.status} Data: ${allWorkspacesResponse.data}`); + } + + const workspaces: Array<{ metadata: { name: string } }> = allWorkspacesResponse.data.items || []; + // look for workspace with pattern: workspaceName + '-' + suffix (e.g., 'test-workspace-2-4fnq') + // this ensures we don't match 'test-workspace-20' when looking for 'test-workspace-2' + const matchingWorkspaces: Array<{ metadata: { name: string } }> = workspaces.filter((ws): boolean => { + const dwName: string = ws.metadata.name; + // check if name starts with workspaceName followed by a dash + if (dwName.startsWith(workspaceName + '-')) { + // verify that what follows the dash looks like a random suffix (lowercase letters/numbers) + const suffix: string = dwName.substring(workspaceName.length + 1); + return suffix.length > 0 && /^[a-z0-9]+$/.test(suffix); + } + return false; + }); + + if (matchingWorkspaces.length === 1) { + Logger.debug(`Found workspace by prefix: ${matchingWorkspaces[0].metadata.name} (requested: ${workspaceName})`); + return matchingWorkspaces[0].metadata.name; + } + + if (matchingWorkspaces.length > 1) { + const names: string = matchingWorkspaces.map((ws): string => ws.metadata.name).join(', '); + throw new Error( + `Multiple workspaces found matching '${workspaceName}': ${names}. Please use exact DevWorkspace name or delete duplicates.` + ); + } + + throw new Error(`Workspace not found: ${workspaceName} (tried exact match and prefix search)`); + } + private async obtainUserNamespace(): Promise { Logger.debug(`${this.userNamespace}`); if (this.userNamespace.length === 0) {