Compare commits
No commits in common. "b164e1b403c49b14d05842c66094a2a99fa0a7d3" and "b72e918ff9699cc1dbac9eca1b4ad39e2733fbb6" have entirely different histories.
b164e1b403
...
b72e918ff9
tests/unit/management
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict'; // eslint-disable-line strict
|
||||||
|
|
||||||
|
const {
|
||||||
|
startWSManagementClient,
|
||||||
|
startPushConnectionHealthCheckServer,
|
||||||
|
} = require('../lib/management/push');
|
||||||
|
|
||||||
|
const logger = require('../lib/utilities/logger');
|
||||||
|
|
||||||
|
const {
|
||||||
|
PUSH_ENDPOINT: pushEndpoint,
|
||||||
|
INSTANCE_ID: instanceId,
|
||||||
|
MANAGEMENT_TOKEN: managementToken,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (!pushEndpoint) {
|
||||||
|
logger.error('missing push endpoint env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instanceId) {
|
||||||
|
logger.error('missing instance id env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!managementToken) {
|
||||||
|
logger.error('missing management token env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
startPushConnectionHealthCheckServer(err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('could not start healthcheck server', { error: err });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const url = `${pushEndpoint}/${instanceId}/ws?metrics=1`;
|
||||||
|
startWSManagementClient(url, managementToken, err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('connection failed, exiting', { error: err });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.info('no more connection, exiting');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict'; // eslint-disable-line strict
|
||||||
|
|
||||||
|
const {
|
||||||
|
startWSManagementClient,
|
||||||
|
startPushConnectionHealthCheckServer,
|
||||||
|
} = require('../lib/management/push');
|
||||||
|
|
||||||
|
const logger = require('../lib/utilities/logger');
|
||||||
|
|
||||||
|
const {
|
||||||
|
PUSH_ENDPOINT: pushEndpoint,
|
||||||
|
INSTANCE_ID: instanceId,
|
||||||
|
MANAGEMENT_TOKEN: managementToken,
|
||||||
|
} = process.env;
|
||||||
|
|
||||||
|
if (!pushEndpoint) {
|
||||||
|
logger.error('missing push endpoint env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instanceId) {
|
||||||
|
logger.error('missing instance id env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!managementToken) {
|
||||||
|
logger.error('missing management token env var');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
startPushConnectionHealthCheckServer(err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('could not start healthcheck server', { error: err });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const url = `${pushEndpoint}/${instanceId}/ws?proxy=1`;
|
||||||
|
startWSManagementClient(url, managementToken, err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('connection failed, exiting', { error: err });
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
logger.info('no more connection, exiting');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1526,6 +1526,25 @@ class Config extends EventEmitter {
|
||||||
this.outboundProxy.certs = certObj.certs;
|
this.outboundProxy.certs = certObj.certs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.managementAgent = {};
|
||||||
|
this.managementAgent.port = 8010;
|
||||||
|
this.managementAgent.host = 'localhost';
|
||||||
|
if (config.managementAgent !== undefined) {
|
||||||
|
if (config.managementAgent.port !== undefined) {
|
||||||
|
assert(Number.isInteger(config.managementAgent.port)
|
||||||
|
&& config.managementAgent.port > 0,
|
||||||
|
'bad config: managementAgent port must be a positive ' +
|
||||||
|
'integer');
|
||||||
|
this.managementAgent.port = config.managementAgent.port;
|
||||||
|
}
|
||||||
|
if (config.managementAgent.host !== undefined) {
|
||||||
|
assert.strictEqual(typeof config.managementAgent.host, 'string',
|
||||||
|
'bad config: management agent host must ' +
|
||||||
|
'be a string');
|
||||||
|
this.managementAgent.host = config.managementAgent.host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ephemeral token to protect the reporting endpoint:
|
// Ephemeral token to protect the reporting endpoint:
|
||||||
// try inherited from parent first, then hardcoded in conf file,
|
// try inherited from parent first, then hardcoded in conf file,
|
||||||
// then create a fresh one as last resort.
|
// then create a fresh one as last resort.
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const vaultclient = require('vaultclient');
|
||||||
const { auth } = require('arsenal');
|
const { auth } = require('arsenal');
|
||||||
|
|
||||||
const { config } = require('../Config');
|
const { config } = require('../Config');
|
||||||
|
@ -20,7 +21,6 @@ function getVaultClient(config) {
|
||||||
port,
|
port,
|
||||||
https: true,
|
https: true,
|
||||||
});
|
});
|
||||||
const vaultclient = require('vaultclient');
|
|
||||||
vaultClient = new vaultclient.Client(host, port, true, key, cert, ca);
|
vaultClient = new vaultclient.Client(host, port, true, key, cert, ca);
|
||||||
} else {
|
} else {
|
||||||
logger.info('vaultclient configuration', {
|
logger.info('vaultclient configuration', {
|
||||||
|
@ -28,7 +28,6 @@ function getVaultClient(config) {
|
||||||
port,
|
port,
|
||||||
https: false,
|
https: false,
|
||||||
});
|
});
|
||||||
const vaultclient = require('vaultclient');
|
|
||||||
vaultClient = new vaultclient.Client(host, port);
|
vaultClient = new vaultclient.Client(host, port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,6 +49,10 @@ function getMemBackend(config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.backends.auth) {
|
switch (config.backends.auth) {
|
||||||
|
case 'mem':
|
||||||
|
implName = 'vaultMem';
|
||||||
|
client = getMemBackend(config);
|
||||||
|
break;
|
||||||
case 'multiple':
|
case 'multiple':
|
||||||
implName = 'vaultChain';
|
implName = 'vaultChain';
|
||||||
client = new ChainBackend('s3', [
|
client = new ChainBackend('s3', [
|
||||||
|
@ -57,14 +60,9 @@ case 'multiple':
|
||||||
getVaultClient(config),
|
getVaultClient(config),
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
case 'vault':
|
default: // vault
|
||||||
implName = 'vault';
|
implName = 'vault';
|
||||||
client = getVaultClient(config);
|
client = getVaultClient(config);
|
||||||
break;
|
|
||||||
default: // mem
|
|
||||||
implName = 'vaultMem';
|
|
||||||
client = getMemBackend(config);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new Vault(client, implName);
|
module.exports = new Vault(client, implName);
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
/**
|
||||||
|
* Target service that should handle a message
|
||||||
|
* @readonly
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
const MessageType = {
|
||||||
|
/** Message that contains a configuration overlay */
|
||||||
|
CONFIG_OVERLAY_MESSAGE: 1,
|
||||||
|
/** Message that requests a metrics report */
|
||||||
|
METRICS_REQUEST_MESSAGE: 2,
|
||||||
|
/** Message that contains a metrics report */
|
||||||
|
METRICS_REPORT_MESSAGE: 3,
|
||||||
|
/** Close the virtual TCP socket associated to the channel */
|
||||||
|
CHANNEL_CLOSE_MESSAGE: 4,
|
||||||
|
/** Write data to the virtual TCP socket associated to the channel */
|
||||||
|
CHANNEL_PAYLOAD_MESSAGE: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target service that should handle a message
|
||||||
|
* @readonly
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
const TargetType = {
|
||||||
|
/** Let the dispatcher choose the most appropriate message */
|
||||||
|
TARGET_ANY: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerSize = 3;
|
||||||
|
|
||||||
|
class ChannelMessageV0 {
|
||||||
|
/**
|
||||||
|
* @param {Buffer} buffer Message bytes
|
||||||
|
*/
|
||||||
|
constructor(buffer) {
|
||||||
|
this.messageType = buffer.readUInt8(0);
|
||||||
|
this.channelNumber = buffer.readUInt8(1);
|
||||||
|
this.target = buffer.readUInt8(2);
|
||||||
|
this.payload = buffer.slice(headerSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {number} Message type
|
||||||
|
*/
|
||||||
|
getType() {
|
||||||
|
return this.messageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {number} Channel number if applicable
|
||||||
|
*/
|
||||||
|
getChannelNumber() {
|
||||||
|
return this.channelNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {number} Target service, or 0 to choose automatically
|
||||||
|
*/
|
||||||
|
getTarget() {
|
||||||
|
return this.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Buffer} Message payload if applicable
|
||||||
|
*/
|
||||||
|
getPayload() {
|
||||||
|
return this.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wire representation of a channel close message
|
||||||
|
*
|
||||||
|
* @param {number} channelId Channel number
|
||||||
|
*
|
||||||
|
* @returns {Buffer} wire representation
|
||||||
|
*/
|
||||||
|
static encodeChannelCloseMessage(channelId) {
|
||||||
|
const buf = Buffer.alloc(headerSize);
|
||||||
|
buf.writeUInt8(MessageType.CHANNEL_CLOSE_MESSAGE, 0);
|
||||||
|
buf.writeUInt8(channelId, 1);
|
||||||
|
buf.writeUInt8(TargetType.TARGET_ANY, 2);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wire representation of a channel data message
|
||||||
|
*
|
||||||
|
* @param {number} channelId Channel number
|
||||||
|
* @param {Buffer} data Payload
|
||||||
|
*
|
||||||
|
* @returns {Buffer} wire representation
|
||||||
|
*/
|
||||||
|
static encodeChannelDataMessage(channelId, data) {
|
||||||
|
const buf = Buffer.alloc(data.length + headerSize);
|
||||||
|
buf.writeUInt8(MessageType.CHANNEL_PAYLOAD_MESSAGE, 0);
|
||||||
|
buf.writeUInt8(channelId, 1);
|
||||||
|
buf.writeUInt8(TargetType.TARGET_ANY, 2);
|
||||||
|
data.copy(buf, headerSize);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a wire representation of a metrics message
|
||||||
|
*
|
||||||
|
* @param {object} body Metrics report
|
||||||
|
*
|
||||||
|
* @returns {Buffer} wire representation
|
||||||
|
*/
|
||||||
|
static encodeMetricsReportMessage(body) {
|
||||||
|
const report = JSON.stringify(body);
|
||||||
|
const buf = Buffer.alloc(report.length + headerSize);
|
||||||
|
buf.writeUInt8(MessageType.METRICS_REPORT_MESSAGE, 0);
|
||||||
|
buf.writeUInt8(0, 1);
|
||||||
|
buf.writeUInt8(TargetType.TARGET_ANY, 2);
|
||||||
|
buf.write(report, headerSize);
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol name used for subprotocol negociation
|
||||||
|
*/
|
||||||
|
static get protocolName() {
|
||||||
|
return 'zenko-secure-channel-v0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ChannelMessageV0,
|
||||||
|
MessageType,
|
||||||
|
TargetType,
|
||||||
|
};
|
|
@ -0,0 +1,94 @@
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
|
||||||
|
const logger = require('../utilities/logger');
|
||||||
|
const _config = require('../Config').config;
|
||||||
|
const { patchConfiguration } = require('./configuration');
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
|
||||||
|
|
||||||
|
const managementAgentMessageType = {
|
||||||
|
/** Message that contains the loaded overlay */
|
||||||
|
NEW_OVERLAY: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONNECTION_RETRY_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
|
||||||
|
function initManagementClient() {
|
||||||
|
const { host, port } = _config.managementAgent;
|
||||||
|
|
||||||
|
const ws = new WebSocket(`ws://${host}:${port}/watch`);
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
logger.info('connected with management agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', (code, reason) => {
|
||||||
|
logger.info('disconnected from management agent', { reason });
|
||||||
|
setTimeout(initManagementClient, CONNECTION_RETRY_TIMEOUT_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', error => {
|
||||||
|
logger.error('error on connection with management agent', { error });
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', data => {
|
||||||
|
const method = 'initManagementclient::onMessage';
|
||||||
|
const log = logger.newRequestLogger();
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
log.error('message without data', { method });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
msg = JSON.parse(data);
|
||||||
|
} catch (err) {
|
||||||
|
log.error('data is an invalid json', { method, err, data });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.payload === undefined) {
|
||||||
|
log.error('message without payload', { method });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof msg.messageType !== 'number') {
|
||||||
|
log.error('messageType is not an integer', {
|
||||||
|
type: typeof msg.messageType,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.messageType) {
|
||||||
|
case managementAgentMessageType.NEW_OVERLAY:
|
||||||
|
patchConfiguration(msg.payload, log, err => {
|
||||||
|
if (err) {
|
||||||
|
log.error('failed to patch overlay', {
|
||||||
|
error: reshapeExceptionError(err),
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
log.error('new overlay message with unmanaged message type', {
|
||||||
|
method,
|
||||||
|
type: msg.messageType,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isManagementAgentUsed() {
|
||||||
|
return process.env.MANAGEMENT_USE_AGENT === '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
managementAgentMessageType,
|
||||||
|
initManagementClient,
|
||||||
|
isManagementAgentUsed,
|
||||||
|
};
|
|
@ -0,0 +1,240 @@
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
|
||||||
|
const { buildAuthDataAccount } = require('../auth/in_memory/builder');
|
||||||
|
const _config = require('../Config').config;
|
||||||
|
const metadata = require('../metadata/wrapper');
|
||||||
|
|
||||||
|
const { getStoredCredentials } = require('./credentials');
|
||||||
|
|
||||||
|
const latestOverlayVersionKey = 'configuration/overlay-version';
|
||||||
|
const managementDatabaseName = 'PENSIEVE';
|
||||||
|
const replicatorEndpoint = 'zenko-cloudserver-replicator';
|
||||||
|
const { decryptSecret } = arsenal.pensieve.credentialUtils;
|
||||||
|
const { patchLocations } = arsenal.patches.locationConstraints;
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
const { replicationBackends } = require('arsenal').constants;
|
||||||
|
|
||||||
|
function overlayHasVersion(overlay) {
|
||||||
|
return overlay && overlay.version !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteOverlayIsNewer(cachedOverlay, remoteOverlay) {
|
||||||
|
return (overlayHasVersion(remoteOverlay) &&
|
||||||
|
(!overlayHasVersion(cachedOverlay) ||
|
||||||
|
remoteOverlay.version > cachedOverlay.version));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the live {Config} object with the new overlay configuration.
|
||||||
|
*
|
||||||
|
* No-op if this version was already applied to the live {Config}.
|
||||||
|
*
|
||||||
|
* @param {object} newConf Overlay configuration to apply
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger
|
||||||
|
* @param {function} cb Function to call with (error, newConf)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function patchConfiguration(newConf, log, cb) {
|
||||||
|
if (newConf.version === undefined) {
|
||||||
|
log.debug('no remote configuration created yet');
|
||||||
|
return process.nextTick(cb, null, newConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_config.overlayVersion !== undefined &&
|
||||||
|
newConf.version <= _config.overlayVersion) {
|
||||||
|
log.debug('configuration version already applied',
|
||||||
|
{ configurationVersion: newConf.version });
|
||||||
|
return process.nextTick(cb, null, newConf);
|
||||||
|
}
|
||||||
|
return getStoredCredentials(log, (err, creds) => {
|
||||||
|
if (err) {
|
||||||
|
return cb(err);
|
||||||
|
}
|
||||||
|
const accounts = [];
|
||||||
|
if (newConf.users) {
|
||||||
|
newConf.users.forEach(u => {
|
||||||
|
if (u.secretKey && u.secretKey.length > 0) {
|
||||||
|
const secretKey = decryptSecret(creds, u.secretKey);
|
||||||
|
// accountType will be service-replication or service-clueso
|
||||||
|
let serviceName;
|
||||||
|
if (u.accountType && u.accountType.startsWith('service-')) {
|
||||||
|
serviceName = u.accountType.split('-')[1];
|
||||||
|
}
|
||||||
|
const newAccount = buildAuthDataAccount(
|
||||||
|
u.accessKey, secretKey, u.canonicalId, serviceName,
|
||||||
|
u.userName);
|
||||||
|
accounts.push(newAccount.accounts[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const restEndpoints = Object.assign({}, _config.restEndpoints);
|
||||||
|
if (newConf.endpoints) {
|
||||||
|
newConf.endpoints.forEach(e => {
|
||||||
|
restEndpoints[e.hostname] = e.locationName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!restEndpoints[replicatorEndpoint]) {
|
||||||
|
restEndpoints[replicatorEndpoint] = 'us-east-1';
|
||||||
|
}
|
||||||
|
|
||||||
|
const locations = patchLocations(newConf.locations, creds, log);
|
||||||
|
if (Object.keys(locations).length !== 0) {
|
||||||
|
try {
|
||||||
|
_config.setLocationConstraints(locations);
|
||||||
|
} catch (error) {
|
||||||
|
const exceptionError = reshapeExceptionError(error);
|
||||||
|
log.error('could not apply configuration version location ' +
|
||||||
|
'constraints', { error: exceptionError,
|
||||||
|
method: 'getStoredCredentials' });
|
||||||
|
return cb(exceptionError);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const locationsWithReplicationBackend = Object.keys(locations)
|
||||||
|
// NOTE: In Orbit, we don't need to have Scality location in our
|
||||||
|
// replication endpoind config, since we do not replicate to
|
||||||
|
// any Scality Instance yet.
|
||||||
|
.filter(key => replicationBackends
|
||||||
|
[locations[key].type])
|
||||||
|
.reduce((obj, key) => {
|
||||||
|
/* eslint no-param-reassign:0 */
|
||||||
|
obj[key] = locations[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
_config.setReplicationEndpoints(
|
||||||
|
locationsWithReplicationBackend);
|
||||||
|
} catch (error) {
|
||||||
|
const exceptionError = reshapeExceptionError(error);
|
||||||
|
log.error('could not apply replication endpoints',
|
||||||
|
{ error: exceptionError, method: 'getStoredCredentials' });
|
||||||
|
return cb(exceptionError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.setAuthDataAccounts(accounts);
|
||||||
|
_config.setRestEndpoints(restEndpoints);
|
||||||
|
_config.setPublicInstanceId(newConf.instanceId);
|
||||||
|
|
||||||
|
if (newConf.browserAccess) {
|
||||||
|
if (Boolean(_config.browserAccessEnabled) !==
|
||||||
|
Boolean(newConf.browserAccess.enabled)) {
|
||||||
|
_config.browserAccessEnabled =
|
||||||
|
Boolean(newConf.browserAccess.enabled);
|
||||||
|
_config.emit('browser-access-enabled-change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_config.overlayVersion = newConf.version;
|
||||||
|
|
||||||
|
log.info('applied configuration version',
|
||||||
|
{ configurationVersion: _config.overlayVersion });
|
||||||
|
|
||||||
|
return cb(null, newConf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes configuration version to the management database
|
||||||
|
*
|
||||||
|
* @param {object} cachedOverlay Latest stored configuration version
|
||||||
|
* for freshness comparison purposes
|
||||||
|
* @param {object} remoteOverlay New configuration version
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger
|
||||||
|
* @param {function} cb Function to call with (error, remoteOverlay)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function saveConfigurationVersion(cachedOverlay, remoteOverlay, log, cb) {
|
||||||
|
if (remoteOverlayIsNewer(cachedOverlay, remoteOverlay)) {
|
||||||
|
const objName = `configuration/overlay/${remoteOverlay.version}`;
|
||||||
|
metadata.putObjectMD(managementDatabaseName, objName, remoteOverlay,
|
||||||
|
{}, log, error => {
|
||||||
|
if (error) {
|
||||||
|
const exceptionError = reshapeExceptionError(error);
|
||||||
|
log.error('could not save configuration',
|
||||||
|
{ error: exceptionError,
|
||||||
|
method: 'saveConfigurationVersion',
|
||||||
|
configurationVersion: remoteOverlay.version });
|
||||||
|
cb(exceptionError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadata.putObjectMD(managementDatabaseName,
|
||||||
|
latestOverlayVersionKey, remoteOverlay.version, {}, log,
|
||||||
|
error => {
|
||||||
|
if (error) {
|
||||||
|
log.error('could not save configuration version', {
|
||||||
|
configurationVersion: remoteOverlay.version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cb(error, remoteOverlay);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.debug('no remote configuration to cache yet');
|
||||||
|
process.nextTick(cb, null, remoteOverlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the latest cached configuration overlay from the management
|
||||||
|
* database, without contacting the Orbit API.
|
||||||
|
*
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger
|
||||||
|
* @param {function} callback Function called with (error, cachedOverlay)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function loadCachedOverlay(log, callback) {
|
||||||
|
return metadata.getObjectMD(managementDatabaseName,
|
||||||
|
latestOverlayVersionKey, {}, log, (err, version) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.is.NoSuchKey) {
|
||||||
|
return process.nextTick(callback, null, {});
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
return metadata.getObjectMD(managementDatabaseName,
|
||||||
|
`configuration/overlay/${version}`, {}, log, (err, conf) => {
|
||||||
|
if (err) {
|
||||||
|
if (err.is.NoSuchKey) {
|
||||||
|
return process.nextTick(callback, null, {});
|
||||||
|
}
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
return callback(null, conf);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAndSaveOverlay(overlay, log) {
|
||||||
|
patchConfiguration(overlay, log, err => {
|
||||||
|
if (err) {
|
||||||
|
log.error('could not apply pushed overlay', {
|
||||||
|
error: reshapeExceptionError(err),
|
||||||
|
method: 'applyAndSaveOverlay',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saveConfigurationVersion(null, overlay, log, err => {
|
||||||
|
if (err) {
|
||||||
|
log.error('could not cache overlay version', {
|
||||||
|
error: reshapeExceptionError(err),
|
||||||
|
method: 'applyAndSaveOverlay',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.info('overlay push processed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
loadCachedOverlay,
|
||||||
|
managementDatabaseName,
|
||||||
|
patchConfiguration,
|
||||||
|
saveConfigurationVersion,
|
||||||
|
remoteOverlayIsNewer,
|
||||||
|
applyAndSaveOverlay,
|
||||||
|
};
|
|
@ -0,0 +1,145 @@
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
const forge = require('node-forge');
|
||||||
|
const request = require('../utilities/request');
|
||||||
|
|
||||||
|
const metadata = require('../metadata/wrapper');
|
||||||
|
|
||||||
|
const managementDatabaseName = 'PENSIEVE';
|
||||||
|
const tokenConfigurationKey = 'auth/zenko/remote-management-token';
|
||||||
|
const tokenRotationDelay = 3600 * 24 * 7 * 1000; // 7 days
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves Orbit API token from the management database.
|
||||||
|
*
|
||||||
|
* The token is used to authenticate stat posting and
|
||||||
|
*
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger to be able to trace
|
||||||
|
* initialization process
|
||||||
|
* @param {function} callback Function called with (error, result)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function getStoredCredentials(log, callback) {
|
||||||
|
metadata.getObjectMD(managementDatabaseName, tokenConfigurationKey, {},
|
||||||
|
log, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function issueCredentials(managementEndpoint, instanceId, log, callback) {
|
||||||
|
log.info('registering with API to get token');
|
||||||
|
|
||||||
|
const keyPair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
|
||||||
|
const privateKey = forge.pki.privateKeyToPem(keyPair.privateKey);
|
||||||
|
const publicKey = forge.pki.publicKeyToPem(keyPair.publicKey);
|
||||||
|
|
||||||
|
const postData = {
|
||||||
|
publicKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(`${managementEndpoint}/${instanceId}/register`,
|
||||||
|
{ body: postData, json: true }, (error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 201) {
|
||||||
|
log.error('could not register instance', {
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
});
|
||||||
|
return callback(arsenal.errors.InternalError);
|
||||||
|
}
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
body.privateKey = privateKey;
|
||||||
|
/* eslint-enable no-param-reassign */
|
||||||
|
return callback(null, body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmInstanceCredentials(
|
||||||
|
managementEndpoint, instanceId, creds, log, callback) {
|
||||||
|
const postData = {
|
||||||
|
serial: creds.serial || 0,
|
||||||
|
publicKey: creds.publicKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
headers: {
|
||||||
|
'x-instance-authentication-token': creds.token,
|
||||||
|
},
|
||||||
|
body: postData,
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(`${managementEndpoint}/${instanceId}/confirm`,
|
||||||
|
opts, (error, response) => {
|
||||||
|
if (error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return callback(null, instanceId, creds.token);
|
||||||
|
}
|
||||||
|
return callback(arsenal.errors.InternalError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes credentials and PKI in the management database.
|
||||||
|
*
|
||||||
|
* In case the management database is new and empty, the instance
|
||||||
|
* is registered as new against the Orbit API with newly-generated
|
||||||
|
* RSA key pair.
|
||||||
|
*
|
||||||
|
* @param {string} managementEndpoint API endpoint
|
||||||
|
* @param {string} instanceId UUID of this deployment
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger to be able to trace
|
||||||
|
* initialization process
|
||||||
|
* @param {function} callback Function called with (error, result)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function initManagementCredentials(
|
||||||
|
managementEndpoint, instanceId, log, callback) {
|
||||||
|
getStoredCredentials(log, (error, value) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.is.NoSuchKey) {
|
||||||
|
return issueCredentials(managementEndpoint, instanceId, log,
|
||||||
|
(error, value) => {
|
||||||
|
if (error) {
|
||||||
|
log.error('could not issue token',
|
||||||
|
{ error: reshapeExceptionError(error),
|
||||||
|
method: 'initManagementCredentials' });
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
log.debug('saving token');
|
||||||
|
return metadata.putObjectMD(managementDatabaseName,
|
||||||
|
tokenConfigurationKey, value, {}, log, error => {
|
||||||
|
if (error) {
|
||||||
|
log.error('could not save token',
|
||||||
|
{ error: reshapeExceptionError(error),
|
||||||
|
method: 'initManagementCredentials',
|
||||||
|
});
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
log.info('saved token locally, ' +
|
||||||
|
'confirming instance');
|
||||||
|
return confirmInstanceCredentials(
|
||||||
|
managementEndpoint, instanceId, value, log,
|
||||||
|
callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
log.debug('could not get token', { error });
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('returning existing token');
|
||||||
|
if (Date.now() - value.issueDate > tokenRotationDelay) {
|
||||||
|
log.warn('management API token is too old, should re-issue');
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, instanceId, value.token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getStoredCredentials,
|
||||||
|
initManagementCredentials,
|
||||||
|
};
|
|
@ -0,0 +1,138 @@
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
const async = require('async');
|
||||||
|
|
||||||
|
const metadata = require('../metadata/wrapper');
|
||||||
|
const logger = require('../utilities/logger');
|
||||||
|
|
||||||
|
const {
|
||||||
|
loadCachedOverlay,
|
||||||
|
managementDatabaseName,
|
||||||
|
patchConfiguration,
|
||||||
|
} = require('./configuration');
|
||||||
|
const { initManagementCredentials } = require('./credentials');
|
||||||
|
const { startWSManagementClient } = require('./push');
|
||||||
|
const { startPollingManagementClient } = require('./poll');
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
const { isManagementAgentUsed } = require('./agentClient');
|
||||||
|
|
||||||
|
const initRemoteManagementRetryDelay = 10000;
|
||||||
|
|
||||||
|
const managementEndpointRoot =
|
||||||
|
process.env.MANAGEMENT_ENDPOINT ||
|
||||||
|
'https://api.zenko.io';
|
||||||
|
const managementEndpoint = `${managementEndpointRoot}/api/v1/instance`;
|
||||||
|
|
||||||
|
const pushEndpointRoot =
|
||||||
|
process.env.PUSH_ENDPOINT ||
|
||||||
|
'https://push.api.zenko.io';
|
||||||
|
const pushEndpoint = `${pushEndpointRoot}/api/v1/instance`;
|
||||||
|
|
||||||
|
function initManagementDatabase(log, callback) {
|
||||||
|
// XXX choose proper owner names
|
||||||
|
const md = new arsenal.models.BucketInfo(managementDatabaseName, 'owner',
|
||||||
|
'owner display name', new Date().toJSON());
|
||||||
|
|
||||||
|
metadata.createBucket(managementDatabaseName, md, log, error => {
|
||||||
|
if (error) {
|
||||||
|
if (error.is.BucketAlreadyExists) {
|
||||||
|
log.info('created management database');
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
log.error('could not initialize management database',
|
||||||
|
{ error: reshapeExceptionError(error),
|
||||||
|
method: 'initManagementDatabase' });
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
log.info('initialized management database');
|
||||||
|
return callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startManagementListeners(instanceId, token) {
|
||||||
|
const mode = process.env.MANAGEMENT_MODE || 'push';
|
||||||
|
if (mode === 'push') {
|
||||||
|
const url = `${pushEndpoint}/${instanceId}/ws`;
|
||||||
|
startWSManagementClient(url, token);
|
||||||
|
} else {
|
||||||
|
startPollingManagementClient(managementEndpoint, instanceId, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes Orbit-based management by:
|
||||||
|
* - creating the management database in metadata
|
||||||
|
* - generating a key pair for credentials encryption
|
||||||
|
* - generating an instance-unique ID
|
||||||
|
* - getting an authentication token for the API
|
||||||
|
* - loading and applying the latest cached overlay configuration
|
||||||
|
* - starting a configuration update and metrics push background task
|
||||||
|
*
|
||||||
|
* @param {werelogs~Logger} log Request-scoped logger to be able to trace
|
||||||
|
* initialization process
|
||||||
|
* @param {function} callback Function to call once the overlay is loaded
|
||||||
|
* (overlay)
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function initManagement(log, callback) {
|
||||||
|
if ((process.env.REMOTE_MANAGEMENT_DISABLE &&
|
||||||
|
process.env.REMOTE_MANAGEMENT_DISABLE !== '0')
|
||||||
|
|| process.env.S3BACKEND === 'mem') {
|
||||||
|
log.info('remote management disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Temporary check before to fully move to the process management agent. */
|
||||||
|
if (isManagementAgentUsed() ^ typeof callback === 'function') {
|
||||||
|
let msg = 'misuse of initManagement function: ';
|
||||||
|
msg += `MANAGEMENT_USE_AGENT: ${process.env.MANAGEMENT_USE_AGENT}`;
|
||||||
|
msg += `, callback type: ${typeof callback}`;
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
// eslint-disable-next-line arrow-body-style
|
||||||
|
cb => { return isManagementAgentUsed() ? metadata.setup(cb) : cb(); },
|
||||||
|
cb => initManagementDatabase(log, cb),
|
||||||
|
cb => metadata.getUUID(log, cb),
|
||||||
|
(instanceId, cb) => initManagementCredentials(
|
||||||
|
managementEndpoint, instanceId, log, cb),
|
||||||
|
(instanceId, token, cb) => {
|
||||||
|
if (!isManagementAgentUsed()) {
|
||||||
|
cb(null, instanceId, token, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadCachedOverlay(log, (err, overlay) => cb(err, instanceId,
|
||||||
|
token, overlay));
|
||||||
|
},
|
||||||
|
(instanceId, token, overlay, cb) => {
|
||||||
|
if (!isManagementAgentUsed()) {
|
||||||
|
cb(null, instanceId, token, overlay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchConfiguration(overlay, log,
|
||||||
|
err => cb(err, instanceId, token, overlay));
|
||||||
|
},
|
||||||
|
], (error, instanceId, token, overlay) => {
|
||||||
|
if (error) {
|
||||||
|
log.error('could not initialize remote management, retrying later',
|
||||||
|
{ error: reshapeExceptionError(error),
|
||||||
|
method: 'initManagement' });
|
||||||
|
setTimeout(initManagement,
|
||||||
|
initRemoteManagementRetryDelay,
|
||||||
|
logger.newRequestLogger());
|
||||||
|
} else {
|
||||||
|
log.info(`this deployment's Instance ID is ${instanceId}`);
|
||||||
|
log.end('management init done');
|
||||||
|
startManagementListeners(instanceId, token);
|
||||||
|
if (callback) {
|
||||||
|
callback(overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initManagement,
|
||||||
|
initManagementDatabase,
|
||||||
|
};
|
|
@ -0,0 +1,157 @@
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
const async = require('async');
|
||||||
|
const request = require('../utilities/request');
|
||||||
|
|
||||||
|
const _config = require('../Config').config;
|
||||||
|
const logger = require('../utilities/logger');
|
||||||
|
const metadata = require('../metadata/wrapper');
|
||||||
|
const {
|
||||||
|
loadCachedOverlay,
|
||||||
|
patchConfiguration,
|
||||||
|
saveConfigurationVersion,
|
||||||
|
} = require('./configuration');
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
|
||||||
|
const pushReportDelay = 30000;
|
||||||
|
const pullConfigurationOverlayDelay = 60000;
|
||||||
|
|
||||||
|
function loadRemoteOverlay(
|
||||||
|
managementEndpoint, instanceId, remoteToken, cachedOverlay, log, cb) {
|
||||||
|
log.debug('loading remote overlay');
|
||||||
|
const opts = {
|
||||||
|
headers: {
|
||||||
|
'x-instance-authentication-token': remoteToken,
|
||||||
|
'x-scal-request-id': log.getSerializedUids(),
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
request.get(`${managementEndpoint}/${instanceId}/config/overlay`, opts,
|
||||||
|
(error, response, body) => {
|
||||||
|
if (error) {
|
||||||
|
return cb(error);
|
||||||
|
}
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
return cb(null, cachedOverlay, body);
|
||||||
|
}
|
||||||
|
if (response.statusCode === 404) {
|
||||||
|
return cb(null, cachedOverlay, {});
|
||||||
|
}
|
||||||
|
return cb(arsenal.errors.AccessForbidden, cachedOverlay, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO save only after successful patch
|
||||||
|
function applyConfigurationOverlay(
|
||||||
|
managementEndpoint, instanceId, remoteToken, log) {
|
||||||
|
async.waterfall([
|
||||||
|
wcb => loadCachedOverlay(log, wcb),
|
||||||
|
(cachedOverlay, wcb) => patchConfiguration(cachedOverlay,
|
||||||
|
log, wcb),
|
||||||
|
(cachedOverlay, wcb) =>
|
||||||
|
loadRemoteOverlay(managementEndpoint, instanceId, remoteToken,
|
||||||
|
cachedOverlay, log, wcb),
|
||||||
|
(cachedOverlay, remoteOverlay, wcb) =>
|
||||||
|
saveConfigurationVersion(cachedOverlay, remoteOverlay, log, wcb),
|
||||||
|
(remoteOverlay, wcb) => patchConfiguration(remoteOverlay,
|
||||||
|
log, wcb),
|
||||||
|
], error => {
|
||||||
|
if (error) {
|
||||||
|
log.error('could not apply managed configuration',
|
||||||
|
{ error: reshapeExceptionError(error),
|
||||||
|
method: 'applyConfigurationOverlay' });
|
||||||
|
}
|
||||||
|
setTimeout(applyConfigurationOverlay, pullConfigurationOverlayDelay,
|
||||||
|
managementEndpoint, instanceId, remoteToken,
|
||||||
|
logger.newRequestLogger());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function postStats(managementEndpoint, instanceId, remoteToken, report, next) {
|
||||||
|
const toURL = `${managementEndpoint}/${instanceId}/stats`;
|
||||||
|
const toOptions = {
|
||||||
|
json: true,
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-instance-authentication-token': remoteToken,
|
||||||
|
},
|
||||||
|
body: report,
|
||||||
|
};
|
||||||
|
const toCallback = (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
logger.info('could not post stats', { error: err });
|
||||||
|
}
|
||||||
|
if (response && response.statusCode !== 201) {
|
||||||
|
logger.info('could not post stats', {
|
||||||
|
body,
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
next(null, instanceId, remoteToken);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return request.post(toURL, toOptions, toCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStats(next) {
|
||||||
|
const fromURL = `http://localhost:${_config.port}/_/report`;
|
||||||
|
const fromOptions = {
|
||||||
|
headers: {
|
||||||
|
'x-scal-report-token': process.env.REPORT_TOKEN,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return request.get(fromURL, fromOptions, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushStats(managementEndpoint, instanceId, remoteToken, next) {
|
||||||
|
if (process.env.PUSH_STATS === 'false') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStats((err, res, report) => {
|
||||||
|
if (err) {
|
||||||
|
logger.info('could not retrieve stats', { error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('report', { report });
|
||||||
|
postStats(
|
||||||
|
managementEndpoint,
|
||||||
|
instanceId,
|
||||||
|
remoteToken,
|
||||||
|
report,
|
||||||
|
next
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(pushStats, pushReportDelay,
|
||||||
|
managementEndpoint, instanceId, remoteToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts background task that updates configuration and pushes stats.
|
||||||
|
*
|
||||||
|
* Periodically polls for configuration updates, and pushes stats at
|
||||||
|
* a fixed interval.
|
||||||
|
*
|
||||||
|
* @param {string} managementEndpoint API endpoint
|
||||||
|
* @param {string} instanceId UUID of this deployment
|
||||||
|
* @param {string} remoteToken API authentication token
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function startPollingManagementClient(
|
||||||
|
managementEndpoint, instanceId, remoteToken) {
|
||||||
|
metadata.notifyBucketChange(() => {
|
||||||
|
pushStats(managementEndpoint, instanceId, remoteToken);
|
||||||
|
});
|
||||||
|
|
||||||
|
pushStats(managementEndpoint, instanceId, remoteToken);
|
||||||
|
applyConfigurationOverlay(managementEndpoint, instanceId, remoteToken,
|
||||||
|
logger.newRequestLogger());
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
startPollingManagementClient,
|
||||||
|
};
|
|
@ -0,0 +1,301 @@
|
||||||
|
const arsenal = require('arsenal');
|
||||||
|
const HttpsProxyAgent = require('https-proxy-agent');
|
||||||
|
const net = require('net');
|
||||||
|
const request = require('../utilities/request');
|
||||||
|
const { URL } = require('url');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
const assert = require('assert');
|
||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const _config = require('../Config').config;
|
||||||
|
const logger = require('../utilities/logger');
|
||||||
|
const metadata = require('../metadata/wrapper');
|
||||||
|
|
||||||
|
const { reshapeExceptionError } = arsenal.errorUtils;
|
||||||
|
const { isManagementAgentUsed } = require('./agentClient');
|
||||||
|
const { applyAndSaveOverlay } = require('./configuration');
|
||||||
|
const {
|
||||||
|
ChannelMessageV0,
|
||||||
|
MessageType,
|
||||||
|
} = require('./ChannelMessageV0');
|
||||||
|
|
||||||
|
const {
|
||||||
|
CONFIG_OVERLAY_MESSAGE,
|
||||||
|
METRICS_REQUEST_MESSAGE,
|
||||||
|
CHANNEL_CLOSE_MESSAGE,
|
||||||
|
CHANNEL_PAYLOAD_MESSAGE,
|
||||||
|
} = MessageType;
|
||||||
|
|
||||||
|
const PING_INTERVAL_MS = 10000;
|
||||||
|
const subprotocols = [ChannelMessageV0.protocolName];
|
||||||
|
|
||||||
|
const cloudServerHost = process.env.SECURE_CHANNEL_DEFAULT_FORWARD_TO_HOST
|
||||||
|
|| 'localhost';
|
||||||
|
const cloudServerPort = process.env.SECURE_CHANNEL_DEFAULT_FORWARD_TO_PORT
|
||||||
|
|| _config.port;
|
||||||
|
|
||||||
|
let overlayMessageListener = null;
|
||||||
|
let connected = false;
|
||||||
|
|
||||||
|
// No wildcard nor cidr/mask match for now
|
||||||
|
function createWSAgent(pushEndpoint, env, log) {
|
||||||
|
const url = new URL(pushEndpoint);
|
||||||
|
const noProxy = (env.NO_PROXY || env.no_proxy
|
||||||
|
|| '').split(',');
|
||||||
|
|
||||||
|
if (noProxy.includes(url.hostname)) {
|
||||||
|
log.info('push server ws has proxy exclusion', { noProxy });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.protocol === 'https:' || url.protocol === 'wss:') {
|
||||||
|
const httpsProxy = (env.HTTPS_PROXY || env.https_proxy);
|
||||||
|
if (httpsProxy) {
|
||||||
|
log.info('push server ws using https proxy', { httpsProxy });
|
||||||
|
return new HttpsProxyAgent(httpsProxy);
|
||||||
|
}
|
||||||
|
} else if (url.protocol === 'http:' || url.protocol === 'ws:') {
|
||||||
|
const httpProxy = (env.HTTP_PROXY || env.http_proxy);
|
||||||
|
if (httpProxy) {
|
||||||
|
log.info('push server ws using http proxy', { httpProxy });
|
||||||
|
return new HttpsProxyAgent(httpProxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allProxy = (env.ALL_PROXY || env.all_proxy);
|
||||||
|
if (allProxy) {
|
||||||
|
log.info('push server ws using wildcard proxy', { allProxy });
|
||||||
|
return new HttpsProxyAgent(allProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('push server ws not using proxy');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts background task that updates configuration and pushes stats.
|
||||||
|
*
|
||||||
|
* Receives pushed Websocket messages on configuration updates, and
|
||||||
|
* sends stat messages in response to API sollicitations.
|
||||||
|
*
|
||||||
|
* @param {string} url API endpoint
|
||||||
|
* @param {string} token API authentication token
|
||||||
|
* @param {function} cb end-of-connection callback
|
||||||
|
*
|
||||||
|
* @returns {undefined}
|
||||||
|
*/
|
||||||
|
function startWSManagementClient(url, token, cb) {
|
||||||
|
logger.info('connecting to push server', { url });
|
||||||
|
function _logError(error, errorMessage, method) {
|
||||||
|
if (error) {
|
||||||
|
logger.error(`management client error: ${errorMessage}`,
|
||||||
|
{ error: reshapeExceptionError(error), method });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketsByChannelId = [];
|
||||||
|
const headers = {
|
||||||
|
'x-instance-authentication-token': token,
|
||||||
|
};
|
||||||
|
const agent = createWSAgent(url, process.env, logger);
|
||||||
|
|
||||||
|
const ws = new WebSocket(url, subprotocols, { headers, agent });
|
||||||
|
let pingTimeout = null;
|
||||||
|
|
||||||
|
function sendPing() {
|
||||||
|
if (ws.readyState === ws.OPEN) {
|
||||||
|
ws.ping(err => _logError(err, 'failed to send a ping', 'sendPing'));
|
||||||
|
}
|
||||||
|
pingTimeout = setTimeout(() => ws.terminate(), PING_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initiatePing() {
|
||||||
|
clearTimeout(pingTimeout);
|
||||||
|
setTimeout(sendPing, PING_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushStats(options) {
|
||||||
|
if (process.env.PUSH_STATS === 'false') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fromURL = `http://${cloudServerHost}:${cloudServerPort}/_/report`;
|
||||||
|
const fromOptions = {
|
||||||
|
json: true,
|
||||||
|
headers: {
|
||||||
|
'x-scal-report-token': process.env.REPORT_TOKEN,
|
||||||
|
'x-scal-report-skip-cache': Boolean(options && options.noCache),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
request.get(fromURL, fromOptions, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
_logError(err, 'failed to get metrics report', 'pushStats');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ws.send(ChannelMessageV0.encodeMetricsReportMessage(body),
|
||||||
|
err => _logError(err, 'failed to send metrics report message',
|
||||||
|
'pushStats'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeChannel(channelId) {
|
||||||
|
const socket = socketsByChannelId[channelId];
|
||||||
|
if (socket) {
|
||||||
|
socket.destroy();
|
||||||
|
delete socketsByChannelId[channelId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function receiveChannelData(channelId, payload) {
|
||||||
|
let socket = socketsByChannelId[channelId];
|
||||||
|
if (!socket) {
|
||||||
|
socket = net.createConnection(cloudServerPort, cloudServerHost);
|
||||||
|
|
||||||
|
socket.on('data', data => {
|
||||||
|
ws.send(ChannelMessageV0.
|
||||||
|
encodeChannelDataMessage(channelId, data), err =>
|
||||||
|
_logError(err, 'failed to send channel data message',
|
||||||
|
'receiveChannelData'));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('drain', () => {
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('error', error => {
|
||||||
|
logger.error('failed to connect to S3', {
|
||||||
|
code: error.code,
|
||||||
|
host: error.address,
|
||||||
|
port: error.port,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('end', () => {
|
||||||
|
socket.destroy();
|
||||||
|
socketsByChannelId[channelId] = null;
|
||||||
|
ws.send(ChannelMessageV0.encodeChannelCloseMessage(channelId),
|
||||||
|
err => _logError(err,
|
||||||
|
'failed to send channel close message',
|
||||||
|
'receiveChannelData'));
|
||||||
|
});
|
||||||
|
|
||||||
|
socketsByChannelId[channelId] = socket;
|
||||||
|
}
|
||||||
|
socket.write(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserAccessChangeHandler() {
|
||||||
|
if (!_config.browserAccessEnabled) {
|
||||||
|
socketsByChannelId.forEach(s => s.close());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
connected = true;
|
||||||
|
logger.info('connected to push server');
|
||||||
|
|
||||||
|
metadata.notifyBucketChange(() => {
|
||||||
|
pushStats({ noCache: true });
|
||||||
|
});
|
||||||
|
_config.on('browser-access-enabled-change', browserAccessChangeHandler);
|
||||||
|
|
||||||
|
initiatePing();
|
||||||
|
});
|
||||||
|
|
||||||
|
const cbOnce = cb ? arsenal.jsutil.once(cb) : null;
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
logger.info('disconnected from push server, reconnecting in 10s');
|
||||||
|
metadata.notifyBucketChange(null);
|
||||||
|
_config.removeListener('browser-access-enabled-change',
|
||||||
|
browserAccessChangeHandler);
|
||||||
|
setTimeout(startWSManagementClient, 10000, url, token);
|
||||||
|
connected = false;
|
||||||
|
|
||||||
|
if (cbOnce) {
|
||||||
|
process.nextTick(cbOnce);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', err => {
|
||||||
|
connected = false;
|
||||||
|
logger.error('error from push server connection', {
|
||||||
|
error: err,
|
||||||
|
errorMessage: err.message,
|
||||||
|
});
|
||||||
|
if (cbOnce) {
|
||||||
|
process.nextTick(cbOnce, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('ping', () => {
|
||||||
|
ws.pong(err => _logError(err, 'failed to send a pong'));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('pong', () => {
|
||||||
|
initiatePing();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', data => {
|
||||||
|
const log = logger.newRequestLogger();
|
||||||
|
const message = new ChannelMessageV0(data);
|
||||||
|
switch (message.getType()) {
|
||||||
|
case CONFIG_OVERLAY_MESSAGE:
|
||||||
|
if (!isManagementAgentUsed()) {
|
||||||
|
applyAndSaveOverlay(JSON.parse(message.getPayload()), log);
|
||||||
|
} else {
|
||||||
|
if (overlayMessageListener) {
|
||||||
|
overlayMessageListener(message.getPayload().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case METRICS_REQUEST_MESSAGE:
|
||||||
|
pushStats();
|
||||||
|
break;
|
||||||
|
case CHANNEL_CLOSE_MESSAGE:
|
||||||
|
closeChannel(message.getChannelNumber());
|
||||||
|
break;
|
||||||
|
case CHANNEL_PAYLOAD_MESSAGE:
|
||||||
|
// browserAccessEnabled defaults to true unless explicitly false
|
||||||
|
if (_config.browserAccessEnabled !== false) {
|
||||||
|
receiveChannelData(
|
||||||
|
message.getChannelNumber(), message.getPayload());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.error('unknown message type from push server',
|
||||||
|
{ messageType: message.getType() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addOverlayMessageListener(callback) {
|
||||||
|
assert(typeof callback === 'function');
|
||||||
|
overlayMessageListener = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPushConnectionHealthCheckServer(cb) {
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url !== '/_/healthcheck') {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.write('Not Found');
|
||||||
|
} else if (connected) {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.write('Connected');
|
||||||
|
} else {
|
||||||
|
res.writeHead(503);
|
||||||
|
res.write('Not Connected');
|
||||||
|
}
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(_config.port, cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createWSAgent,
|
||||||
|
startWSManagementClient,
|
||||||
|
startPushConnectionHealthCheckServer,
|
||||||
|
addOverlayMessageListener,
|
||||||
|
};
|
|
@ -2,9 +2,9 @@ const MetadataWrapper = require('arsenal').storage.metadata.MetadataWrapper;
|
||||||
const { config } = require('../Config');
|
const { config } = require('../Config');
|
||||||
const logger = require('../utilities/logger');
|
const logger = require('../utilities/logger');
|
||||||
const constants = require('../../constants');
|
const constants = require('../../constants');
|
||||||
|
const bucketclient = require('bucketclient');
|
||||||
|
|
||||||
const clientName = config.backends.metadata;
|
const clientName = config.backends.metadata;
|
||||||
let bucketclient;
|
|
||||||
let params;
|
let params;
|
||||||
if (clientName === 'mem') {
|
if (clientName === 'mem') {
|
||||||
params = {};
|
params = {};
|
||||||
|
@ -21,7 +21,6 @@ if (clientName === 'mem') {
|
||||||
noDbOpen: null,
|
noDbOpen: null,
|
||||||
};
|
};
|
||||||
} else if (clientName === 'scality') {
|
} else if (clientName === 'scality') {
|
||||||
bucketclient = require('bucketclient');
|
|
||||||
params = {
|
params = {
|
||||||
bucketdBootstrap: config.bucketd.bootstrap,
|
bucketdBootstrap: config.bucketd.bootstrap,
|
||||||
bucketdLog: config.bucketd.log,
|
bucketdLog: config.bucketd.log,
|
||||||
|
|
|
@ -18,6 +18,11 @@ const locationStorageCheck =
|
||||||
require('./api/apiUtils/object/locationStorageCheck');
|
require('./api/apiUtils/object/locationStorageCheck');
|
||||||
const vault = require('./auth/vault');
|
const vault = require('./auth/vault');
|
||||||
const metadata = require('./metadata/wrapper');
|
const metadata = require('./metadata/wrapper');
|
||||||
|
const { initManagement } = require('./management');
|
||||||
|
const {
|
||||||
|
initManagementClient,
|
||||||
|
isManagementAgentUsed,
|
||||||
|
} = require('./management/agentClient');
|
||||||
|
|
||||||
const HttpAgent = require('agentkeepalive');
|
const HttpAgent = require('agentkeepalive');
|
||||||
const QuotaService = require('./quotas/quotas');
|
const QuotaService = require('./quotas/quotas');
|
||||||
|
@ -51,6 +56,7 @@ const STATS_INTERVAL = 5; // 5 seconds
|
||||||
const STATS_EXPIRY = 30; // 30 seconds
|
const STATS_EXPIRY = 30; // 30 seconds
|
||||||
const statsClient = new StatsClient(localCacheClient, STATS_INTERVAL,
|
const statsClient = new StatsClient(localCacheClient, STATS_INTERVAL,
|
||||||
STATS_EXPIRY);
|
STATS_EXPIRY);
|
||||||
|
const enableRemoteManagement = true;
|
||||||
|
|
||||||
class S3Server {
|
class S3Server {
|
||||||
/**
|
/**
|
||||||
|
@ -321,6 +327,18 @@ class S3Server {
|
||||||
QuotaService?.setup(log);
|
QuotaService?.setup(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO this should wait for metadata healthcheck to be ok
|
||||||
|
// TODO only do this in cluster master
|
||||||
|
if (enableRemoteManagement) {
|
||||||
|
if (!isManagementAgentUsed()) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initManagement(logger.newRequestLogger());
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
initManagementClient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.started = true;
|
this.started = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
const Uuid = require('uuid');
|
||||||
|
const WebSocket = require('ws');
|
||||||
|
|
||||||
|
const logger = require('./lib/utilities/logger');
|
||||||
|
const { initManagement } = require('./lib/management');
|
||||||
|
const _config = require('./lib/Config').config;
|
||||||
|
const { managementAgentMessageType } = require('./lib/management/agentClient');
|
||||||
|
const { addOverlayMessageListener } = require('./lib/management/push');
|
||||||
|
const { saveConfigurationVersion } = require('./lib/management/configuration');
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: auth?
|
||||||
|
// TODO: werelogs with a specific name.
|
||||||
|
|
||||||
|
const CHECK_BROKEN_CONNECTIONS_FREQUENCY_MS = 15000;
|
||||||
|
|
||||||
|
|
||||||
|
class ManagementAgentServer {
|
||||||
|
constructor() {
|
||||||
|
this.port = _config.managementAgent.port || 8010;
|
||||||
|
this.wss = null;
|
||||||
|
this.loadedOverlay = null;
|
||||||
|
|
||||||
|
this.stop = this.stop.bind(this);
|
||||||
|
process.on('SIGINT', this.stop);
|
||||||
|
process.on('SIGHUP', this.stop);
|
||||||
|
process.on('SIGQUIT', this.stop);
|
||||||
|
process.on('SIGTERM', this.stop);
|
||||||
|
process.on('SIGPIPE', () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
start(_cb) {
|
||||||
|
const cb = _cb || function noop() {};
|
||||||
|
|
||||||
|
/* Define REPORT_TOKEN env variable needed by the management
|
||||||
|
* module. */
|
||||||
|
process.env.REPORT_TOKEN = process.env.REPORT_TOKEN
|
||||||
|
|| _config.reportToken
|
||||||
|
|| Uuid.v4();
|
||||||
|
|
||||||
|
initManagement(logger.newRequestLogger(), overlay => {
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
if (overlay) {
|
||||||
|
this.loadedOverlay = overlay;
|
||||||
|
this.startServer();
|
||||||
|
} else {
|
||||||
|
error = new Error('failed to init management');
|
||||||
|
}
|
||||||
|
return cb(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
if (!this.wss) {
|
||||||
|
process.exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.wss.close(() => {
|
||||||
|
logger.info('server shutdown');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer() {
|
||||||
|
this.wss = new WebSocket.Server({
|
||||||
|
port: this.port,
|
||||||
|
clientTracking: true,
|
||||||
|
path: '/watch',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.wss.on('connection', this.onConnection.bind(this));
|
||||||
|
this.wss.on('listening', this.onListening.bind(this));
|
||||||
|
this.wss.on('error', this.onError.bind(this));
|
||||||
|
|
||||||
|
setInterval(this.checkBrokenConnections.bind(this),
|
||||||
|
CHECK_BROKEN_CONNECTIONS_FREQUENCY_MS);
|
||||||
|
|
||||||
|
addOverlayMessageListener(this.onNewOverlay.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnection(socket, request) {
|
||||||
|
function hearthbeat() {
|
||||||
|
this.isAlive = true;
|
||||||
|
}
|
||||||
|
logger.info('client connected to watch route', {
|
||||||
|
ip: request.connection.remoteAddress,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
socket.isAlive = true;
|
||||||
|
socket.on('pong', hearthbeat.bind(socket));
|
||||||
|
|
||||||
|
if (socket.readyState !== socket.OPEN) {
|
||||||
|
logger.error('client socket not in ready state', {
|
||||||
|
state: socket.readyState,
|
||||||
|
client: socket._socket._peername,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
messageType: managementAgentMessageType.NEW_OVERLAY,
|
||||||
|
payload: this.loadedOverlay,
|
||||||
|
};
|
||||||
|
socket.send(JSON.stringify(msg), error => {
|
||||||
|
if (error) {
|
||||||
|
logger.error('failed to send remoteOverlay to client', {
|
||||||
|
error,
|
||||||
|
client: socket._socket._peername,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onListening() {
|
||||||
|
logger.info('websocket server listening',
|
||||||
|
{ port: this.port });
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(error) {
|
||||||
|
logger.error('websocket server error', { error });
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendNewOverlayToClient(client) {
|
||||||
|
if (client.readyState !== client.OPEN) {
|
||||||
|
logger.error('client socket not in ready state', {
|
||||||
|
state: client.readyState,
|
||||||
|
client: client._socket._peername,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = {
|
||||||
|
messageType: managementAgentMessageType.NEW_OVERLAY,
|
||||||
|
payload: this.loadedOverlay,
|
||||||
|
};
|
||||||
|
client.send(JSON.stringify(msg), error => {
|
||||||
|
if (error) {
|
||||||
|
logger.error(
|
||||||
|
'failed to send remoteOverlay to management agent client', {
|
||||||
|
error, client: client._socket._peername,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onNewOverlay(remoteOverlay) {
|
||||||
|
const remoteOverlayObj = JSON.parse(remoteOverlay);
|
||||||
|
saveConfigurationVersion(
|
||||||
|
this.loadedOverlay, remoteOverlayObj, logger, err => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('failed to save remote overlay', { err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loadedOverlay = remoteOverlayObj;
|
||||||
|
this.wss.clients.forEach(
|
||||||
|
this._sendNewOverlayToClient.bind(this)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkBrokenConnections() {
|
||||||
|
this.wss.clients.forEach(client => {
|
||||||
|
if (!client.isAlive) {
|
||||||
|
logger.info('close broken connection', {
|
||||||
|
client: client._socket._peername,
|
||||||
|
});
|
||||||
|
client.terminate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.isAlive = false;
|
||||||
|
client.ping();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new ManagementAgentServer();
|
||||||
|
server.start();
|
12
package.json
12
package.json
|
@ -21,9 +21,10 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@azure/storage-blob": "^12.12.0",
|
"@azure/storage-blob": "^12.12.0",
|
||||||
"@hapi/joi": "^17.1.0",
|
"@hapi/joi": "^17.1.0",
|
||||||
"arsenal": "git+https://git.yourcmc.ru/vitalif/zenko-arsenal.git#development/8.1",
|
"arsenal": "git+https://github.com/scality/arsenal#8.1.130",
|
||||||
"async": "~2.5.0",
|
"async": "~2.5.0",
|
||||||
"aws-sdk": "2.905.0",
|
"aws-sdk": "2.905.0",
|
||||||
|
"bucketclient": "scality/bucketclient#8.1.9",
|
||||||
"bufferutil": "^4.0.6",
|
"bufferutil": "^4.0.6",
|
||||||
"commander": "^2.9.0",
|
"commander": "^2.9.0",
|
||||||
"cron-parser": "^2.11.0",
|
"cron-parser": "^2.11.0",
|
||||||
|
@ -40,13 +41,14 @@
|
||||||
"npm-run-all": "~4.1.5",
|
"npm-run-all": "~4.1.5",
|
||||||
"prom-client": "14.2.0",
|
"prom-client": "14.2.0",
|
||||||
"request": "^2.81.0",
|
"request": "^2.81.0",
|
||||||
"scubaclient": "git+https://git.yourcmc.ru/vitalif/zenko-scubaclient.git",
|
"scubaclient": "git+https://github.com/scality/scubaclient.git",
|
||||||
"sql-where-parser": "~2.2.1",
|
"sql-where-parser": "~2.2.1",
|
||||||
"utapi": "git+https://git.yourcmc.ru/vitalif/zenko-utapi.git",
|
"utapi": "github:scality/utapi#8.1.15",
|
||||||
"utf-8-validate": "^5.0.8",
|
"utf-8-validate": "^5.0.8",
|
||||||
"utf8": "~2.1.1",
|
"utf8": "~2.1.1",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"werelogs": "git+https://git.yourcmc.ru/vitalif/zenko-werelogs.git#development/8.1",
|
"vaultclient": "scality/vaultclient#8.3.20",
|
||||||
|
"werelogs": "scality/werelogs#8.1.5",
|
||||||
"ws": "^5.1.0",
|
"ws": "^5.1.0",
|
||||||
"xml2js": "~0.4.16"
|
"xml2js": "~0.4.16"
|
||||||
},
|
},
|
||||||
|
@ -54,7 +56,7 @@
|
||||||
"bluebird": "^3.3.1",
|
"bluebird": "^3.3.1",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^8.14.0",
|
||||||
"eslint-config-airbnb-base": "^13.1.0",
|
"eslint-config-airbnb-base": "^13.1.0",
|
||||||
"eslint-config-scality": "git+https://git.yourcmc.ru/vitalif/zenko-eslint-config-scality.git#8.2.0",
|
"eslint-config-scality": "scality/Guidelines#8.2.0",
|
||||||
"eslint-plugin-import": "^2.14.0",
|
"eslint-plugin-import": "^2.14.0",
|
||||||
"eslint-plugin-mocha": "^10.1.0",
|
"eslint-plugin-mocha": "^10.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const {
|
||||||
|
createWSAgent,
|
||||||
|
} = require('../../../lib/management/push');
|
||||||
|
|
||||||
|
const proxy = 'http://proxy:3128/';
|
||||||
|
const logger = { info: () => {} };
|
||||||
|
|
||||||
|
function testVariableSet(httpProxy, httpsProxy, allProxy, noProxy) {
|
||||||
|
return () => {
|
||||||
|
it(`should use ${httpProxy} environment variable`, () => {
|
||||||
|
let agent = createWSAgent('https://pushserver', {
|
||||||
|
[httpProxy]: 'http://proxy:3128',
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent, null);
|
||||||
|
|
||||||
|
agent = createWSAgent('http://pushserver', {
|
||||||
|
[httpProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent.proxy.href, proxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should use ${httpsProxy} environment variable`, () => {
|
||||||
|
let agent = createWSAgent('http://pushserver', {
|
||||||
|
[httpsProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent, null);
|
||||||
|
|
||||||
|
agent = createWSAgent('https://pushserver', {
|
||||||
|
[httpsProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent.proxy.href, proxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should use ${allProxy} environment variable`, () => {
|
||||||
|
let agent = createWSAgent('http://pushserver', {
|
||||||
|
[allProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent.proxy.href, proxy);
|
||||||
|
|
||||||
|
agent = createWSAgent('https://pushserver', {
|
||||||
|
[allProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent.proxy.href, proxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should use ${noProxy} environment variable`, () => {
|
||||||
|
let agent = createWSAgent('http://pushserver', {
|
||||||
|
[noProxy]: 'pushserver',
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent, null);
|
||||||
|
|
||||||
|
agent = createWSAgent('http://pushserver', {
|
||||||
|
[noProxy]: 'pushserver',
|
||||||
|
[httpProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent, null);
|
||||||
|
|
||||||
|
agent = createWSAgent('http://pushserver', {
|
||||||
|
[noProxy]: 'pushserver2',
|
||||||
|
[httpProxy]: proxy,
|
||||||
|
}, logger);
|
||||||
|
assert.equal(agent.proxy.href, proxy);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Websocket connection agent', () => {
|
||||||
|
describe('with no proxy env', () => {
|
||||||
|
it('should handle empty proxy environment', () => {
|
||||||
|
const agent = createWSAgent('https://pushserver', {}, logger);
|
||||||
|
assert.equal(agent, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with lowercase proxy env',
|
||||||
|
testVariableSet('http_proxy', 'https_proxy', 'all_proxy', 'no_proxy'));
|
||||||
|
|
||||||
|
describe('with uppercase proxy env',
|
||||||
|
testVariableSet('HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY'));
|
||||||
|
});
|
|
@ -0,0 +1,239 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const { DummyRequestLogger } = require('../helpers');
|
||||||
|
const log = new DummyRequestLogger();
|
||||||
|
|
||||||
|
const metadata = require('../../../lib/metadata/wrapper');
|
||||||
|
const managementDatabaseName = 'PENSIEVE';
|
||||||
|
const tokenConfigurationKey = 'auth/zenko/remote-management-token';
|
||||||
|
|
||||||
|
const { privateKey, accessKey, decryptedSecretKey, secretKey, canonicalId,
|
||||||
|
userName } = require('./resources.json');
|
||||||
|
const shortid = '123456789012';
|
||||||
|
const email = 'customaccount1@setbyenv.com';
|
||||||
|
const arn = 'arn:aws:iam::123456789012:root';
|
||||||
|
const { config } = require('../../../lib/Config');
|
||||||
|
|
||||||
|
const {
|
||||||
|
remoteOverlayIsNewer,
|
||||||
|
patchConfiguration,
|
||||||
|
} = require('../../../lib/management/configuration');
|
||||||
|
|
||||||
|
const {
|
||||||
|
initManagementDatabase,
|
||||||
|
} = require('../../../lib/management/index');
|
||||||
|
|
||||||
|
function initManagementCredentialsMock(cb) {
|
||||||
|
return metadata.putObjectMD(managementDatabaseName,
|
||||||
|
tokenConfigurationKey, { privateKey }, {},
|
||||||
|
log, error => cb(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original Config
|
||||||
|
const overlayVersionOriginal = Object.assign({}, config.overlayVersion);
|
||||||
|
const authDataOriginal = Object.assign({}, config.authData);
|
||||||
|
const locationConstraintsOriginal = Object.assign({},
|
||||||
|
config.locationConstraints);
|
||||||
|
const restEndpointsOriginal = Object.assign({}, config.restEndpoints);
|
||||||
|
const browserAccessEnabledOriginal = config.browserAccessEnabled;
|
||||||
|
const instanceId = '19683e55-56f7-4a4c-98a7-706c07e4ec30';
|
||||||
|
const publicInstanceId = crypto.createHash('sha256')
|
||||||
|
.update(instanceId)
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
function resetConfig() {
|
||||||
|
config.overlayVersion = overlayVersionOriginal;
|
||||||
|
config.authData = authDataOriginal;
|
||||||
|
config.locationConstraints = locationConstraintsOriginal;
|
||||||
|
config.restEndpoints = restEndpointsOriginal;
|
||||||
|
config.browserAccessEnabled = browserAccessEnabledOriginal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertConfig(actualConf, expectedConf) {
|
||||||
|
Object.keys(expectedConf).forEach(key => {
|
||||||
|
assert.deepStrictEqual(actualConf[key], expectedConf[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('patchConfiguration', () => {
|
||||||
|
before(done => initManagementDatabase(log, err => {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
return initManagementCredentialsMock(done);
|
||||||
|
}));
|
||||||
|
beforeEach(() => {
|
||||||
|
resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should modify config using the new config', done => {
|
||||||
|
const newConf = {
|
||||||
|
version: 1,
|
||||||
|
instanceId,
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
secretKey,
|
||||||
|
accessKey,
|
||||||
|
canonicalId,
|
||||||
|
userName,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
endpoints: [
|
||||||
|
{
|
||||||
|
hostname: '1.1.1.1',
|
||||||
|
locationName: 'us-east-1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locations: {
|
||||||
|
'us-east-1': {
|
||||||
|
name: 'us-east-1',
|
||||||
|
objectId: 'us-east-1',
|
||||||
|
locationType: 'location-file-v1',
|
||||||
|
legacyAwsBehavior: true,
|
||||||
|
details: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
browserAccess: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return patchConfiguration(newConf, log, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
const actualConf = getConfig();
|
||||||
|
const expectedConf = {
|
||||||
|
overlayVersion: 1,
|
||||||
|
publicInstanceId,
|
||||||
|
browserAccessEnabled: true,
|
||||||
|
authData: {
|
||||||
|
accounts: [{
|
||||||
|
name: userName,
|
||||||
|
email,
|
||||||
|
arn,
|
||||||
|
canonicalID: canonicalId,
|
||||||
|
shortid,
|
||||||
|
keys: [{
|
||||||
|
access: accessKey,
|
||||||
|
secret: decryptedSecretKey,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
locationConstraints: {
|
||||||
|
'us-east-1': {
|
||||||
|
type: 'file',
|
||||||
|
objectId: 'us-east-1',
|
||||||
|
legacyAwsBehavior: true,
|
||||||
|
isTransient: false,
|
||||||
|
sizeLimitGB: null,
|
||||||
|
details: { supportsVersioning: true },
|
||||||
|
name: 'us-east-1',
|
||||||
|
locationType: 'location-file-v1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assertConfig(actualConf, expectedConf);
|
||||||
|
assert.deepStrictEqual(actualConf.restEndpoints['1.1.1.1'],
|
||||||
|
'us-east-1');
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply second configuration if version (2) is greater than ' +
|
||||||
|
'overlayVersion (1)', done => {
|
||||||
|
const newConf1 = {
|
||||||
|
version: 1,
|
||||||
|
instanceId,
|
||||||
|
};
|
||||||
|
const newConf2 = {
|
||||||
|
version: 2,
|
||||||
|
instanceId,
|
||||||
|
browserAccess: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
patchConfiguration(newConf1, log, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
return patchConfiguration(newConf2, log, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
const actualConf = getConfig();
|
||||||
|
const expectedConf = {
|
||||||
|
overlayVersion: 2,
|
||||||
|
browserAccessEnabled: true,
|
||||||
|
};
|
||||||
|
assertConfig(actualConf, expectedConf);
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not apply the second configuration if version equals ' +
|
||||||
|
'overlayVersion', done => {
|
||||||
|
const newConf1 = {
|
||||||
|
version: 1,
|
||||||
|
instanceId,
|
||||||
|
};
|
||||||
|
const newConf2 = {
|
||||||
|
version: 1,
|
||||||
|
instanceId,
|
||||||
|
browserAccess: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
patchConfiguration(newConf1, log, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
return patchConfiguration(newConf2, log, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
const actualConf = getConfig();
|
||||||
|
const expectedConf = {
|
||||||
|
overlayVersion: 1,
|
||||||
|
browserAccessEnabled: undefined,
|
||||||
|
};
|
||||||
|
assertConfig(actualConf, expectedConf);
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('remoteOverlayIsNewer', () => {
|
||||||
|
it('should return remoteOverlayIsNewer equals false if remote overlay ' +
|
||||||
|
'is less than the cached', () => {
|
||||||
|
const cachedOverlay = {
|
||||||
|
version: 2,
|
||||||
|
};
|
||||||
|
const remoteOverlay = {
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
const isRemoteOverlayNewer = remoteOverlayIsNewer(cachedOverlay,
|
||||||
|
remoteOverlay);
|
||||||
|
assert.equal(isRemoteOverlayNewer, false);
|
||||||
|
});
|
||||||
|
it('should return remoteOverlayIsNewer equals false if remote overlay ' +
|
||||||
|
'and the cached one are equal', () => {
|
||||||
|
const cachedOverlay = {
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
const remoteOverlay = {
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
const isRemoteOverlayNewer = remoteOverlayIsNewer(cachedOverlay,
|
||||||
|
remoteOverlay);
|
||||||
|
assert.equal(isRemoteOverlayNewer, false);
|
||||||
|
});
|
||||||
|
it('should return remoteOverlayIsNewer equals true if remote overlay ' +
|
||||||
|
'version is greater than the cached one ', () => {
|
||||||
|
const cachedOverlay = {
|
||||||
|
version: 0,
|
||||||
|
};
|
||||||
|
const remoteOverlay = {
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
const isRemoteOverlayNewer = remoteOverlayIsNewer(cachedOverlay,
|
||||||
|
remoteOverlay);
|
||||||
|
assert.equal(isRemoteOverlayNewer, true);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEAj13sSYE40lAX2qpBvfdGfcSVNtBf8i5FH+E8FAhORwwPu+2S\r\n3yBQbgwHq30WWxunGb1NmZL1wkVZ+vf12DtxqFRnMA08LfO4oO6oC4V8XfKeuHyJ\r\n1qlaKRINz6r9yDkTHtwWoBnlAINurlcNKgGD5p7D+G26Chbr/Oo0ZwHula9DxXy6\r\neH8/bJ5/BynyNyyWRPoAO+UkUdY5utkFCUq2dbBIhovMgjjikf5p2oWqnRKXc+JK\r\nBegr6lSHkkhyqNhTmd8+wA+8Cace4sy1ajY1t5V4wfRZea5vwl/HlyyKodvHdxng\r\nJgg6H61JMYPkplY6Gr9OryBKEAgq02zYoYTDfwIDAQABAoIBAAuDYGlavkRteCzw\r\nRU1LIVcSRWVcgIgDXTu9K8T0Ec0008Kkxomyn6LmxmroJbZ1VwsDH8s4eRH73ckA\r\nxrZxt6Pr+0lplq6eBvKtl8MtGhq1VDe+kJczjHEF6SQHOFAu/TEaPZrn2XMcGvRX\r\nO1BnRL9tepFlxm3u/06VRFYNWqqchM+tFyzLu2AuiuKd5+slSX7KZvVgdkY1ErKH\r\ngB75lPyhPb77C/6ptqUisVMSO4JhLhsD0+ekDVY982Sb7KkI+szdWSbtMx9Ek2Wo\r\ntXwJz7I8T7IbODy9aW9G+ydyhMDFmaEYIaDVFKJj5+fluNza3oQ5PtFNVE50GQJA\r\nsisGqfECgYEAwpkwt0KpSamSEH6qknNYPOwxgEuXWoFVzibko7is2tFPvY+YJowb\r\n68MqHIYhf7gHLq2dc5Jg1TTbGqLECjVxp4xLU4c95KBy1J9CPAcuH4xQLDXmeLzP\r\nJ2YgznRocbzAMCDAwafCr3uY9FM7oGDHAi5bE5W11xWx+9MlFExL3JkCgYEAvJp5\r\nf+JGN1W037bQe2QLYUWGszewZsvplnNOeytGQa57w4YdF42lPhMz6Kc/zdzKZpN9\r\njrshiIDhAD5NCno6dwqafBAW9WZl0sn7EnlLhD4Lwm8E9bRHnC9H82yFuqmNrzww\r\nzxBCQogJISwHiVz4EkU48B283ecBn0wT/fAa19cCgYEApKWsnEHgrhy1IxOpCoRh\r\nUhqdv2k1xDPN/8DUjtnAFtwmVcLa/zJopU/Zn4y1ZzSzjwECSTi+iWZRQ/YXXHPf\r\nl92SFjhFW92Niuy8w8FnevXjF6T7PYiy1SkJ9OR1QlZrXc04iiGBDazLu115A7ce\r\nanACS03OLw+CKgl6Q/RR83ECgYBCUngDVoimkMcIHHt3yJiP3ikeAKlRnMdJlsa0\r\nXWVZV4hCG3lDfRXsnEgWuimftNKf+6GdfYSvQdLdiQsCcjT5A4uLsQTByv5nf4uA\r\n1ZKOsFrmRrARzxGXhLDikvj7yP//7USkq+0BBGFhfuAvl7fMhPceyPZPehqB7/jf\r\nxX1LBQKBgAn5GgSXzzS0e06ZlP/VrKxreOHa5Z8wOmqqYQ0QTeczAbNNmuITdwwB\r\nNkbRqpVXRIfuj0BQBegAiix8om1W4it0cwz54IXBwQULxJR1StWxj3jo4QtpMQ+z\r\npVPdB1Ilb9zPV1YvDwRfdS1xsobzznAx56ecsXduZjs9mF61db8Q\r\n-----END RSA PRIVATE KEY-----\r\n",
|
||||||
|
"publicKey": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAj13sSYE40lAX2qpBvfdG\r\nfcSVNtBf8i5FH+E8FAhORwwPu+2S3yBQbgwHq30WWxunGb1NmZL1wkVZ+vf12Dtx\r\nqFRnMA08LfO4oO6oC4V8XfKeuHyJ1qlaKRINz6r9yDkTHtwWoBnlAINurlcNKgGD\r\n5p7D+G26Chbr/Oo0ZwHula9DxXy6eH8/bJ5/BynyNyyWRPoAO+UkUdY5utkFCUq2\r\ndbBIhovMgjjikf5p2oWqnRKXc+JKBegr6lSHkkhyqNhTmd8+wA+8Cace4sy1ajY1\r\nt5V4wfRZea5vwl/HlyyKodvHdxngJgg6H61JMYPkplY6Gr9OryBKEAgq02zYoYTD\r\nfwIDAQAB\r\n-----END PUBLIC KEY-----\r\n",
|
||||||
|
"accessKey": "QXP3VDG3SALNBX2QBJ1C",
|
||||||
|
"secretKey": "K5FyqZo5uFKfw9QBtn95o6vuPuD0zH/1seIrqPKqGnz8AxALNSx6EeRq7G1I6JJpS1XN13EhnwGn2ipsml3Uf2fQ00YgEmImG8wzGVZm8fWotpVO4ilN4JGyQCah81rNX4wZ9xHqDD7qYR5MyIERxR/osoXfctOwY7GGUjRKJfLOguNUlpaovejg6mZfTvYAiDF+PTO1sKUYqHt1IfKQtsK3dov1EFMBB5pWM7sVfncq/CthKN5M+VHx9Y87qdoP3+7AW+RCBbSDOfQgxvqtS7PIAf10mDl8k2kEURLz+RqChu4O4S0UzbEmtja7wa7WYhYKv/tM/QeW7kyNJMmnPg==",
|
||||||
|
"decryptedSecretKey": "n7PSZ3U6SgerF9PCNhXYsq3S3fRKVGdZTicGV8Ur",
|
||||||
|
"canonicalId": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be",
|
||||||
|
"userName": "orbituser"
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const { getCapabilities } = require('../../../lib/utilities/reportHandler');
|
||||||
|
|
||||||
|
// Ensures that expected features are enabled even if they
|
||||||
|
// rely on optional dependencies (such as secureChannelOptimizedPath)
|
||||||
|
describe('report handler', () => {
|
||||||
|
it('should report current capabilities', () => {
|
||||||
|
const c = getCapabilities();
|
||||||
|
assert.strictEqual(c.locationTypeDigitalOcean, true);
|
||||||
|
assert.strictEqual(c.locationTypeS3Custom, true);
|
||||||
|
assert.strictEqual(c.locationTypeSproxyd, true);
|
||||||
|
assert.strictEqual(c.locationTypeHyperdriveV2, true);
|
||||||
|
assert.strictEqual(c.locationTypeLocal, true);
|
||||||
|
assert.strictEqual(c.preferredReadLocation, true);
|
||||||
|
assert.strictEqual(c.managedLifecycle, true);
|
||||||
|
assert.strictEqual(c.secureChannelOptimizedPath, true);
|
||||||
|
assert.strictEqual(c.s3cIngestLocation, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{ value: 'true', result: true },
|
||||||
|
{ value: 'TRUE', result: true },
|
||||||
|
{ value: 'tRuE', result: true },
|
||||||
|
{ value: '1', result: true },
|
||||||
|
{ value: 'false', result: false },
|
||||||
|
{ value: 'FALSE', result: false },
|
||||||
|
{ value: 'FaLsE', result: false },
|
||||||
|
{ value: '0', result: false },
|
||||||
|
{ value: 'foo', result: false },
|
||||||
|
{ value: '', result: true },
|
||||||
|
{ value: undefined, result: true },
|
||||||
|
].forEach(param =>
|
||||||
|
it(`should allow set local file system capability ${param.value}`, () => {
|
||||||
|
const OLD_ENV = process.env;
|
||||||
|
|
||||||
|
if (param.value !== undefined) process.env.LOCAL_VOLUME_CAPABILITY = param.value;
|
||||||
|
assert.strictEqual(getCapabilities().locationTypeLocal, param.result);
|
||||||
|
|
||||||
|
process.env = OLD_ENV;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,72 @@
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const {
|
||||||
|
ChannelMessageV0,
|
||||||
|
MessageType,
|
||||||
|
TargetType,
|
||||||
|
} = require('../../../lib/management/ChannelMessageV0');
|
||||||
|
|
||||||
|
const {
|
||||||
|
CONFIG_OVERLAY_MESSAGE,
|
||||||
|
METRICS_REQUEST_MESSAGE,
|
||||||
|
METRICS_REPORT_MESSAGE,
|
||||||
|
CHANNEL_CLOSE_MESSAGE,
|
||||||
|
CHANNEL_PAYLOAD_MESSAGE,
|
||||||
|
} = MessageType;
|
||||||
|
|
||||||
|
const { TARGET_ANY } = TargetType;
|
||||||
|
|
||||||
|
describe('ChannelMessageV0', () => {
|
||||||
|
describe('codec', () => {
|
||||||
|
it('should roundtrip metrics report', () => {
|
||||||
|
const b = ChannelMessageV0.encodeMetricsReportMessage({ a: 1 });
|
||||||
|
const m = new ChannelMessageV0(b);
|
||||||
|
|
||||||
|
assert.strictEqual(METRICS_REPORT_MESSAGE, m.getType());
|
||||||
|
assert.strictEqual(0, m.getChannelNumber());
|
||||||
|
assert.strictEqual(m.getTarget(), TARGET_ANY);
|
||||||
|
assert.strictEqual(m.getPayload().toString(), '{"a":1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should roundtrip channel data', () => {
|
||||||
|
const data = new Buffer('dummydata');
|
||||||
|
const b = ChannelMessageV0.encodeChannelDataMessage(50, data);
|
||||||
|
const m = new ChannelMessageV0(b);
|
||||||
|
|
||||||
|
assert.strictEqual(CHANNEL_PAYLOAD_MESSAGE, m.getType());
|
||||||
|
assert.strictEqual(50, m.getChannelNumber());
|
||||||
|
assert.strictEqual(m.getTarget(), TARGET_ANY);
|
||||||
|
assert.strictEqual(m.getPayload().toString(), 'dummydata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should roundtrip channel close', () => {
|
||||||
|
const b = ChannelMessageV0.encodeChannelCloseMessage(3);
|
||||||
|
const m = new ChannelMessageV0(b);
|
||||||
|
|
||||||
|
assert.strictEqual(CHANNEL_CLOSE_MESSAGE, m.getType());
|
||||||
|
assert.strictEqual(3, m.getChannelNumber());
|
||||||
|
assert.strictEqual(m.getTarget(), TARGET_ANY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decoder', () => {
|
||||||
|
it('should parse metrics request', () => {
|
||||||
|
const b = new Buffer([METRICS_REQUEST_MESSAGE, 0, 0]);
|
||||||
|
const m = new ChannelMessageV0(b);
|
||||||
|
|
||||||
|
assert.strictEqual(METRICS_REQUEST_MESSAGE, m.getType());
|
||||||
|
assert.strictEqual(0, m.getChannelNumber());
|
||||||
|
assert.strictEqual(m.getTarget(), TARGET_ANY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse overlay push', () => {
|
||||||
|
const b = new Buffer([CONFIG_OVERLAY_MESSAGE, 0, 0, 34, 65, 34]);
|
||||||
|
const m = new ChannelMessageV0(b);
|
||||||
|
|
||||||
|
assert.strictEqual(CONFIG_OVERLAY_MESSAGE, m.getType());
|
||||||
|
assert.strictEqual(0, m.getChannelNumber());
|
||||||
|
assert.strictEqual(m.getTarget(), TARGET_ANY);
|
||||||
|
assert.strictEqual(m.getPayload().toString(), '"A"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue