Merge pull request #26748 from 04kash/long-url

Modify locations and refresh-keys Tables to Support Long URLs
This commit is contained in:
Fredrik Adelöw
2024-10-09 20:18:59 +02:00
committed by GitHub
7 changed files with 216 additions and 4 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-backend': patch
---
Added migration `20241003170511_alter_target_in_locations.js` to change the target column in the `locations` table to TEXT type.
Added a hash for the key column in the `refresh_keys` table.
@@ -0,0 +1,47 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// @ts-check
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function up(knex) {
await knex.schema.alterTable('locations', table => {
table.text('target').alter();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function down(knex) {
const oversizedEntries = await knex('locations').where(
knex.raw('LENGTH(target) > 255'),
);
if (oversizedEntries.length > 0) {
throw new Error(
`Migration aborted: Found ${oversizedEntries.length} entries with 'target' exceeding 255 characters. Manual intervention required.`,
);
}
await knex.schema.alterTable('locations', table => {
table.string('target').alter();
});
};
@@ -480,7 +480,7 @@ describe('DefaultProcessingDatabase', () => {
);
it.each(databases.eachSupportedId())(
'stores the refresh keys for the entity',
'stores the refresh keys for the entity where key length is 255 chars or less',
async databaseId => {
const mockLogger = {
debug: jest.fn(),
@@ -535,6 +535,67 @@ describe('DefaultProcessingDatabase', () => {
});
},
);
it.each(databases.eachSupportedId())(
'stores the refresh keys for the entity where key length is greater than 255 chars',
async databaseId => {
const mockLogger = {
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
};
const { knex, db } = await createDatabase(
databaseId,
mockLogger as unknown as Logger,
);
await insertRefreshStateRow(knex, {
entity_id: id,
entity_ref: 'location:default/fakelocation',
unprocessed_entity: '{}',
processed_entity: '{}',
errors: '[]',
next_update_at: '2021-04-01 13:37:00',
last_discovery_at: '2021-04-01 13:37:00',
});
const deferredEntities = [
{
entity: {
apiVersion: '1',
kind: 'Location',
metadata: {
name: 'next',
},
},
locationKey: 'mock',
},
];
await db.transaction(tx =>
db.updateProcessedEntity(tx, {
id,
processedEntity,
resultHash: '',
relations: [],
deferredEntities,
refreshKeys: [
{
key: `url:https://example.com/foo-bar-test-group/very-long-group-name-that-exceeds-255-characters-just-to-test-the-limits-of-url-length-in-the-catalog-info-yaml-file-and-see-how-the-backstage-system-handles-it-making/test-this-alright-1/-/blob/main/catalog-info.yaml`,
},
],
}),
);
const refreshKeys = await knex<DbRefreshKeysRow>('refresh_keys')
.where({ entity_id: id })
.select();
expect(refreshKeys[0]).toEqual({
entity_id: id,
key: `url:https://example.com/foo-bar-test-group/very-long-group-name-that-exceeds-255-characters-just-to-test-the-limits-of-url-length-in-the-catalog-info-yaml-file-and-see-how-the-back#sha256:edfb606500d184900e63891e5279d35bf0069ea251e90d15c0a430de6023d905`,
});
},
);
});
describe('updateEntityCache', () => {
@@ -41,7 +41,7 @@ import {
import { checkLocationKeyConflict } from './operations/refreshState/checkLocationKeyConflict';
import { insertUnprocessedEntity } from './operations/refreshState/insertUnprocessedEntity';
import { updateUnprocessedEntity } from './operations/refreshState/updateUnprocessedEntity';
import { generateStableHash } from './util';
import { generateStableHash, generateTargetKey } from './util';
import {
EventBroker,
EventParams,
@@ -158,7 +158,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase {
'refresh_keys',
refreshKeys.map(k => ({
entity_id: id,
key: k.key,
key: generateTargetKey(k.key),
})),
BATCH_SIZE,
);
@@ -16,6 +16,7 @@
import { Knex } from 'knex';
import { DbRefreshStateRow } from '../../tables';
import { generateTargetKey } from '../../util';
/**
* Schedules a future refresh of entities, by so called "refresh keys" that may
@@ -30,10 +31,12 @@ export async function refreshByRefreshKeys(options: {
}): Promise<void> {
const { tx, keys } = options;
const hashedKeys = keys.map(k => generateTargetKey(k));
await tx<DbRefreshStateRow>('refresh_state')
.whereIn('entity_id', function selectEntityRefs(inner) {
inner
.whereIn('key', keys)
.whereIn('key', hashedKeys)
.select({
entity_id: 'refresh_keys.entity_id',
})
@@ -23,3 +23,11 @@ export function generateStableHash(entity: Entity) {
.update(stableStringify({ ...entity }))
.digest('hex');
}
export function generateTargetKey(target: string) {
return target.length > 255
? `${target.slice(0, 180)}#sha256:${createHash('sha256')
.update(target)
.digest('hex')}`
: target;
}
@@ -308,4 +308,91 @@ describe('migrations', () => {
await knex.destroy();
},
);
it.each(databases.eachSupportedId())(
'20241003170511_alter_target_in_locations.js, %p',
async databaseId => {
const knex = await databases.init(databaseId);
await migrateUntilBefore(
knex,
'20241003170511_alter_target_in_locations.js',
);
// Insert a simple URL before the migration
await knex
.insert({
id: '1f99f7f6-1d87-4e8c-994a-8a2ba1d542f6',
type: 'url',
target: 'https://example.com',
})
.into('locations');
await migrateUpOnce(knex);
// Verify that the target column is now 'text'
const columnInfo = await knex('locations').columnInfo();
expect(columnInfo.target.type).toBe('text');
// Insert a long URL (exceeding 255 characters)
await knex
.insert({
id: '1f99f6f7-1d87-4e8c-994a-2a8ba1d542f6',
type: 'url',
target:
'https://example.com/foo-bar-test-group/very-long-group-name-that-exceeds-255-characters-just-to-test-the-limits-of-url-length-in-the-catalog-info-yaml-file-and-see-how-the-backstage-system-handles-it-making/test-this-alright-1/-/blob/main/catalog-info.yaml',
})
.into('locations');
// Verify that both the simple and long URLs exist after the migration
await expect(knex('locations').where({ type: 'url' })).resolves.toEqual(
expect.arrayContaining([
{
id: '1f99f7f6-1d87-4e8c-994a-8a2ba1d542f6',
target: 'https://example.com',
type: 'url',
},
{
id: '1f99f6f7-1d87-4e8c-994a-2a8ba1d542f6',
target:
'https://example.com/foo-bar-test-group/very-long-group-name-that-exceeds-255-characters-just-to-test-the-limits-of-url-length-in-the-catalog-info-yaml-file-and-see-how-the-backstage-system-handles-it-making/test-this-alright-1/-/blob/main/catalog-info.yaml',
type: 'url',
},
]),
);
await expect(migrateDownOnce(knex)).rejects.toThrow(
`Migration aborted: Found 1 entries with 'target' exceeding 255 characters. Manual intervention required.`,
);
// Now remove the long URL
await knex('locations')
.where({
id: '1f99f6f7-1d87-4e8c-994a-2a8ba1d542f6',
})
.del();
// Retry the migration down after removing the long URL
await migrateDownOnce(knex);
// Verify that the column type has reverted to varchar
const revertedColumnInfo = await knex('locations').columnInfo();
expect(revertedColumnInfo.target.type).toMatch(
/^(varchar|character varying)$/,
);
// Verify that the short URL still exists in the table
await expect(knex('locations').where({ type: 'url' })).resolves.toEqual(
expect.arrayContaining([
{
id: '1f99f7f6-1d87-4e8c-994a-8a2ba1d542f6',
target: 'https://example.com',
type: 'url',
},
]),
);
await knex.destroy();
},
);
});