Skip to content
Merged
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 @@ -142,14 +142,17 @@ describe('DevWorkspace API Service', () => {
const res = await devWorkspaceService.patch(namespace, name, patches);
expect(res.devWorkspace).toStrictEqual(getDevWorkspace());
expect(res.headers).toStrictEqual({});
expect(spyPatchNamespacedCustomObject).toHaveBeenCalledWith({
group: devworkspaceGroup,
version: devworkspaceLatestVersion,
namespace,
plural: devworkspacePlural,
name,
body: patches,
});
expect(spyPatchNamespacedCustomObject).toHaveBeenCalledWith(
{
group: devworkspaceGroup,
version: devworkspaceLatestVersion,
namespace,
plural: devworkspacePlural,
name,
body: patches,
},
expect.anything(),
);
});

test('deleting', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,17 @@ describe('DevWorkspaceTemplate API Service', () => {

const res = await templateService.patch(namespace, name, patches);
expect(res).toEqual(getDevWorkspaceTemplate());
expect(spyPatchNamespacedCustomObject).toHaveBeenCalledWith({
group: devworkspacetemplateGroup,
version: devworkspacetemplateLatestVersion,
namespace,
plural: devworkspacetemplatePlural,
name,
body: patches,
});
expect(spyPatchNamespacedCustomObject).toHaveBeenCalledWith(
{
group: devworkspacetemplateGroup,
version: devworkspacetemplateLatestVersion,
namespace,
plural: devworkspacetemplatePlural,
name,
body: patches,
},
expect.anything(),
);
});

test('deleting', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,16 @@ describe('Workspace Preferences API Service', () => {

expect(spyReadNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace: 'user-che',
body: {
data: { [SKIP_AUTHORIZATION_KEY]: '[github, bitbucket]' },
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace: 'user-che',
body: {
data: { [SKIP_AUTHORIZATION_KEY]: '[github, bitbucket]' },
},
},
});
expect.anything(),
);
});

test('add a very first trusted source URL to trusted-source workspace preferences', async () => {
Expand All @@ -113,16 +116,19 @@ describe('Workspace Preferences API Service', () => {

expect(spyReadNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '["source-url"]',
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '["source-url"]',
},
},
},
});
expect.anything(),
);
});

test('add a new trusted source URL to trusted-source workspace preferences', async () => {
Expand All @@ -136,16 +142,19 @@ describe('Workspace Preferences API Service', () => {

expect(spyReadNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '["source1","source2","source3"]',
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '["source1","source2","source3"]',
},
},
},
});
expect.anything(),
);
});

test('add trust all to trusted-source workspace preferences when there is some trusted URLs', async () => {
Expand All @@ -159,16 +168,19 @@ describe('Workspace Preferences API Service', () => {

expect(spyReadNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '"*"',
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
[TRUSTED_SOURCES_KEY]: '"*"',
},
},
},
});
expect.anything(),
);
});

test('delete all trusted sources from trusted-source workspace preferences', async () => {
Expand All @@ -182,14 +194,17 @@ describe('Workspace Preferences API Service', () => {

expect(spyReadNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalled();
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: DEV_WORKSPACE_PREFERENCES_CONFIGMAP,
namespace,
body: {
data: {
[SKIP_AUTHORIZATION_KEY]: '[]',
},
},
},
});
expect.anything(),
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,21 @@ email=""

expect(spyReadNamespacedConfigMap).toHaveBeenCalledTimes(1);
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledTimes(1);
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith({
name: 'workspace-userdata-gitconfig-configmap',
namespace: 'user-che',
body: {
data: {
gitconfig: `[user]
expect(spyPatchNamespacedConfigMap).toHaveBeenCalledWith(
{
name: 'workspace-userdata-gitconfig-configmap',
namespace: 'user-che',
body: {
data: {
gitconfig: `[user]
email="user-2@che"
name="User Two"
`,
},
},
},
});
expect.anything(),
);
});

it('should throw when can`t read the ConfigMap', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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 { PatchStrategy } from '@kubernetes/client-node';
import {
ConfigurationOptions,
RequestContext,
ResponseContext,
} from '@kubernetes/client-node/dist/gen';
import { wrapOptions } from '@kubernetes/client-node/dist/gen/configuration';

/**
* Creates a middleware that explicitly sets the Content-Type header for patch requests.
* This ensures consistent behavior regardless of @kubernetes/client-node defaults.
*/
function createPatchMiddleware(contentType: string) {
return {
pre: async (context: RequestContext): Promise<RequestContext> => {
context.setHeaderParam('Content-Type', contentType);
return context;
},
post: async (context: ResponseContext): Promise<ResponseContext> => {
return context;
},
};
}

/**
* Configuration options for patch operations on built-in Kubernetes resources (ConfigMaps, etc.).
* Uses strategic-merge-patch for merge patch format: { data: { key: value } }
*/
export const STRATEGIC_MERGE_PATCH_OPTIONS: ConfigurationOptions = wrapOptions({
middleware: [createPatchMiddleware(PatchStrategy.StrategicMergePatch)],
})!;

/**
* Configuration options for patch operations on Custom Resources (DevWorkspaces, etc.).
* Uses json-patch for JSON Patch array format: [{ op: 'replace', path: '...', value: '...' }]
*/
export const JSON_PATCH_OPTIONS: ConfigurationOptions = wrapOptions({
middleware: [createPatchMiddleware(PatchStrategy.JsonPatch)],
})!;
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import * as k8s from '@kubernetes/client-node';

import { STRATEGIC_MERGE_PATCH_OPTIONS } from '@/devworkspaceClient/services/helpers/patchOptions';
import { retryableExec } from '@/devworkspaceClient/services/helpers/retryableExec';

export type CoreV1API = Pick<
Expand Down Expand Up @@ -46,8 +47,8 @@ export function prepareCoreV1API(kc: k8s.KubeConfig): CoreV1API {
retryableExec(() => coreV1API.listNamespacedPod(...args)),
listNamespacedSecret: (...args: Parameters<typeof coreV1API.listNamespacedSecret>) =>
retryableExec(() => coreV1API.listNamespacedSecret(...args)),
patchNamespacedConfigMap: (...args: Parameters<typeof coreV1API.patchNamespacedConfigMap>) =>
retryableExec(() => coreV1API.patchNamespacedConfigMap(...args)),
patchNamespacedConfigMap: (param: Parameters<typeof coreV1API.patchNamespacedConfigMap>[0]) =>
retryableExec(() => coreV1API.patchNamespacedConfigMap(param, STRATEGIC_MERGE_PATCH_OPTIONS)),
readNamespacedConfigMap: (...args: Parameters<typeof coreV1API.readNamespacedConfigMap>) =>
retryableExec(() => coreV1API.readNamespacedConfigMap(...args)),
readNamespacedPod: (...args: Parameters<typeof coreV1API.readNamespacedPod>) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import * as k8s from '@kubernetes/client-node';

import { JSON_PATCH_OPTIONS } from '@/devworkspaceClient/services/helpers/patchOptions';
import { retryableExec } from '@/devworkspaceClient/services/helpers/retryableExec';

export type CustomObjectAPI = Pick<
Expand Down Expand Up @@ -50,7 +51,8 @@ export function prepareCustomObjectAPI(kc: k8s.KubeConfig): CustomObjectAPI {
...args: Parameters<typeof customObjectsApi.deleteNamespacedCustomObject>
) => retryableExec(() => customObjectsApi.deleteNamespacedCustomObject(...args)),
patchNamespacedCustomObject: (
...args: Parameters<typeof customObjectsApi.patchNamespacedCustomObject>
) => retryableExec(() => customObjectsApi.patchNamespacedCustomObject(...args)),
param: Parameters<typeof customObjectsApi.patchNamespacedCustomObject>[0],
) =>
retryableExec(() => customObjectsApi.patchNamespacedCustomObject(param, JSON_PATCH_OPTIONS)),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class WorkspacePreferencesApiService implements IWorkspacePreferencesApi
});
} catch (error) {
const message = `Unable to update workspace preferences in the namespace "${namespace}"`;
throw createError(undefined, ERROR_LABEL, message);
throw createError(error, ERROR_LABEL, message);
}
}

Expand Down