ApiServerClient: Avoid special characters in generateName (#107331)

This commit is contained in:
Ivan Ortega Alba
2025-06-27 16:24:13 +00:00
committed by GitHub
parent dbef739814
commit 45dabd2862
2 changed files with 227 additions and 5 deletions
+223 -2
View File
@@ -1,12 +1,23 @@
import { getBackendSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { DatasourceAPIVersions } from './client';
import { DatasourceAPIVersions, ScopedResourceClient } from './client';
import { GroupVersionResource } from './types';
jest.mock('@grafana/runtime', () => ({
getBackendSrv: jest.fn().mockReturnValue({
get: jest.fn(),
post: jest.fn(),
}),
config: {},
config: {
buildInfo: { versionString: 'test-version' },
},
}));
jest.mock('app/core/services/context_srv');
jest.mock('../../api/utils', () => ({
getAPINamespace: jest.fn().mockReturnValue('default'),
}));
describe('DatasourceAPIVersions', () => {
@@ -33,3 +44,213 @@ describe('DatasourceAPIVersions', () => {
expect(getMock).toHaveBeenCalledWith('/apis');
});
});
describe('ScopedResourceClient', () => {
let client: ScopedResourceClient;
let postMock: jest.Mock;
const gvr: GroupVersionResource = { group: 'test.grafana.app', version: 'v1', resource: 'testresources' };
beforeEach(() => {
jest.clearAllMocks();
postMock = jest.fn().mockResolvedValue({ metadata: { name: 'created-resource' } });
(getBackendSrv as jest.Mock).mockReturnValue({
post: postMock,
});
client = new ScopedResourceClient(gvr);
});
describe('create', () => {
it('should generate name prefix from user login with alphabetic characters', async () => {
contextSrv.user.login = 'john.doe123';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'jo',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should handle login with special characters and numbers', async () => {
contextSrv.user.login = 'user@example.com';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'us',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should handle login starting with numbers', async () => {
contextSrv.user.login = '123admin456';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'ad',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should handle login with only one alphabetic character', async () => {
contextSrv.user.login = '123a456';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'a',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should handle login with no alphabetic characters', async () => {
contextSrv.user.login = '12345@#$%';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'g',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should fall back to "g" when no login exists', async () => {
// @ts-expect-error - we want to test the fallback behavior
contextSrv.user.login = null;
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'g',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should fall back to "g" when login is undefined', async () => {
// @ts-expect-error - we want to test the fallback behavior
contextSrv.user.login = undefined;
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'g',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should fall back to "g" when login is empty string', async () => {
contextSrv.user.login = '';
const obj = { metadata: {}, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'g',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
it('should not set generateName when metadata.name is already provided', async () => {
contextSrv.user.login = 'testuser';
const obj = { metadata: { name: 'existing-name' }, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
name: 'existing-name',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
expect(postMock.mock.calls[0][1].metadata).not.toHaveProperty('generateName');
});
it('should not set generateName when metadata.generateName is already provided', async () => {
contextSrv.user.login = 'testuser';
const obj = { metadata: { generateName: 'existing-prefix' }, spec: {} };
await client.create(obj);
expect(postMock).toHaveBeenCalledWith(
'/apis/test.grafana.app/v1/namespaces/default/testresources',
{
metadata: {
generateName: 'existing-prefix',
annotations: { 'grafana.app/saved-from-ui': 'test-version' },
},
spec: {},
},
{ params: undefined }
);
});
});
});
+4 -3
View File
@@ -113,9 +113,10 @@ export class ScopedResourceClient<T = object, S = object, K = string> implements
public async create(obj: ResourceForCreate<T, K>, params?: ResourceClientWriteParams): Promise<Resource<T, S, K>> {
if (!obj.metadata.name && !obj.metadata.generateName) {
const login = contextSrv.user.login;
// GenerateName lets the apiserver create a new uid for the name
// THe passed in value is the suggested prefix
obj.metadata.generateName = login ? login.slice(0, 2) : 'g';
// GenerateName lets the apiserver create a unique name by appending random characters to the prefix.
// This strips out special characters, numbers, and symbols to ensure a valid prefix.
const alphabeticChars = login ? login.replace(/[^a-zA-Z]/g, '').slice(0, 2) : '';
obj.metadata.generateName = alphabeticChars || 'g';
}
setSavedFromUIAnnotation(obj.metadata);
return getBackendSrv().post(this.url, obj, {