Merge pull request #4714 from erdoganoksuz/feature/OpenStackSwiftPublisher
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
|
||||
const checkFileExists = async (Key: string): Promise<boolean> => {
|
||||
// Key will always have / as file separator irrespective of OS since cloud providers expects /.
|
||||
// Normalize Key to OS specific path before checking if file exists.
|
||||
const filePath = path.join(rootDir, Key);
|
||||
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.F_OK);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class PkgCloudStorageClient {
|
||||
getFile(
|
||||
containerName: string,
|
||||
file: string,
|
||||
callback: (err: any, file: string) => any,
|
||||
) {
|
||||
checkFileExists(file).then(res => {
|
||||
if (!res) {
|
||||
callback('File does not exist', file);
|
||||
throw new Error('File does not exist');
|
||||
} else {
|
||||
callback(undefined, 'success');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getContainer(
|
||||
containerName: string,
|
||||
callback: (err: string, container: string) => any,
|
||||
) {
|
||||
if (containerName !== 'mock') {
|
||||
callback('Container does not exist', containerName);
|
||||
throw new Error('Container does not exist');
|
||||
} else {
|
||||
callback('Container does not exist', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
upload({ remote }: { remote: string }) {
|
||||
const filePath = path.join(rootDir, remote);
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
emitter.emit('success');
|
||||
(emitter as any).end = () => true;
|
||||
} else {
|
||||
emitter.emit(
|
||||
'error',
|
||||
new Error(`The file ${filePath} does not exist !`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
download({ remote }: { remote: string }) {
|
||||
const filePath = path.join(rootDir, remote);
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (fs.existsSync(filePath)) {
|
||||
emitter.emit('data', Buffer.from(fs.readFileSync(filePath)));
|
||||
emitter.emit('end');
|
||||
} else {
|
||||
emitter.emit(
|
||||
'error',
|
||||
new Error(`The file ${filePath} does not exist !`),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return emitter;
|
||||
}
|
||||
}
|
||||
|
||||
export class storage {
|
||||
static createClient() {
|
||||
return new PkgCloudStorageClient();
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@
|
||||
"mime-types": "^2.1.27",
|
||||
"mock-fs": "^4.13.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"pkgcloud": "^2.2.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
@@ -66,6 +67,7 @@
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/pkgcloud": "^1.7.4",
|
||||
"@types/recursive-readdir": "^2.2.0"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import {
|
||||
Entity,
|
||||
EntityName,
|
||||
ENTITY_DEFAULT_NAMESPACE,
|
||||
} from '@backstage/catalog-model';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import * as winston from 'winston';
|
||||
import { OpenStackSwiftPublish } from './openStackSwift';
|
||||
import { PublisherBase, TechDocsMetadata } from './types';
|
||||
|
||||
// NOTE: /packages/techdocs-common/__mocks__ is being used to mock pkgcloud client library
|
||||
|
||||
const createMockEntity = (annotations = {}): Entity => {
|
||||
return {
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'test-component-name',
|
||||
namespace: 'test-namespace',
|
||||
annotations: {
|
||||
...annotations,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createMockEntityName = (): EntityName => ({
|
||||
kind: 'TestKind',
|
||||
name: 'test-component-name',
|
||||
namespace: 'test-namespace',
|
||||
});
|
||||
|
||||
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
|
||||
const getEntityRootDir = (entity: Entity) => {
|
||||
const {
|
||||
kind,
|
||||
metadata: { namespace, name },
|
||||
} = entity;
|
||||
|
||||
return path.join(rootDir, namespace || ENTITY_DEFAULT_NAMESPACE, kind, name);
|
||||
};
|
||||
|
||||
const logger = winston.createLogger();
|
||||
jest.spyOn(logger, 'info').mockReturnValue(logger);
|
||||
jest.spyOn(logger, 'error').mockReturnValue(logger);
|
||||
|
||||
let publisher: PublisherBase;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs.restore();
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'openStackSwift',
|
||||
openStackSwift: {
|
||||
credentials: {
|
||||
username: 'mockuser',
|
||||
password: 'verystrongpass',
|
||||
},
|
||||
authUrl: 'mockauthurl',
|
||||
region: 'mockregion',
|
||||
containerName: 'mock',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger);
|
||||
});
|
||||
|
||||
describe('OpenStackSwiftPublish', () => {
|
||||
describe('publish', () => {
|
||||
beforeEach(() => {
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'index.html': '',
|
||||
'404.html': '',
|
||||
assets: {
|
||||
'main.css': '',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should publish a directory', async () => {
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
expect(
|
||||
await publisher.publish({
|
||||
entity,
|
||||
directory: entityRootDir,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fail to publish a directory', async () => {
|
||||
expect.assertions(3);
|
||||
const wrongPathToGeneratedDirectory = path.join(
|
||||
rootDir,
|
||||
'wrong',
|
||||
'path',
|
||||
'to',
|
||||
'generatedDirectory',
|
||||
);
|
||||
|
||||
const entity = createMockEntity();
|
||||
await expect(
|
||||
publisher.publish({
|
||||
entity,
|
||||
directory: wrongPathToGeneratedDirectory,
|
||||
}),
|
||||
).rejects.toThrowError();
|
||||
|
||||
await publisher
|
||||
.publish({
|
||||
entity,
|
||||
directory: wrongPathToGeneratedDirectory,
|
||||
})
|
||||
.catch(error => {
|
||||
expect(error.message).toEqual(
|
||||
// Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error
|
||||
// Issue reported https://github.com/tschaub/mock-fs/issues/118
|
||||
expect.stringContaining(
|
||||
`Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`,
|
||||
),
|
||||
);
|
||||
expect(error.message).toEqual(
|
||||
expect.stringContaining(wrongPathToGeneratedDirectory),
|
||||
);
|
||||
});
|
||||
mockFs.restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasDocsBeenGenerated', () => {
|
||||
it('should return true if docs has been generated', async () => {
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'index.html': 'file-content',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(true);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should return false if docs has not been generated', async () => {
|
||||
const entity = createMockEntity();
|
||||
|
||||
expect(await publisher.hasDocsBeenGenerated(entity)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTechDocsMetadata', () => {
|
||||
it('should return tech docs metadata', async () => {
|
||||
const entityNameMock = createMockEntityName();
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json':
|
||||
'{"site_name": "backstage", "site_description": "site_content"}',
|
||||
},
|
||||
});
|
||||
|
||||
const expectedMetadata: TechDocsMetadata = {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
).toStrictEqual(expectedMetadata);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should return tech docs metadata when json encoded with single quotes', async () => {
|
||||
const entityNameMock = createMockEntityName();
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
mockFs({
|
||||
[entityRootDir]: {
|
||||
'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content'}`,
|
||||
},
|
||||
});
|
||||
|
||||
const expectedMetadata: TechDocsMetadata = {
|
||||
site_name: 'backstage',
|
||||
site_description: 'site_content',
|
||||
};
|
||||
expect(
|
||||
await publisher.fetchTechDocsMetadata(entityNameMock),
|
||||
).toStrictEqual(expectedMetadata);
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should return an error if the techdocs_metadata.json file is not present', async () => {
|
||||
const entityNameMock = createMockEntityName();
|
||||
const entity = createMockEntity();
|
||||
const entityRootDir = getEntityRootDir(entity);
|
||||
|
||||
await publisher
|
||||
.fetchTechDocsMetadata(entityNameMock)
|
||||
.catch(error =>
|
||||
expect(error).toEqual(
|
||||
new Error(
|
||||
`TechDocs metadata fetch failed, The file ${path.join(
|
||||
entityRootDir,
|
||||
'techdocs_metadata.json',
|
||||
)} does not exist !`,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import { storage } from 'pkgcloud';
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
import JSON5 from 'json5';
|
||||
import createLimiter from 'p-limit';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
|
||||
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
|
||||
|
||||
const streamToBuffer = (stream: Readable): Promise<Buffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const chunks: any[] = [];
|
||||
stream.on('data', chunk => chunks.push(chunk));
|
||||
stream.on('error', reject);
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to parse the response data ${e.message}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export class OpenStackSwiftPublish implements PublisherBase {
|
||||
static fromConfig(config: Config, logger: Logger): PublisherBase {
|
||||
let containerName = '';
|
||||
try {
|
||||
containerName = config.getString(
|
||||
'techdocs.publisher.openStackSwift.containerName',
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Since techdocs.publisher.type is set to 'openStackSwift' in your app config, " +
|
||||
'techdocs.publisher.openStackSwift.containerName is required.',
|
||||
);
|
||||
}
|
||||
|
||||
const openStackSwiftConfig = config.getConfig(
|
||||
'techdocs.publisher.openStackSwift',
|
||||
);
|
||||
|
||||
const storageClient = storage.createClient({
|
||||
provider: 'openstack',
|
||||
username: openStackSwiftConfig.getString('credentials.username'),
|
||||
password: openStackSwiftConfig.getString('credentials.password'),
|
||||
authUrl: openStackSwiftConfig.getString('authUrl'),
|
||||
keystoneAuthVersion:
|
||||
openStackSwiftConfig.getOptionalString('keystoneAuthVersion') || 'v3',
|
||||
domainId: openStackSwiftConfig.getOptionalString('domainId') || 'default',
|
||||
domainName:
|
||||
openStackSwiftConfig.getOptionalString('domainName') || 'Default',
|
||||
region: openStackSwiftConfig.getString('region'),
|
||||
});
|
||||
|
||||
// Check if the defined container exists. Being able to connect means the configuration is good
|
||||
// and the storage client will work.
|
||||
storageClient.getContainer(containerName, (err, container) => {
|
||||
if (container) {
|
||||
logger.info(
|
||||
`Successfully connected to the OpenStack Swift container ${containerName}.`,
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`Could not retrieve metadata about the OpenStack Swift container ${containerName}. ` +
|
||||
'Make sure the container exists. Also make sure that authentication is setup either by ' +
|
||||
'explicitly defining credentials and region in techdocs.publisher.openStackSwift in app config or ' +
|
||||
'by using environment variables. Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',
|
||||
);
|
||||
|
||||
logger.error(`from OpenStack client library: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
return new OpenStackSwiftPublish(storageClient, containerName, logger);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly storageClient: storage.Client,
|
||||
private readonly containerName: string,
|
||||
private readonly logger: Logger,
|
||||
) {
|
||||
this.storageClient = storageClient;
|
||||
this.containerName = containerName;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload all the files from the generated `directory` to the OpenStack Swift container.
|
||||
* Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html
|
||||
*/
|
||||
async publish({ entity, directory }: PublishRequest): Promise<void> {
|
||||
try {
|
||||
// Note: OpenStack Swift manages creation of parent directories if they do not exist.
|
||||
// So collecting path of only the files is good enough.
|
||||
const allFilesToUpload = await getFileTreeRecursively(directory);
|
||||
const limiter = createLimiter(10);
|
||||
const uploadPromises: Array<Promise<unknown>> = [];
|
||||
for (const filePath of allFilesToUpload) {
|
||||
// Remove the absolute path prefix of the source directory
|
||||
// Path of all files to upload, relative to the root of the source directory
|
||||
// e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']
|
||||
const relativeFilePath = path.relative(directory, filePath);
|
||||
|
||||
// Convert destination file path to a POSIX path for uploading.
|
||||
// Swift expects / as path separator and relativeFilePath will contain \\ on Windows.
|
||||
// https://docs.openstack.org/python-openstackclient/pike/cli/man/openstack.html
|
||||
const relativeFilePathPosix = relativeFilePath
|
||||
.split(path.sep)
|
||||
.join(path.posix.sep);
|
||||
|
||||
// The / delimiter is intentional since it represents the cloud storage and not the local file system.
|
||||
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
|
||||
const destination = `${entityRootDir}/${relativeFilePathPosix}`; // Swift container file relative path
|
||||
|
||||
const params = {
|
||||
container: this.containerName,
|
||||
remote: destination,
|
||||
};
|
||||
|
||||
// Rate limit the concurrent execution of file uploads to batches of 10 (per publish)
|
||||
const uploadFile = limiter(
|
||||
() =>
|
||||
new Promise((res, rej) => {
|
||||
const readStream = fs.createReadStream(filePath, 'utf8');
|
||||
|
||||
const writeStream = this.storageClient.upload(params);
|
||||
|
||||
writeStream.on('error', rej);
|
||||
|
||||
writeStream.on('success', res);
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
}),
|
||||
);
|
||||
uploadPromises.push(uploadFile);
|
||||
}
|
||||
await Promise.all(uploadPromises);
|
||||
this.logger.info(
|
||||
`Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${allFilesToUpload.length}`,
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
const errorMessage = `Unable to upload file(s) to OpenStack Swift. ${e}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async fetchTechDocsMetadata(
|
||||
entityName: EntityName,
|
||||
): Promise<TechDocsMetadata> {
|
||||
try {
|
||||
return await new Promise<TechDocsMetadata>(async (resolve, reject) => {
|
||||
const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;
|
||||
|
||||
const stream = this.storageClient.download({
|
||||
container: this.containerName,
|
||||
remote: `${entityRootDir}/techdocs_metadata.json`,
|
||||
});
|
||||
|
||||
try {
|
||||
const techdocsMetadataJson = await streamToBuffer(stream);
|
||||
if (!techdocsMetadataJson) {
|
||||
throw new Error(
|
||||
`Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`,
|
||||
);
|
||||
}
|
||||
|
||||
const techdocsMetadata = JSON5.parse(
|
||||
techdocsMetadataJson.toString('utf-8'),
|
||||
);
|
||||
|
||||
resolve(techdocsMetadata);
|
||||
} catch (err) {
|
||||
this.logger.error(err.message);
|
||||
reject(new Error(err.message));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`TechDocs metadata fetch failed, ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Express route middleware to serve static files on a route in techdocs-backend.
|
||||
*/
|
||||
docsRouter(): express.Handler {
|
||||
return async (req, res) => {
|
||||
// Trim the leading forward slash
|
||||
// filePath example - /default/Component/documented-component/index.html
|
||||
|
||||
const filePath = req.path.replace(/^\//, '');
|
||||
|
||||
// Files with different extensions (CSS, HTML) need to be served with different headers
|
||||
const fileExtension = path.extname(filePath);
|
||||
const responseHeaders = getHeadersForFileExtension(fileExtension);
|
||||
|
||||
const stream = this.storageClient.download({
|
||||
container: this.containerName,
|
||||
remote: filePath,
|
||||
});
|
||||
|
||||
try {
|
||||
// Inject response headers
|
||||
for (const [headerKey, headerValue] of Object.entries(
|
||||
responseHeaders,
|
||||
)) {
|
||||
res.setHeader(headerKey, headerValue);
|
||||
}
|
||||
|
||||
res.send(await streamToBuffer(stream));
|
||||
} catch (err) {
|
||||
this.logger.warn(err.message);
|
||||
res.status(404).send(err.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function which checks if index.html of an Entity's docs site is available. This
|
||||
* can be used to verify if there are any pre-generated docs available to serve.
|
||||
*/
|
||||
async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {
|
||||
try {
|
||||
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
|
||||
|
||||
return new Promise(res => {
|
||||
this.storageClient.getFile(
|
||||
this.containerName,
|
||||
`${entityRootDir}/index.html`,
|
||||
(err, file) => {
|
||||
if (!err && file) {
|
||||
res(true);
|
||||
} else {
|
||||
res(false);
|
||||
this.logger.warn(err.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { LocalPublish } from './local';
|
||||
import { GoogleGCSPublish } from './googleStorage';
|
||||
import { AwsS3Publish } from './awsS3';
|
||||
import { AzureBlobStoragePublish } from './azureBlobStorage';
|
||||
import { OpenStackSwiftPublish } from './openStackSwift';
|
||||
|
||||
const logger = getVoidLogger();
|
||||
const discovery: jest.Mocked<PluginEndpointDiscovery> = {
|
||||
@@ -161,4 +162,30 @@ describe('Publisher', () => {
|
||||
});
|
||||
expect(publisher).toBeInstanceOf(AzureBlobStoragePublish);
|
||||
});
|
||||
|
||||
it('should create Open Stack Swift publisher from config', async () => {
|
||||
const mockConfig = new ConfigReader({
|
||||
techdocs: {
|
||||
requestUrl: 'http://localhost:7000',
|
||||
publisher: {
|
||||
type: 'openStackSwift',
|
||||
openStackSwift: {
|
||||
credentials: {
|
||||
username: 'mockuser',
|
||||
password: 'verystrongpass',
|
||||
},
|
||||
authUrl: 'mockauthurl',
|
||||
region: 'mockregion',
|
||||
containerName: 'mock',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const publisher = await Publisher.fromConfig(mockConfig, {
|
||||
logger,
|
||||
discovery,
|
||||
});
|
||||
expect(publisher).toBeInstanceOf(OpenStackSwiftPublish);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import { LocalPublish } from './local';
|
||||
import { GoogleGCSPublish } from './googleStorage';
|
||||
import { AwsS3Publish } from './awsS3';
|
||||
import { AzureBlobStoragePublish } from './azureBlobStorage';
|
||||
import { OpenStackSwiftPublish } from './openStackSwift';
|
||||
|
||||
type factoryOptions = {
|
||||
logger: Logger;
|
||||
@@ -53,6 +54,11 @@ export class Publisher {
|
||||
'Creating Azure Blob Storage Container publisher for TechDocs',
|
||||
);
|
||||
return AzureBlobStoragePublish.fromConfig(config, logger);
|
||||
case 'openStackSwift':
|
||||
logger.info(
|
||||
'Creating OpenStack Swift Container publisher for TechDocs',
|
||||
);
|
||||
return OpenStackSwiftPublish.fromConfig(config, logger);
|
||||
case 'local':
|
||||
logger.info('Creating Local publisher for TechDocs');
|
||||
return new LocalPublish(config, logger, discovery);
|
||||
|
||||
@@ -23,7 +23,8 @@ export type PublisherType =
|
||||
| 'local'
|
||||
| 'googleGcs'
|
||||
| 'awsS3'
|
||||
| 'azureBlobStorage';
|
||||
| 'azureBlobStorage'
|
||||
| 'openStackSwift';
|
||||
|
||||
export type PublishRequest = {
|
||||
entity: Entity;
|
||||
|
||||
Reference in New Issue
Block a user