catalog-backend: disallow duplicate entity search keys
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Reject catalog entities that have duplicate fields that vary only in casing.
|
||||
@@ -209,6 +209,11 @@ export class Stitcher {
|
||||
entity.metadata.etag = hash;
|
||||
}
|
||||
|
||||
// This may throw if the entity is invalid, so we call it before
|
||||
// the final_entites write, even though we may end up not needing
|
||||
// to write the search index.
|
||||
const searchEntries = buildEntitySearch(entityId, entity);
|
||||
|
||||
const rowsChanged = await this.database<DbFinalEntitiesRow>(
|
||||
'final_entities',
|
||||
)
|
||||
@@ -235,7 +240,6 @@ export class Stitcher {
|
||||
// B writes the entity ->
|
||||
// B writes search ->
|
||||
// A writes search
|
||||
const searchEntries = buildEntitySearch(entityId, entity);
|
||||
await this.database<DbSearchRow>('search')
|
||||
.where({ entity_id: entityId })
|
||||
.delete();
|
||||
|
||||
@@ -163,5 +163,51 @@ describe('buildEntitySearch', () => {
|
||||
{ entity_id: 'eid', key: 'relations.t2', value: 'k:ns/b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('rejects duplicate keys', () => {
|
||||
expect(() =>
|
||||
buildEntitySearch('eid', {
|
||||
relations: [],
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: {
|
||||
name: 'n',
|
||||
Namespace: 'ns',
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Entity has duplicate keys that vary only in casing, 'metadata.namespace'`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
buildEntitySearch('eid', {
|
||||
relations: [
|
||||
{ type: 'dup', target: { kind: 'k', namespace: 'ns', name: 'a' } },
|
||||
{ type: 'DUP', target: { kind: 'k', namespace: 'ns', name: 'b' } },
|
||||
],
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: { name: 'n' },
|
||||
}),
|
||||
).toThrow(
|
||||
`Entity has duplicate keys that vary only in casing, 'relations.dup'`,
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
buildEntitySearch('eid', {
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
metadata: { name: 'n' },
|
||||
spec: {
|
||||
owner: 'o',
|
||||
OWNER: 'o',
|
||||
lifecycle: 'production',
|
||||
lifeCycle: 'production',
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
`Entity has duplicate keys that vary only in casing, 'spec.owner', 'spec.lifecycle'`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { DbSearchRow } from '../database/tables';
|
||||
|
||||
// These are excluded in the generic loop, either because they do not make sense
|
||||
@@ -184,5 +185,23 @@ export function buildEntitySearch(
|
||||
});
|
||||
}
|
||||
|
||||
// This validates that there are no keys that vary only in casing, such
|
||||
// as `spec.foo` and `spec.Foo`.
|
||||
const keys = new Set(raw.map(r => r.key));
|
||||
const lowerKeys = new Set(raw.map(r => r.key.toLocaleLowerCase('en-US')));
|
||||
if (keys.size !== lowerKeys.size) {
|
||||
const difference = [];
|
||||
for (const key of keys) {
|
||||
const lower = key.toLocaleLowerCase('en-US');
|
||||
if (!lowerKeys.delete(lower)) {
|
||||
difference.push(lower);
|
||||
}
|
||||
}
|
||||
const badKeys = `'${difference.join("', '")}'`;
|
||||
throw new InputError(
|
||||
`Entity has duplicate keys that vary only in casing, ${badKeys}`,
|
||||
);
|
||||
}
|
||||
|
||||
return mapToRows(raw, entityId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user