feat(kubernetes-plugin): add secrets rendering (#31415)

* feat: mask secret datas

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* feat: add secrets accordion

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* feat: add test code for secrets accordion

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* feat: add test code for secrets fetch

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* chore: changeset

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* chore: yarn build:api-reports

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* chore: remove secrets from default object and rollback test code

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* chore: extract to helper function

Signed-off-by: 김병준 <kingbj0429@gmail.com>

* chore: add test code for helper function

Signed-off-by: 김병준 <kingbj0429@gmail.com>

---------

Signed-off-by: 김병준 <kingbj0429@gmail.com>
This commit is contained in:
KoB
2025-12-02 09:35:34 +09:00
committed by GitHub
parent f966a8557f
commit 8fa8d87e3b
15 changed files with 568 additions and 7 deletions
@@ -1263,5 +1263,160 @@ describe('KubernetesFetcher', () => {
]);
expect(result.responses).toMatchObject(POD_METRICS_FIXTURE);
});
it('should mask secret data values with ***', async () => {
worker.use(
rest.get('http://localhost:9999/api/v1/secrets', (req, res, ctx) =>
res(
checkToken(req, ctx, 'token'),
withLabels(req, ctx, {
items: [
{
metadata: { name: 'secret-name' },
data: {
username: 'dXNlcm5hbWU=',
password: 'cGFzc3dvcmQ=',
apiKey: 'YXBpS2V5',
},
},
],
}),
),
),
);
const result = await sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {},
},
credential: { type: 'bearer token', token: 'token' },
objectTypesToFetch: new Set([
{
group: '',
apiVersion: 'v1',
plural: 'secrets',
objectType: 'secrets',
},
]),
labelSelector: '',
customResources: [],
});
expect(result).toStrictEqual({
errors: [],
responses: [
{
type: 'secrets',
resources: [
{
metadata: {
name: 'secret-name',
labels: {},
},
data: {
username: '***',
password: '***',
apiKey: '***',
},
},
],
},
],
});
});
it('should handle secrets without data field', async () => {
worker.use(
rest.get('http://localhost:9999/api/v1/secrets', (req, res, ctx) =>
res(
checkToken(req, ctx, 'token'),
withLabels(req, ctx, {
items: [
{
metadata: { name: 'secret-without-data' },
},
],
}),
),
),
);
const result = await sut.fetchObjectsForService({
serviceId: 'some-service',
clusterDetails: {
name: 'cluster1',
url: 'http://localhost:9999',
authMetadata: {},
},
credential: { type: 'bearer token', token: 'token' },
objectTypesToFetch: new Set([
{
group: '',
apiVersion: 'v1',
plural: 'secrets',
objectType: 'secrets',
},
]),
labelSelector: '',
customResources: [],
});
expect(result).toStrictEqual({
errors: [],
responses: [
{
type: 'secrets',
resources: [
{
metadata: {
name: 'secret-without-data',
labels: {},
},
},
],
},
],
});
});
});
describe('transformResources', () => {
let fetcher: KubernetesClientBasedFetcher;
beforeEach(() => {
fetcher = new KubernetesClientBasedFetcher({
logger: mockServices.logger.mock(),
});
});
it('removes List suffix from custom resource kinds', () => {
const result = (fetcher as any).transformResources(
'customresources',
'HelloWorldList',
[{ name: 'foo' }],
);
expect(result[0].kind).toBe('HelloWorld');
});
it('masks secret data values', () => {
const result = (fetcher as any).transformResources(
'secrets',
'SecretList',
[{ data: { password: 'secret123' } }],
);
expect(result[0].data.password).toBe('***');
});
it('returns other types unchanged', () => {
const items = [{ name: 'pod' }];
const result = (fetcher as any).transformResources(
'pods',
'PodList',
items,
);
expect(result).toBe(items);
});
});
});
@@ -103,13 +103,7 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
? r.json().then(
({ kind, items }): FetchResponse => ({
type: objectType,
resources:
objectType === 'customresources'
? items.map((item: JsonObject) => ({
...item,
kind: kind.replace(/(List)$/, ''),
}))
: items,
resources: this.transformResources(objectType, kind, items),
}),
)
: this.handleUnsuccessfulResponse(params.clusterDetails.name, r),
@@ -320,4 +314,33 @@ export class KubernetesClientBasedFetcher implements KubernetesFetcher {
}
return [url, requestInit];
}
private transformResources(
objectType: string,
kind: string,
items: JsonObject[],
): JsonObject[] {
if (objectType === 'customresources') {
return items.map((item: JsonObject) => ({
...item,
kind: kind.replace(/(List)$/, ''),
}));
}
if (objectType === 'secrets') {
return items.map((item: JsonObject) => {
if (item.data && typeof item.data === 'object') {
return {
...item,
data: Object.fromEntries(
Object.keys(item.data).map(key => [key, '***']),
),
};
}
return item;
});
}
return items;
}
}