feat(config-loader): Fix handling of keys with backslashes.

Signed-off-by: Aramis <sennyeyaramis@gmail.com>
This commit is contained in:
Aramis
2023-05-21 19:22:12 -04:00
parent a1d8f1ad4f
commit f25427f665
6 changed files with 109 additions and 12 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/config-loader': patch
---
Fix a bug where config items with `/` in the key were incorrectly handled.
@@ -170,4 +170,64 @@ describe('compileConfigSchemas', () => {
visibilityBySchemaPath: new Map(),
});
});
it('should handle slashes correctly', () => {
const validate = compileConfigSchemas([
{
path: 'a1',
value: {
type: 'object',
properties: {
'/circleci/api': {
type: 'object',
properties: {
target: {
type: 'string',
},
headers: {
type: 'object',
properties: {
'Circle-Token': {
type: 'string',
visibility: 'secret',
},
},
},
},
},
'/gocd': { type: 'string', visibility: 'backend' },
},
},
},
]);
expect(
validate([
{
data: {
'/circleci/api': {
target: 'test',
headers: {
'Circle-Token': 'my-token',
},
},
'/gocd': 'test',
},
context: 'test',
},
]),
).toEqual({
visibilityByDataPath: new Map(
Object.entries({
'//circleci/api/headers/Circle-Token': 'secret',
}),
),
visibilityBySchemaPath: new Map(
Object.entries({
'/properties//circleci/api/properties/headers/properties/Circle-Token':
'secret',
}),
),
deprecationByDataPath: new Map(),
});
});
});
+4 -9
View File
@@ -26,6 +26,7 @@ import {
ConfigVisibility,
} from './types';
import { SchemaObject } from 'json-schema-traverse';
import { normalizeAjvPath } from './utils';
/**
* This takes a collection of Backstage configuration schemas from various
@@ -66,10 +67,7 @@ export function compileConfigSchemas(
return false;
}
if (visibility && visibility !== 'backend') {
const normalizedPath = context.instancePath.replace(
/\['?(.*?)'?\]/g,
(_, segment) => `/${segment}`,
);
const normalizedPath = normalizeAjvPath(context.instancePath);
visibilityByDataPath.set(normalizedPath, visibility);
}
return true;
@@ -85,10 +83,7 @@ export function compileConfigSchemas(
if (context?.instancePath === undefined) {
return false;
}
const normalizedPath = context.instancePath.replace(
/\['?(.*?)'?\]/g,
(_, segment) => `/${segment}`,
);
const normalizedPath = normalizeAjvPath(context.instancePath);
// create mapping of deprecation description and data path of property
deprecationByDataPath.set(normalizedPath, deprecationDescription);
return true;
@@ -123,7 +118,7 @@ export function compileConfigSchemas(
const visibilityBySchemaPath = new Map<string, ConfigVisibility>();
traverse(merged, (schema, path) => {
if (schema.visibility && schema.visibility !== 'backend') {
visibilityBySchemaPath.set(path, schema.visibility);
visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility);
}
});
@@ -21,6 +21,7 @@ import {
TransformFunc,
ValidationError,
} from './types';
import { normalizeAjvPath } from './utils';
/**
* This filters data by visibility by discovering the visibility of each
@@ -163,7 +164,10 @@ export function filterErrorsByVisibility(
// We don't use this method for all the errors as the data path is more robust
// and doesn't require us to properly trim the schema path.
if (error.keyword === 'required') {
const trimmedPath = error.schemaPath.slice(1, -'/required'.length);
const trimmedPath = normalizeAjvPath(error.schemaPath).slice(
1,
-'/required'.length,
);
const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`;
if (
visibleSchemaPaths.some(visiblePath => visiblePath.startsWith(fullPath))
@@ -173,7 +177,8 @@ export function filterErrorsByVisibility(
}
const vis =
visibilityByDataPath.get(error.instancePath) ?? DEFAULT_CONFIG_VISIBILITY;
visibilityByDataPath.get(normalizeAjvPath(error.instancePath)) ??
DEFAULT_CONFIG_VISIBILITY;
return vis && includeVisibilities.includes(vis);
});
}
+4 -1
View File
@@ -25,6 +25,7 @@ import {
ConfigSchemaPackageEntry,
CONFIG_VISIBILITIES,
} from './types';
import { normalizeAjvPath } from './utils';
/**
* Options that control the loading of configuration schema files in the backend.
@@ -49,7 +50,9 @@ function errorsToError(errors: ValidationError[]): Error {
const paramStr = Object.entries(params)
.map(([name, value]) => `${name}=${value}`)
.join(' ');
return `Config ${message || ''} { ${paramStr} } at ${instancePath}`;
return `Config ${message || ''} { ${paramStr} } at ${normalizeAjvPath(
instancePath,
)}`;
});
const error = new Error(`Config validation failed, ${messages.join('; ')}`);
(error as any).messages = messages;
@@ -0,0 +1,29 @@
/*
* Copyright 2023 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.
*/
/**
* AJV does some encoding to schema and instance paths. Revert those encodings to
* easier to read and parse values.
* See https://github.com/ajv-validator/ajv/blob/master/lib/compile/util.ts#L69.
*
* @param path Path from AJV.
* @returns Updated path with correct characters.
*/
export function normalizeAjvPath(path: string) {
return path
.replace(/~1/g, '/')
.replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
}