Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add bmc serve to create a server from a bundled config #2306

Merged
merged 2 commits into from
Jul 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/cli/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CommandJobCreate } from './cogify/action.job.js';
import { CommandBundle } from './config/action.bundle.js';
import { CommandImport } from './config/action.import.js';
import { CommandScreenShot } from './screenshot/action.screenshot.js';
import { CommandServe } from './server/action.serve.js';
import { CommandSprites } from './sprites/action.sprites.js';

export class BasemapsConfigCommandLine extends BaseCommandLine {
Expand All @@ -25,6 +26,7 @@ export class BasemapsConfigCommandLine extends BaseCommandLine {
this.addAction(new CommandScreenShot());

this.addAction(new CommandSprites());
this.addAction(new CommandServe());

this.addAction(new CommandList());
}
Expand Down
26 changes: 7 additions & 19 deletions packages/cli/src/cli/screenshot/action.screenshot.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Config, Env, fsa, LogConfig, LogType } from '@basemaps/shared';
import { createServer } from '@basemaps/server';
import { Env, fsa, LogConfig, LogType } from '@basemaps/shared';
import { CommandLineAction, CommandLineStringParameter } from '@rushstack/ts-command-line';
import { FastifyInstance } from 'fastify/types/instance';
import { mkdir } from 'fs/promises';
import getPort from 'get-port';
import { Browser, chromium } from 'playwright';
import { CommandLineAction, CommandLineStringParameter } from '@rushstack/ts-command-line';
import { z } from 'zod';
import getPort from 'get-port';
import { createServer } from '@basemaps/server';
import { FastifyInstance } from 'fastify/types/instance';
import { ConfigBundled, ConfigProviderMemory } from '@basemaps/config';

export const DefaultTestTiles = './test-tiles/default.test.tiles.json';
export const DefaultHost = 'basemaps.linz.govt.nz';
Expand Down Expand Up @@ -108,22 +107,11 @@ export class CommandScreenShot extends CommandLineAction {
}

async startServer(port: number, config: string, logger: LogType): Promise<FastifyInstance> {
// Bundle Config
const configJson = await fsa.readJson<ConfigBundled>(config);
const mem = ConfigProviderMemory.fromJson(configJson);
Config.setConfigProvider(mem);

// Setup the assets
if (this.assets.value) {
const isExists = await fsa.exists(this.assets.value);
if (!isExists) throw new Error('--asset path is missing');
process.env[Env.AssetLocation] = this.assets.value;
}

// Start server
const server = createServer(logger);
const server = await createServer({ config, assets: this.assets.value }, logger);
return await new Promise<FastifyInstance>((resolve) => server.listen(port, '0.0.0.0', () => resolve(server)));
}

async takeScreenshots(host: string, chrome: Browser, logger: LogType): Promise<void> {
const tiles = this.tiles.value ?? DefaultTestTiles;
const outputPath = this.output.value ?? DefaultOutput;
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/src/cli/server/action.serve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CommandLineAction } from '@rushstack/ts-command-line';

import { createServer } from '@basemaps/server';
import { Const, Env, LogConfig } from '@basemaps/shared';
const DefaultPort = 5000;

export class CommandServe extends CommandLineAction {
config = this.defineStringParameter({
argumentName: 'CONFIG',
parameterLongName: '--config',
description: 'Configuration source to use',
});
assets = this.defineStringParameter({
argumentName: 'ASSETS',
parameterLongName: '--assets',
description: 'Where the assets (sprites, fonts) are located',
});
port = this.defineIntegerParameter({
argumentName: 'PORT',
parameterLongName: '--port',
description: 'port to use',
defaultValue: DefaultPort,
});

public constructor() {
super({
actionName: 'serve',
summary: 'Cli tool to create sprite sheet',
documentation: 'Create a sprite sheet from a folder of sprites',
});
}

protected onDefineParameters(): void {
//noop
}

protected async onExecute(): Promise<void> {
const logger = LogConfig.get();

const config = this.config.value ?? 'dynamodb://' + Const.TileMetadata.TableName;
const port = this.port.value;
const assets = this.assets.value;

// Force a default url base so WMTS requests know their relative url
const ServerUrl = Env.get(Env.PublicUrlBase) ?? `http://localhost:${port}`;
process.env[Env.PublicUrlBase] = ServerUrl;

const server = await createServer({ config, assets }, logger);
server.listen(port ?? DefaultPort, '0.0.0.0', () => {
logger.info({ url: ServerUrl }, 'ServerStarted');
});
}
}
105 changes: 5 additions & 100 deletions packages/server/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import { ConfigJson, ConfigProvider, ConfigProviderDynamo, ConfigProviderMemory } from '@basemaps/config';
import { TileSetLocal } from '@basemaps/lambda-tiler/build/cli/tile.set.local.js';
import { TileSets } from '@basemaps/lambda-tiler/build/tile.set.cache.js';
import { Config, Const, Env, fsa, LogConfig, LogType } from '@basemaps/shared';
import { Const, Env, LogConfig } from '@basemaps/shared';
import { BaseCommandLine, CliInfo } from '@basemaps/shared/build/cli/base.js';
import { basename, dirname } from 'path';
import { createServer } from './server.js';

CliInfo.package = 'basemaps/server';

const BaseProvider: ConfigProvider = {
id: 'pv_linz',
version: 1,
serviceIdentification: {},
serviceProvider: {
name: 'basemaps/server',
contact: {
address: {},
},
},
} as any;

const DefaultPort = 5000;

export class BasemapsServerCommand extends BaseCommandLine {
Expand All @@ -28,15 +12,6 @@ export class BasemapsServerCommand extends BaseCommandLine {
parameterLongName: '--config',
description: 'Configuration source to use',
});
ignoreConfig = this.defineFlagParameter({
parameterLongName: '--no-config',
description: 'Assume no config and just load tiffs from the configuration path',
});
bundle = this.defineStringParameter({
argumentName: 'BUNDLE',
parameterLongName: '--bundle',
description: 'Compile the configuration into a bundle and output path',
});
assets = this.defineStringParameter({
argumentName: 'ASSETS',
parameterLongName: '--assets',
Expand All @@ -51,97 +26,27 @@ export class BasemapsServerCommand extends BaseCommandLine {

constructor() {
super({
toolFilename: 'bms',
toolFilename: 'basemaps-server',
toolDescription: 'Create a WMTS/XYZ Tile server from basemaps config',
});
}

static args = [];

async loadTiffs(tiffPath: string, serverUrl: string, logger: LogType): Promise<void> {
const config = new ConfigProviderMemory();
Config.setConfigProvider(config);

const tifSets = new Map<string, TileSetLocal>();

for await (const file of fsa.details(tiffPath)) {
const lowerPath = file.path.toLowerCase();
if (lowerPath.endsWith('.tiff') || lowerPath.endsWith('.tif')) {
const tiffPath = dirname(file.path);
if (tifSets.has(tiffPath)) continue;

const tileSet = basename(tiffPath);
const tsl = new TileSetLocal(tileSet, tiffPath);
tifSets.set(tiffPath, tsl);

await tsl.load();
TileSets.add(tsl, new Date('3000-01-01').getTime());

const wmtsUrl = `${serverUrl}/v1/tiles/${tileSet}/WMTSCapabilities.xml`;
logger.info({ tileSetId: tileSet, wmtsUrl }, 'TileSet:Loaded');
if (!config.objects.has('pv_linz')) config.put(BaseProvider);
}
}
}

async onExecute(): Promise<void> {
await super.onExecute();

const logger = LogConfig.get();
logger.level = 'debug';
const config = this.config.value ?? 'dynamodb://' + Const.TileMetadata.TableName;
const port = this.port.value;
const assets = this.assets.value;

const ServerUrl = Env.get(Env.PublicUrlBase) ?? `http://localhost:${port}`;
// Force a default url base so WMTS requests know their relative url
process.env[Env.PublicUrlBase] = ServerUrl;

if (config.startsWith('dynamodb://')) {
// Load config from dynamodb table
const table = config.slice('dynamodb://'.length);
logger.info({ path: config, table, mode: 'dynamo' }, 'Starting Server');
Config.setConfigProvider(new ConfigProviderDynamo(table));
} else if (config.endsWith('.json')) {
// Bundled config
logger.info({ path: config, mode: 'config' }, 'Starting Server');
const configJson = await fsa.read(config);
const mem = ConfigProviderMemory.fromJson(JSON.parse(configJson.toString()));
Config.setConfigProvider(mem);
} else if (this.ignoreConfig.value) {
// Load config directly from tiff files
logger.info({ path: config, mode: 'tiffs' }, 'Starting Server');
await this.loadTiffs(config, ServerUrl, logger);
} else {
const mem = await ConfigJson.fromPath(config, logger);
const bundlePath = this.bundle.value;
if (bundlePath) {
await fsa.writeJson(bundlePath, mem.toJson());
logger.info({ path: bundlePath }, 'ConfigBundled');
return;
}
// Assume the folder is a collection of config files
logger.info({ path: config, mode: 'config' }, 'Starting Server');

mem.createVirtualTileSets();
Config.setConfigProvider(mem);
}

if (this.assets.value) {
const isExists = await fsa.exists(this.assets.value);
if (!isExists) throw new Error('--asset path is missing');
process.env[Env.AssetLocation] = this.assets.value;
}

if (this.bundle.value != null) {
throw new Error('--bundle path provided without providing a configuration path');
}

createServer(logger).listen(port ?? DefaultPort, '0.0.0.0', () => {
const server = await createServer({ config, assets }, logger);
server.listen(port ?? DefaultPort, '0.0.0.0', () => {
logger.info({ url: ServerUrl }, 'ServerStarted');
});
}
}

new BasemapsServerCommand().executeWithoutErrorHandling().catch((c) => {
console.error(c);
});
38 changes: 36 additions & 2 deletions packages/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Config, ConfigJson, ConfigProviderDynamo, ConfigProviderMemory } from '@basemaps/config';
import { handler } from '@basemaps/lambda-tiler';
import { LogType } from '@basemaps/shared';
import { Env, fsa, LogType } from '@basemaps/shared';
import fastifyStatic from '@fastify/static';
import { lf } from '@linzjs/lambda';
import { ALBEvent, ALBResult, APIGatewayProxyResultV2, CloudFrontRequestResult, Context } from 'aws-lambda';
Expand Down Expand Up @@ -30,9 +31,42 @@ function getLandingLocation(): string | null {
}
}

export function createServer(logger: LogType): FastifyInstance {
export interface ServerOptions {
/** Path to assets */
assets?: string;

/** Path to configuration or a dynamdb table */
config: string;
}

export async function createServer(opts: ServerOptions, logger: LogType): Promise<FastifyInstance> {
const BasemapsServer = fastify();

if (opts.config.startsWith('dynamodb://')) {
// Load config from dynamodb table
const table = opts.config.slice('dynamodb://'.length);
logger.info({ path: opts.config, table, mode: 'dynamo' }, 'Starting Server');
Config.setConfigProvider(new ConfigProviderDynamo(table));
} else if (opts.config.endsWith('.json')) {
// Bundled config
logger.info({ path: opts.config, mode: 'config:bundle' }, 'Starting Server');
const configJson = await fsa.read(opts.config);
const mem = ConfigProviderMemory.fromJson(JSON.parse(configJson.toString()));
Config.setConfigProvider(mem);
} else {
const mem = await ConfigJson.fromPath(opts.config, logger);
logger.info({ path: opts.config, mode: 'config' }, 'Starting Server');
mem.createVirtualTileSets();
Config.setConfigProvider(mem);
}

if (opts.assets) {
const isExists = await fsa.exists(opts.assets);
if (!isExists) throw new Error(`--assets path "${opts.assets}" does not exist`);
logger.info({ path: opts.assets }, 'Config:Assets');
process.env[Env.AssetLocation] = opts.assets;
}

const landingLocation = getLandingLocation();
if (landingLocation == null) {
logger.warn('Server:Landing:Failed');
Expand Down