Merge pull request #7003 from backstage/techdocs/beta-local
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': patch
|
||||
---
|
||||
|
||||
Errors encountered while attempting to load TechDocs search indices at
|
||||
collation-time are now logged at `DEBUG` instead of `WARN` level.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
"Local" (out-of-the-box) publisher explicitly follows lower-case entity triplet
|
||||
logic.
|
||||
@@ -25,10 +25,10 @@ import mockFs from 'mock-fs';
|
||||
import * as os from 'os';
|
||||
import { LocalPublish } from './local';
|
||||
|
||||
const createMockEntity = (annotations = {}) => {
|
||||
const createMockEntity = (annotations = {}, lowerCase = false) => {
|
||||
return {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
kind: lowerCase ? 'testkind' : 'TestKind',
|
||||
metadata: {
|
||||
name: 'test-component-name',
|
||||
annotations: {
|
||||
@@ -65,11 +65,42 @@ describe('local publisher', () => {
|
||||
|
||||
const publisher = new LocalPublish(mockConfig, logger, testDiscovery);
|
||||
const mockEntity = createMockEntity();
|
||||
const lowerMockEntity = createMockEntity(undefined, true);
|
||||
|
||||
await publisher.publish({ entity: mockEntity, directory: tmpDir });
|
||||
|
||||
expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true);
|
||||
|
||||
// Lower/upper should be treated the same.
|
||||
expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(true);
|
||||
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should respect legacy casing', async () => {
|
||||
mockFs({
|
||||
[tmpDir]: {
|
||||
'index.html': '',
|
||||
},
|
||||
});
|
||||
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
},
|
||||
});
|
||||
|
||||
const publisher = new LocalPublish(mockConfig, logger, testDiscovery);
|
||||
const mockEntity = createMockEntity();
|
||||
const lowerMockEntity = createMockEntity(undefined, true);
|
||||
|
||||
await publisher.publish({ entity: mockEntity, directory: tmpDir });
|
||||
|
||||
expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true);
|
||||
|
||||
// Lower/upper should be treated differently.
|
||||
expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(false);
|
||||
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
@@ -86,6 +117,13 @@ describe('local publisher', () => {
|
||||
[resolvedDir]: {
|
||||
'unsafe.html': '<html></html>',
|
||||
'unsafe.svg': '<svg></svg>',
|
||||
default: {
|
||||
testkind: {
|
||||
testname: {
|
||||
'index.html': 'found it',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -107,5 +145,38 @@ describe('local publisher', () => {
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect case-sensitive triplet path to lower-case', async () => {
|
||||
const response = await request(app)
|
||||
.get('/default/TestKind/TestName/index.html')
|
||||
.expect('Location', '/default/testkind/testname/index.html');
|
||||
expect(response.status).toBe(301);
|
||||
});
|
||||
|
||||
it('should resolve lower-case triplet path content eventually', async () => {
|
||||
const response = await request(app)
|
||||
.get('/default/TestKind/TestName/index.html')
|
||||
.redirects(1);
|
||||
expect(response.text).toEqual('found it');
|
||||
});
|
||||
|
||||
it('should not redirect when legacy case setting is used', async () => {
|
||||
const legacyConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
legacyUseCaseSensitiveTripletPaths: true,
|
||||
},
|
||||
});
|
||||
const legacyPublisher = new LocalPublish(
|
||||
legacyConfig,
|
||||
logger,
|
||||
testDiscovery,
|
||||
);
|
||||
app = express().use(legacyPublisher.docsRouter());
|
||||
|
||||
const response = await request(app).get(
|
||||
'/default/TestKind/TestName/index.html',
|
||||
);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,8 @@ try {
|
||||
* called "static" at the root of techdocs-backend plugin.
|
||||
*/
|
||||
export class LocalPublish implements PublisherBase {
|
||||
private legacyPathCasing: boolean;
|
||||
|
||||
// TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers.
|
||||
// Move the logic of setting staticDocsDir based on config over to fromConfig,
|
||||
// and set the value as a class parameter.
|
||||
@@ -70,6 +72,10 @@ export class LocalPublish implements PublisherBase {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
this.discovery = discovery;
|
||||
this.legacyPathCasing =
|
||||
config.getOptionalBoolean(
|
||||
'techdocs.legacyUseCaseSensitiveTripletPaths',
|
||||
) || false;
|
||||
}
|
||||
|
||||
async getReadiness(): Promise<ReadinessResponse> {
|
||||
@@ -81,8 +87,7 @@ export class LocalPublish implements PublisherBase {
|
||||
publish({ entity, directory }: PublishRequest): Promise<PublishResponse> {
|
||||
const entityNamespace = entity.metadata.namespace ?? 'default';
|
||||
|
||||
const publishDir = path.join(
|
||||
staticDocsDir,
|
||||
const publishDir = this.staticEntityPathJoin(
|
||||
entityNamespace,
|
||||
entity.kind,
|
||||
entity.metadata.name,
|
||||
@@ -119,8 +124,7 @@ export class LocalPublish implements PublisherBase {
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
): Promise<TechDocsMetadata> {
|
||||
const metadataPath = path.join(
|
||||
staticDocsDir,
|
||||
const metadataPath = this.staticEntityPathJoin(
|
||||
entityName.namespace,
|
||||
entityName.kind,
|
||||
entityName.name,
|
||||
@@ -138,23 +142,61 @@ export class LocalPublish implements PublisherBase {
|
||||
}
|
||||
|
||||
docsRouter(): express.Handler {
|
||||
return express.static(staticDocsDir, {
|
||||
// Handle content-type header the same as all other publishers.
|
||||
setHeaders: (res, filePath) => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const headers = getHeadersForFileExtension(fileExtension);
|
||||
for (const [header, value] of Object.entries(headers)) {
|
||||
res.setHeader(header, value);
|
||||
}
|
||||
},
|
||||
const router = express.Router();
|
||||
|
||||
// Redirect middleware ensuring that requests to case-sensitive entity
|
||||
// triplet paths are always sent to lower-case versions.
|
||||
router.use((req, res, next) => {
|
||||
// If legacy path casing is on, let the request immediately continue.
|
||||
if (this.legacyPathCasing) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Generate a lower-case entity triplet path.
|
||||
const [_, namespace, kind, name, ...rest] = req.path.split('/');
|
||||
|
||||
// Ignore non-triplet objects.
|
||||
if (!namespace || !kind || !name) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const newPath = [
|
||||
_,
|
||||
namespace.toLowerCase(),
|
||||
kind.toLowerCase(),
|
||||
name.toLowerCase(),
|
||||
...rest,
|
||||
].join('/');
|
||||
|
||||
// If there was no change, then let express.static() handle the request.
|
||||
if (newPath === req.path) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Otherwise, redirect to the new path.
|
||||
return res.redirect(req.baseUrl + newPath, 301);
|
||||
});
|
||||
|
||||
router.use(
|
||||
express.static(staticDocsDir, {
|
||||
// Handle content-type header the same as all other publishers.
|
||||
setHeaders: (res, filePath) => {
|
||||
const fileExtension = path.extname(filePath);
|
||||
const headers = getHeadersForFileExtension(fileExtension);
|
||||
for (const [header, value] of Object.entries(headers)) {
|
||||
res.setHeader(header, value);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
|
||||
const namespace = entity.metadata.namespace ?? 'default';
|
||||
|
||||
const indexHtmlPath = path.join(
|
||||
staticDocsDir,
|
||||
const indexHtmlPath = this.staticEntityPathJoin(
|
||||
namespace,
|
||||
entity.kind,
|
||||
entity.metadata.name,
|
||||
@@ -210,4 +252,22 @@ export class LocalPublish implements PublisherBase {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility wrapper around path.join(), used to control legacy case logic.
|
||||
*/
|
||||
protected staticEntityPathJoin(...allParts: string[]): string {
|
||||
if (this.legacyPathCasing) {
|
||||
const [namespace, kind, name, ...parts] = allParts;
|
||||
return path.join(staticDocsDir, namespace, kind, name, ...parts);
|
||||
}
|
||||
const [namespace, kind, name, ...parts] = allParts;
|
||||
return path.join(
|
||||
staticDocsDir,
|
||||
namespace.toLowerCase(),
|
||||
kind.toLowerCase(),
|
||||
name.toLowerCase(),
|
||||
...parts,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
?.target?.name || '',
|
||||
}));
|
||||
} catch (e) {
|
||||
this.logger.warn(
|
||||
this.logger.debug(
|
||||
`Failed to retrieve tech docs search index for entity ${entityInfo.namespace}/${entityInfo.kind}/${entityInfo.name}`,
|
||||
e,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user