Skip to content

Commit c5a69aa

Browse files
authored
fix(backend): restore user OAuth token injection in workspace terminals (CRW-11193) (#1606)
* fix(backend): add polling fallback in PostStartInjector If the Kubernetes Watch fails to start, fall back to polling the DevWorkspace via GET every 10 s for up to 300 s. Inject on Running; stop without injection on Failed, Failing, Stopped, Stopping, or Terminating. While polling is active the workspace key remains in activeWatches to prevent duplicate start attempts. Fixes: CRW-11193 Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com> * refactor(backend): reuse watchInNamespace in PostStartInjector Replace the standalone k8s.Watch in PostStartInjector with devworkspaceApi.watchInNamespace() — the same code path used by the WebSocket SUBSCRIBE DEV_WORKSPACE channel. Benefits: - No separate watch infrastructure; reuses existing DevWorkspace service - User OAuth token for both watch and polling (passed via devworkspaceApi) - stopWatching() called after injection (clean unsubscribe) - Shared injectCredentials() helper removes duplication between watch-path and polling-fallback Fixes: CRW-11193 Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com> * test(common): cover DevWorkspaceStatus re-export getter to fix 100% function coverage (CRW-11193) The export { DevWorkspaceStatus } from './dto/api' in src/index.ts compiles to an Object.defineProperty getter that Istanbul counts as a function. The existing test never accessed DevWorkspaceStatus via the package index, leaving that getter uncovered and dropping function coverage to 96% (24/25), which failed the 100% threshold enforced by jest config. Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Orel <oorel@redhat.com> --------- Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent 12f196f commit c5a69aa

10 files changed

Lines changed: 392 additions & 250 deletions

File tree

packages/common/src/__tests__/index.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
* Red Hat, Inc. - initial API and implementation
1111
*/
1212

13-
import common from '../';
13+
import common, { DevWorkspaceStatus } from '../';
1414

1515
describe('Common', () => {
1616
it('should export all shared code', () => {
1717
expect(common).toBeDefined();
1818
expect(common.helpers).toBeDefined();
1919
});
20+
21+
it('should export DevWorkspaceStatus', () => {
22+
expect(DevWorkspaceStatus).toBeDefined();
23+
expect(DevWorkspaceStatus.RUNNING).toBe('Running');
24+
});
2025
});

packages/common/src/dto/api/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,16 @@ export type IEditors = Array<V230Devfile>;
222222
export type IEventList = CoreV1EventList;
223223
export type IPodList = V1PodList;
224224

225+
export enum DevWorkspaceStatus {
226+
FAILED = 'Failed',
227+
FAILING = 'Failing',
228+
STARTING = 'Starting',
229+
TERMINATING = 'Terminating',
230+
RUNNING = 'Running',
231+
STOPPED = 'Stopped',
232+
STOPPING = 'Stopping',
233+
}
234+
225235
export interface IDevWorkspaceList {
226236
apiVersion?: string;
227237
kind?: string;

packages/common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export * from './dto/cluster-info';
1717
export * from './dto/cluster-config';
1818
export * from './types';
1919
export * from './constants';
20+
export { DevWorkspaceStatus } from './dto/api';
2021

2122
export { helpers, api };
2223

packages/dashboard-backend/src/routes/api/__tests__/devworkspaces.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ describe('DevWorkspaces Routes', () => {
8080

8181
const { PostStartInjector } = jest.requireMock('@/services/PostStartInjector');
8282
expect(PostStartInjector.watchAndInject).toHaveBeenCalledWith(
83-
expect.any(Object),
8483
namespace,
8584
workspaceName,
85+
expect.objectContaining({ watchInNamespace: expect.any(Function) }),
8686
expect.objectContaining({ injectKubeConfig: expect.any(Function) }),
8787
expect.objectContaining({ podmanLogin: expect.any(Function) }),
8888
);
@@ -132,9 +132,9 @@ describe('DevWorkspaces Routes', () => {
132132

133133
const { PostStartInjector } = jest.requireMock('@/services/PostStartInjector');
134134
expect(PostStartInjector.watchAndInject).toHaveBeenCalledWith(
135-
expect.any(Object),
136135
namespace,
137136
workspaceName,
137+
expect.objectContaining({ watchInNamespace: expect.any(Function) }),
138138
expect.objectContaining({ injectKubeConfig: expect.any(Function) }),
139139
expect.objectContaining({ podmanLogin: expect.any(Function) }),
140140
);

packages/dashboard-backend/src/routes/api/devworkspaces.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
namespacedWorkspaceSchema,
2222
} from '@/constants/schemas';
2323
import { restParams } from '@/models';
24-
import { getDevWorkspaceClient, getKubeConfig } from '@/routes/api/helpers/getDevWorkspaceClient';
24+
import { getDevWorkspaceClient } from '@/routes/api/helpers/getDevWorkspaceClient';
2525
import { getToken } from '@/routes/api/helpers/getToken';
2626
import { getSchema } from '@/services/helpers';
2727
import { PostStartInjector } from '@/services/PostStartInjector';
@@ -68,11 +68,14 @@ export function registerDevworkspacesRoutes(instance: FastifyInstance) {
6868
// PATCH /spec/started=true (restart), missing first-time creation.
6969
const workspaceName = devWorkspace.metadata?.name;
7070
if (devworkspace.spec?.started === true && workspaceName) {
71-
const kc = getKubeConfig(token);
71+
// Pass a fresh devworkspaceApi instance (DevWorkspaceClient.devworkspaceApi is a
72+
// getter — each access returns a new DevWorkspaceApiService). PostStartInjector
73+
// calls stopWatching() on this instance when done; passing the same instance that
74+
// the route already used for .create() would cancel the route's own watch.
7275
PostStartInjector.watchAndInject(
73-
kc,
7476
namespace,
7577
workspaceName,
78+
dwClient.devworkspaceApi,
7679
dwClient.kubeConfigApi,
7780
dwClient.podmanApi,
7881
);
@@ -111,11 +114,11 @@ export function registerDevworkspacesRoutes(instance: FastifyInstance) {
111114

112115
const isStarting = patch.some(p => p.path === '/spec/started' && p.value === true);
113116
if (isStarting) {
114-
const kc = getKubeConfig(token);
117+
// See comment above — pass a fresh devworkspaceApi instance.
115118
PostStartInjector.watchAndInject(
116-
kc,
117119
namespace,
118120
workspaceName,
121+
dwClient.devworkspaceApi,
119122
dwClient.kubeConfigApi,
120123
dwClient.podmanApi,
121124
);

packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export const getDevWorkspaceClient = jest.fn(
240240
listInNamespace: _namespace => Promise.resolve(stubDevWorkspacesList),
241241
patch: (_namespace, _name, _patches) =>
242242
Promise.resolve({ devWorkspace: stubDevWorkspace, headers: stubHeaders }),
243+
watchInNamespace: (_listener, _params) => Promise.resolve(),
244+
stopWatching: () => undefined,
243245
} as IDevWorkspaceApi,
244246
dockerConfigApi: {
245247
read: _namespace => Promise.resolve(stubDockerConfig),

0 commit comments

Comments
 (0)