blob: 8da2dc581c49f2bdb2c6105636928ad146e7230d [file] [log] [blame]
/*
* 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 { execSync } from "child_process";
import { existsSync, readFileSync } from "fs";
/**
* ServerVersion contains versioning information for the server,
* consistent with what other components provide, even if some
* of it doesn't really make sense for a Node server.
*/
export interface ServerVersion {
/** The ATC version of the server e.g. 5.0.0 */
version: string;
/**
* The number of commits in the development branch that produced this
* version of ATC - if known.
*/
commits?: string;
/**
* The hash of the commit at which this ATC instance was built - if known.
*/
hash?: string;
/**
* The "Enterprise Linux" release for which this version of ATC was built
* (which should never actually matter for Traffic Portal) - if known.
*/
elRelease?: string;
/**
* The CPU architecture for which this version of ATC was built (which
* should REALLY never matter for Traffic Portal) - if known.
*/
arch?: string;
}
/** Converts the given version to a string. */
export function versionToString(v: ServerVersion): string {
let ret = `traffic-portal-${v.version}`;
// Can't allow either-or, because since git hashes could
// possibly be entirely numeric that would make the case
// where only one is present confusing - is it commit
// count or hash?
if (v.commits) {
ret += `-${v.commits}`;
if (v.hash) {
ret += `.${v.hash}`;
}
}
if (v.elRelease) {
ret += `.${v.elRelease}`;
}
if (v.arch) {
ret += `.${v.arch}`;
}
return ret;
}
function isServerVersion(v: unknown): v is ServerVersion {
// tslint:disable completed-docs
if (typeof(v) !== "object") {
console.error("version does not represent a server version");
return false;
}
if (!v) {
console.error("'null' is not a valid server version");
return false;
}
if (!Object.prototype.hasOwnProperty.call(v, "version")) {
console.error("version missing required field 'version'");
return false;
}
if (typeof((v as {version: unknown; }).version) !== "string") {
return false;
}
if (Object.prototype.hasOwnProperty.call(v, "commits") && (typeof((v as {commits: unknown; }).commits)) !== "string") {
console.error(`version property 'commits' has incorrect type; want: string, got: ${typeof((v as {commits: unknown; }).commits)}`);
return false;
}
if (Object.prototype.hasOwnProperty.call(v, "hash") && (typeof((v as {hash: unknown; }).hash)) !== "string") {
console.error(`version property 'hash' has incorrect type; want: string, got: ${typeof((v as {hash: unknown; }).hash)}`);
return false;
}
if (Object.prototype.hasOwnProperty.call(v, "elRelease") && (typeof((v as {elRelease: unknown; }).elRelease)) !== "string") {
console.error(`version property 'elRelease' has incorrect type; want: string, got: ${typeof((v as {elRelease: unknown; }).elRelease)}`);
return false;
}
if (Object.prototype.hasOwnProperty.call(v, "arch") && (typeof((v as {arch: unknown; }).arch)) !== "string") {
console.error(`version property 'arch' has incorrect type; want: string, got: ${typeof((v as {arch: unknown; }).arch)}`);
return false;
}
return true;
// tslint:enable completed-docs
}
interface BaseConfig {
/** Whether or not SSL certificate errors from Traffic Ops will be ignored. */
insecure?: boolean;
/** The port on which Traffic Portal listens. */
port: number;
/** The URL of the Traffic Ops API. */
trafficOps: URL;
/** Whether or not to serve HTTPS */
useSSL?: boolean;
/** Contains all of the versioning information. */
version: ServerVersion;
}
interface ConfigWithSSL {
/** The path to the SSL certificate Traffic Portal will use. */
certPath: string;
/** The path to the SSL private key Traffic Portal will use. */
keyPath: string;
/** Whether or not to serve HTTPS */
useSSL: true;
}
interface ConfigWithoutSSL {
/** Whether or not to serve HTTPS */
useSSL?: false;
}
/** ServerConfig holds server configuration. */
export type ServerConfig = BaseConfig & (ConfigWithSSL | ConfigWithoutSSL);
/**
* isConfig checks specifically the contents of configuration files
* passed through JSON.parse, so it doesn't validate the 'version', since
* that doesn't need to be in the configuration file.
*/
function isConfig(c: unknown): c is ServerConfig {
// tslint:disable completed-docs
if (typeof(c) !== "object") {
throw new Error("configuration is not an object");
}
if (!c) {
throw new Error("'null' is not a valid configuration");
}
if (Object.prototype.hasOwnProperty.call(c, "insecure")) {
if (typeof((c as {insecure: unknown; }).insecure) !== "boolean") {
throw new Error("'insecure' must be a boolean");
}
} else {
(c as {insecure: boolean; }).insecure = false;
}
if (!Object.prototype.hasOwnProperty.call(c, "port")) {
throw new Error("'port' is required");
}
if (typeof((c as {port: unknown; }).port) !== "number") {
throw new Error("'port' must be a number");
}
if (!Object.prototype.hasOwnProperty.call(c, "trafficOps")) {
throw new Error("'trafficOps' is required");
}
if (typeof((c as {trafficOps: unknown; }).trafficOps) !== "string") {
throw new Error("'trafficOps' must be a string");
}
try {
(c as {trafficOps: URL; }).trafficOps = new URL((c as {trafficOps: string; }).trafficOps);
} catch (e) {
throw new Error(`'trafficOps' is not a valid URL: ${e}`);
}
if (Object.prototype.hasOwnProperty.call(c, "useSSL")) {
if (typeof((c as {useSSL: unknown; }).useSSL) !== "boolean") {
throw new Error("'useSSL' must be a boolean");
}
if ((c as {useSSL: boolean; }).useSSL) {
if (!Object.prototype.hasOwnProperty.call(c, "certPath")) {
throw new Error("'certPath' is required to use SSL");
}
if (typeof((c as {certPath: unknown; }).certPath) !== "string") {
throw new Error("'certPath' must be a string");
}
if (!Object.prototype.hasOwnProperty.call(c, "keyPath")) {
throw new Error("'keyPath' is required to use SSL");
}
if (typeof((c as {keyPath: unknown; }).keyPath) !== "string") {
throw new Error("'keyPath' must be a string");
}
}
}
return true;
// tstlint:enable completed-docs
}
const defaultVersionFile = "/etc/traffic-portal/version.json";
/**
* Retrieves the server's version from the file path provided.
*
* @param path The path to a version file containing a ServerVersion object.
* Defaults to /etc/traffic-portal/version.json. If this file doesn't exist,
* the version may be deduced from the execution environment using git and
* looking for the ATC VERSION file.
*/
export function getVersion(path?: string): ServerVersion {
if (!path) {
path = defaultVersionFile;
}
if (existsSync(path)) {
const v = JSON.parse(readFileSync(path, {encoding: "utf8"}));
if (isServerVersion(v)) {
return v;
}
throw new Error(`contents of version file '${path}' does not represent an ATC version`);
}
if (!existsSync("../../VERSION")) {
throw new Error(`'${path}' doesn't exist and '../../VERSION' doesn't exist`);
}
const ver: ServerVersion = {
version: readFileSync("../../VERSION", {encoding: "utf8"}).trimEnd()
};
try {
ver.commits = String(execSync("git rev-list HEAD", {encoding: "utf8"}).split("\n").length);
ver.hash = execSync("git rev-parse --short=8 HEAD", {encoding: "utf8"}).trimEnd();
} catch (e) {
console.warn("getting git parts of version:", e);
}
try {
const releaseNo = execSync("rpm -q --qf '%{version}' -f /etc/redhat-release", {encoding: "utf8"}).trimEnd();
ver.elRelease = `el${releaseNo}`;
} catch (e) {
ver.elRelease = "el7";
console.warn(`getting RHEL version: ${e}`);
}
try {
ver.arch = execSync("uname -m", {encoding: "utf8"});
} catch (e) {
console.warn(`getting system architecture: ${e}`);
}
return ver;
}
interface Args {
trafficOps?: URL;
insecure?: boolean;
port?: number;
certPath?: string;
keyPath?: string;
configFile: string;
}
export const defaultConfigFile = "/etc/traffic-portal/config.js";
export function getConfig(args: Args, ver: ServerVersion): ServerConfig {
let cfg: ServerConfig = {
insecure: false,
port: 4200,
trafficOps: new URL("https://example.com"),
useSSL: false,
version: ver
};
let readFromFile = false;
if (existsSync(args.configFile)) {
const cfgFromFile = JSON.parse(readFileSync(args.configFile, {encoding: "utf8"}));
try {
if (isConfig(cfgFromFile)) {
cfg = cfgFromFile;
cfg.version = ver;
}
} catch (err) {
throw new Error(`invalid configuration file at '${args.configFile}': ${err}`);
}
readFromFile = true;
} else if (args.configFile !== defaultConfigFile) {
throw new Error(`no such configuration file: ${args.configFile}`);
}
if (args.port) {
cfg.port = args.port;
}
if (isNaN(cfg.port) || cfg.port <= 0 || cfg.port > 65535) {
throw new Error(`invalid port: ${cfg.port}`);
}
if (args.trafficOps) {
cfg.trafficOps = args.trafficOps;
} else if (!readFromFile) {
if (Object.prototype.hasOwnProperty.call(process.env, "TO_URL")) {
const envURL = (process.env as {TO_URL: string; }).TO_URL;
try {
cfg.trafficOps = new URL(envURL);
} catch (e) {
throw new Error(`invalid Traffic Ops URL from environment: ${envURL}`);
}
} else {
throw new Error("Traffic Ops URL must be specified");
}
}
if (readFromFile && cfg.useSSL) {
if (args.certPath) {
cfg.certPath = args.certPath;
}
if (args.keyPath) {
cfg.keyPath = args.keyPath;
}
} else if (!readFromFile || cfg.useSSL === undefined) {
if (args.certPath) {
if (!args.keyPath) {
throw new Error("must specify either both a key path and a cert path, or neither");
}
cfg = {
certPath: args.certPath,
insecure: cfg.insecure,
keyPath: args.keyPath,
port: cfg.port,
trafficOps: cfg.trafficOps,
useSSL: true,
version: ver
};
} else if (args.keyPath) {
throw new Error("must specify either both a key path and a cert path, or neither");
}
}
if (cfg.useSSL) {
if (!existsSync(cfg.certPath)) {
throw new Error(`no such certificate file: ${cfg.certPath}`);
}
if (!existsSync(cfg.keyPath)) {
throw new Error(`no such key file: ${cfg.keyPath}`);
}
}
return cfg;
}