Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,67 @@ describe('WorkspaceActionsDropdown', () => {
);
});
});
describe('action: refresh kubeconfig', () => {
test('refresh kubeconfig', async () => {
mockHandleAction.mockResolvedValueOnce(undefined);

workspace.status = DevWorkspaceStatus.RUNNING;
renderComponent(workspace, 'kebab-toggle', { isExpanded: true });

const actionRefreshKubeconfig = screen.queryByRole('menuitem', {
name: 'Action: Refresh Kubeconfig',
});

expect(actionRefreshKubeconfig).not.toBeNull();

await user.click(actionRefreshKubeconfig!);

await jest.advanceTimersByTimeAsync(1000);

expect(mockShowConfirmation).not.toHaveBeenCalled();
expect(mockHandleAction).toHaveBeenCalledWith(
WorkspaceAction.REFRESH_KUBECONFIG,
workspace.uid,
);
expect(mockOnAction).toHaveBeenCalledWith(
WorkspaceAction.REFRESH_KUBECONFIG,
workspace.uid,
true, // succeeded
);
});
test('dont render refresh kubeconfig action for non-running workspace', async () => {
workspace.status = DevWorkspaceStatus.STOPPED;
renderComponent(workspace, 'kebab-toggle', { isExpanded: true });

const actionRefreshKubeconfig = screen.queryByRole('menuitem', {
name: 'Action: Refresh Kubeconfig',
});

expect(actionRefreshKubeconfig).not.toBeNull();

await user.click(actionRefreshKubeconfig!);

await jest.advanceTimersByTimeAsync(1000);

expect(mockShowConfirmation).not.toHaveBeenCalled();
expect(mockHandleAction).not.toHaveBeenCalled();
});
test('dont render refresh kubeconfig action if workspace is in Terminating status', async () => {
workspace.status = DevWorkspaceStatus.TERMINATING;
renderComponent(workspace, 'kebab-toggle', { isExpanded: true });

const actionRefreshKubeconfig = screen.queryByRole('menuitem', {
name: 'Action: Refresh Kubeconfig',
});

await user.click(actionRefreshKubeconfig!);

await jest.advanceTimersByTimeAsync(1000);

expect(mockShowConfirmation).not.toHaveBeenCalled();
expect(mockHandleAction).not.toHaveBeenCalled();
});
});
});

describe('actions status', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ class WorkspaceActionsDropdownComponent extends React.PureComponent<Props, State
getItem(WorkspaceAction.RESTART_WORKSPACE, isTerminating || isStopped),
getItem(WorkspaceAction.STOP_WORKSPACE, isTerminating || isStopped),
getItem(WorkspaceAction.DELETE_WORKSPACE, isTerminating),
getItem(WorkspaceAction.REFRESH_KUBECONFIG, isTerminating || isStopped),
];

// The 'Workspace Details' action is available only with kebab-toggle because this actions widget is used on the workspace details page without kebab-toggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ class WorkspaceActionsProvider extends React.Component<Props, State> {
await this.props.restartWorkspace(workspace);
}
break;
case WorkspaceAction.REFRESH_KUBECONFIG:
{
await this.props.refreshKubeconfigWorkspace(workspace);
}
break;
default:
console.warn(`Unhandled action type: "${action}".`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export enum WorkspaceAction {
START_IN_BACKGROUND = 'Start in background',
STOP_WORKSPACE = 'Stop Workspace',
WORKSPACE_DETAILS = 'Workspace Details',
REFRESH_KUBECONFIG = 'Refresh Kubeconfig',
}

export enum UserPreferencesTab {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,22 @@ describe('Workspaces Actions', () => {
expect(actions[0]).toEqual(workspaceUIDClearAction());
});
});

describe('refreshKubeconfigWorkspace', () => {
it('should dispatch refreshKubeconfigWorkspace action', async () => {
const mockRefreshKubeconfigWorkspace = jest.fn();
jest.spyOn(devWorkspacesActionCreators, 'refreshKubeconfigWorkspace').mockImplementationOnce(
(...args: unknown[]) =>
async () =>
mockRefreshKubeconfigWorkspace(...args),
);

store.dispatch(actionCreators.refreshKubeconfigWorkspace(mockWorkspace));

const actions = store.getActions();
expect(actions).toHaveLength(0);

expect(mockRefreshKubeconfigWorkspace).toHaveBeenCalledWith(mockWorkspace.ref);
});
});
});
6 changes: 6 additions & 0 deletions packages/dashboard-frontend/src/store/Workspaces/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export const actionCreators = {
await dispatch(devWorkspacesActionCreators.restartWorkspace(workspace.ref));
},

refreshKubeconfigWorkspace:
(workspace: Workspace): AppThunk =>
async dispatch => {
await dispatch(devWorkspacesActionCreators.refreshKubeconfigWorkspace(workspace.ref));
},

stopWorkspace:
(workspace: Workspace): AppThunk =>
async dispatch => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2018-2025 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
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { injectKubeConfig, podmanLogin } from '@/services/backend-client/devWorkspaceApi';
import devfileApi from '@/services/devfileApi';
import { DevWorkspaceStatus } from '@/services/helpers/types';
import { RootState } from '@/store';
import { createMockStore } from '@/store/__mocks__/mockActionsTestStore';
import { refreshKubeconfigWorkspace } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/refreshKubeconfigWorkspace';

jest.mock('@eclipse-che/common');
jest.mock('@/store/Workspaces/devWorkspaces/actions/actionCreators');
jest.mock('@/services/backend-client/devWorkspaceApi');

const mockNamespace = 'test-namespace';
const mockName = 'test-workspace';
const mockDevworkspaceId = 'test-devworkspace-id';
const mockWorkspace = {
metadata: {
namespace: mockNamespace,
name: mockName,
uid: mockDevworkspaceId,
},
status: {
phase: DevWorkspaceStatus.RUNNING,
devworkspaceId: mockDevworkspaceId,
},
} as devfileApi.DevWorkspace;

describe('devWorkspaces, actions', () => {
let store: ReturnType<typeof createMockStore>;

beforeEach(() => {
store = createMockStore({
devWorkspaces: {
isLoading: false,
resourceVersion: '',
workspaces: [mockWorkspace],
startedWorkspaces: {},
warnings: {},
},
} as Partial<RootState> as RootState);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('refreshKubeconfigWorkspace', () => {
it('should not call injectKubeConfig if the workspace is not running', async () => {
const mockStoppedWorkspace = {
...mockWorkspace,
status: {
phase: DevWorkspaceStatus.STOPPED,
devworkspaceId: mockDevworkspaceId,
},
};
store.dispatch(refreshKubeconfigWorkspace(mockStoppedWorkspace));
expect(injectKubeConfig).not.toHaveBeenCalledWith(mockNamespace, mockDevworkspaceId);
expect(podmanLogin).not.toHaveBeenCalledWith(mockNamespace, mockDevworkspaceId);
});
it('should call injectKubeConfig if the workspace is running', async () => {
await store.dispatch(refreshKubeconfigWorkspace(mockWorkspace));
expect(injectKubeConfig).toHaveBeenCalledWith(mockNamespace, mockDevworkspaceId);
expect(podmanLogin).toHaveBeenCalledWith(mockNamespace, mockDevworkspaceId);
});
it('should not call injectKubeConfig if devworkspaceId is undefined', async () => {
const mockWorkspaceWithoutId = {
...mockWorkspace,
status: {
phase: DevWorkspaceStatus.RUNNING,
},
} as devfileApi.DevWorkspace;
await store.dispatch(refreshKubeconfigWorkspace(mockWorkspaceWithoutId));
expect(injectKubeConfig).not.toHaveBeenCalled();
expect(podmanLogin).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import { createWorkspaceFromDevfile } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromDevfile';
import { createWorkspaceFromResources } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/createWorkspaceFromResources';
import { handleWebSocketMessage } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/handleWebSocketMessage';
import { refreshKubeconfigWorkspace } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/refreshKubeconfigWorkspace';
import { requestWorkspace } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/requestWorkspace';
import { requestWorkspaces } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/requestWorkspaces';
import { restartWorkspace } from '@/store/Workspaces/devWorkspaces/actions/actionCreators/restartWorkspace';
Expand Down Expand Up @@ -40,4 +41,5 @@ export const actionCreators = {
updateWorkspace,
updateWorkspaceAnnotation,
updateWorkspaceWithDefaultDevfile,
refreshKubeconfigWorkspace,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2018-2025 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
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { injectKubeConfig, podmanLogin } from '@/services/backend-client/devWorkspaceApi';
import devfileApi from '@/services/devfileApi';
import { DevWorkspaceStatus } from '@/services/helpers/types';
import { AppThunk } from '@/store';

export const refreshKubeconfigWorkspace =
(workspace: devfileApi.DevWorkspace): AppThunk =>
async () => {
if (workspace.status?.phase !== DevWorkspaceStatus.RUNNING) {
return;
}
const devworkspaceId = workspace.status?.devworkspaceId;
const namespace = workspace.metadata.namespace;
if (devworkspaceId !== undefined && namespace !== '') {
try {
await injectKubeConfig(namespace, devworkspaceId);
await podmanLogin(namespace, devworkspaceId);
} catch (error) {
console.error(`Failed to refresh kubeconfig for "${workspace.metadata.name}": ${error}`);
}
}
};
Loading