@itentialopensource/adapter-godaddy
Version:
This adapter integrates with system described as: GoDaddy REST API v1 and v2.
490 lines (454 loc) • 17.7 kB
JavaScript
/* @copyright Itential, LLC 2020 */
/* eslint import/no-extraneous-dependencies: warn */
/* eslint global-require: warn */
/* eslint import/no-dynamic-require: warn */
/* eslint-disable no-console */
const path = require('path');
const cp = require('child_process');
const fs = require('fs-extra');
module.exports = {
SERVICE_CONFIGS_COLLECTION: 'service_configs',
IAP_PROFILES_COLLECTION: 'iap_profiles',
/**
* @summary update newConnection properties in adapter config
*
* @function updateNewConnection
* @param {Object} config - adaper configuration object required by IAP
* @param {Object} newConnection - connection related property collection from user
*/
updateNewConnection: (config, newConnection) => {
const updatedConfig = JSON.parse(JSON.stringify(config));
Object.keys(newConnection).forEach((key) => {
updatedConfig.properties.properties[key] = newConnection[key];
});
return updatedConfig;
},
/**
* @summary assemble heathcheck endpoint into an URL
*
* @function getHealthCheckEndpointURL
* @param {Object} endpoint - user updated healthcheck endpoint object
* @param {Object} config - adaper configuration object required by IAP
*/
getHealthCheckEndpointURL: (endpoint, config) => {
const p = config.properties.properties;
const healthCheckEndpointURL = `${p.protocol}://${p.host}${p.base_path}${p.version}${endpoint.healthCheckEndpoint}`;
console.log({ healthCheckEndpointURL });
return healthCheckEndpointURL;
},
/**
* @summary persist healthcheck endpoint when user make update
*
* @function updateHealthCheckEndpoint
* @param {Object} newHealthCheckEndpoint - user confirmed healthcheck object
* @param {Object} healthCheckEndpoint - existing healthcheck object
* @param {Object} healthcheck - ./entities/.system/action.json object
*/
updateHealthCheckEndpoint: (newHealthCheckEndpoint, healthCheckEndpoint, healthcheck) => {
if (newHealthCheckEndpoint.healthCheckEndpoint !== healthCheckEndpoint.healthCheckEndpoint) {
const p = healthcheck.actions[1].entitypath;
const newEntitypath = p.slice(0, 21) + newHealthCheckEndpoint.healthCheckEndpoint + p.slice(p.length - 8);
const updatedHealthcheck = JSON.parse(JSON.stringify(healthcheck));
updatedHealthcheck.actions[1].entitypath = newEntitypath;
console.log('updating healthcheck setting');
fs.writeFileSync('./entities/.system/action.json', JSON.stringify(updatedHealthcheck, null, 2));
}
},
/**
* @summary update authentication property given new input value from user
* compare values from auth and newAuth, if there's difference
* update adapter config
* @function updateAuth
* @param {Object} newAuth - user confirmed authentication object
* @param {Object} auth - existing authentication object
* @param {Object} config - adaper configuration object required by IAP
*/
updateAuth: (newAuth, auth, config) => {
const updatedConfig = JSON.parse(JSON.stringify(config));
if (Object.keys(newAuth).every((key) => newAuth[key] === auth[key])) {
return config;
}
Object.keys(newAuth).forEach((key) => {
updatedConfig.properties.properties.authentication[key] = newAuth[key];
});
console.log(updatedConfig.properties.properties.authentication);
return updatedConfig;
},
/**
* @summary add mark current auth_method with `(current)`
*
* @function getDisplayAuthOptions
* @param {String} currentAuth - current auth method in adapter config
* @param {Array} authOptions - available auth method
*/
getDisplayAuthOptions: (currentAuth, authOptions) => {
const displayAuthOptions = JSON.parse(JSON.stringify(authOptions));
displayAuthOptions[authOptions.indexOf(currentAuth)] += ' (current)';
return displayAuthOptions;
},
/**
* @summary decrypt IAP properties
* code from pronghorn-core/migration_scripts/installService.js
*
* @function decryptProperties
*/
decryptProperties: (props, iapDir) => {
const { PropertyEncryption } = require(path.join(iapDir, 'node_modules/@itential/itential-utils'));
const propertyEncryption = new PropertyEncryption({
algorithm: 'aes-256-ctr',
key: 'TG9uZ0Rpc3RhbmNlUnVubmVyUHJvbmdob3JuCg==',
encoding: 'utf-8'
});
return propertyEncryption.decryptProps(props);
},
/**
* @summary create connection object for verification
*
* @function getConnection
* @param {Object} props - adapter config.properties
*/
getConnection: (props) => {
const connection = {
host: props.properties.host,
base_path: props.properties.base_path,
protocol: props.properties.protocol,
version: props.properties.version,
port: props.properties.port
};
return connection;
},
/**
* @summary update connection properties based on user answer
*
* @function getNewProps
* @param {Array} answers - values collected from CLI
* @param {Object} connection - connection property verified by user
*/
getNewProps: (answers, connection) => {
if (answers.every((answer) => answer === '')) {
return connection;
}
const newConnection = {};
const properties = Object.keys(connection);
for (let i = 0; i < answers.length; i += 1) {
if (answers[i]) {
newConnection[properties[i]] = Number.isNaN(Number(answers[i])) ? answers[i] : Number(answers[i]);
} else {
newConnection[properties[i]] = connection[properties[i]];
}
}
return newConnection;
},
/**
* @summary extract endpoint string from healthcheck object
*
* @function getHealthCheckEndpoint
* @param {Object} healthcheck - {Object} healthcheck - ./entities/.system/action.json object
*/
getHealthCheckEndpoint: (healthcheck) => {
const endpoint = healthcheck.actions[1].entitypath.slice(21, healthcheck.actions[1].entitypath.length - 8);
return { healthCheckEndpoint: endpoint };
},
/**
* @summary Verify that the adapter is in the correct directory
* - Within IAP
* - In node_modules/@ namespace
* verify the adapter is installed under node_modules/
* and is consistent with the name property of package.json
* and the node_modules/ is in the correct path within IAP
* @param {String} dirname - current path
* @param {String} name - name property from package.json
*/
verifyInstallationDir: (dirname, name) => {
const pathArray = dirname.split(path.sep);
const expectedPath = `node_modules/${name}`;
const currentPath = pathArray.slice(pathArray.length - 3, pathArray.length).join('/');
if (currentPath.trim() !== expectedPath.trim()) {
throw new Error(`adapter should be installed under ${expectedPath} but is installed under ${currentPath}`);
}
const serverFile = path.join(dirname, '../../../', 'server.js');
if (!fs.existsSync(serverFile)) {
throw new Error(`adapter should be installed under IAP/${expectedPath}`);
}
console.log(`adapter correctly installed at ${currentPath}`);
},
/**
* @summary execute command and preserve the output the same as run command in shell
*
* @function systemSync
* @param {String} cmd - Command to execute
* @param {boolean} process - Whether stdout should be processed and returned
*/
systemSync: function systemSync(cmd, process) {
if (process) {
let stdout;
try {
stdout = cp.execSync(cmd).toString();
} catch (error) {
console.log('execute command error', error.stdout.toString(), error.stderr.toString());
stdout = error.stdout.toString();
}
const output = this.getTestCount(stdout);
output.stdout = stdout;
return output;
}
try {
return cp.execSync(cmd, { stdio: 'inherit' });
} catch (error) {
return console.error(error.stdout);
}
},
/**
* @summary parses a string and returns the number parsed from startIndex backwards
*
* @function parseNum
* @param {String} inputStr - Any String
* @param {Number} startIndex - Index to begin parsing
*/
parseNum: function parseNum(inputStr, startIndex) {
let count = '';
let currChar;
let start = startIndex;
while (currChar !== ' ') {
currChar = inputStr.charAt(start);
count = currChar + count;
start -= 1;
}
return parseInt(count, 10);
},
/**
* @summary Parses a mocha test result and returns the count of passing and failing tests
*
* @function getTestCount
* @param {String} testStr - Output from mocha test
*/
getTestCount: function getTestCount(testStr) {
const passIndex = testStr.search('passing');
const failIndex = testStr.search('failing');
const passCount = passIndex >= 0 ? this.parseNum(testStr, passIndex - 2) : 0;
const failCount = failIndex >= 0 ? this.parseNum(testStr, failIndex - 2) : 0;
return { passCount, failCount };
},
/**
* @summary remove package-lock.json and node_modules directory if exists
* run npm install and print result to stdout
*/
npmInstall: function npmInstall() {
fs.removeSync('../package-lock.json');
fs.removeSync('../node_modules/');
console.log('Run npm install ...');
this.systemSync('npm install');
},
/**
* @summary run lint, unit test and integration test
* print result to stdout
*/
runTest: function runTest() {
this.systemSync('npm run lint:errors');
this.systemSync('npm run test:unit');
this.systemSync('npm run test:integration');
},
/**
* @summary run basicget with mocha
* @param {boolean} scriptFlag - whether the function is ran from a script
* print result to stdout
* returns mocha test results otherwise
*/
runBasicGet: function runBasicGet(scriptFlag) {
const testPath = path.resolve(__dirname, '..', 'test/integration/adapterTestBasicGet.js');
return this.systemSync(`mocha ${testPath} --exit`, !scriptFlag);
},
/**
* @summary run connectivity with mocha
* @param {String} host - Host url to run healthcheck
* @param {boolean} scriptFlag - Whether the function is ran from a script
* print result to stdout if ran from script
* returns mocha test results otherwise
*/
runConnectivity: function runConnectivity(host, scriptFlag) {
let testPath = 'test/integration/adapterTestConnectivity.js';
let executable = 'mocha';
if (!scriptFlag) {
testPath = path.resolve(__dirname, '..', testPath);
executable = path.join(__dirname, '..', 'node_modules/mocha/bin/mocha');
}
return this.systemSync(`${executable} ${testPath} --HOST=${host} --timeout 10000 --exit`, !scriptFlag);
},
/**
* @summary create Adapter property
*
* @function createAdapter
* @param {Object} pronghornProps - decrypted 'properties.json' from IAP root directory
* @param {Object} profileItem - pronghorn props saved in database
* @param {Object} adapterPronghorn - ./pronghorn.json in adapter dir
* @param {Object} sampleProperties - './sampleProperties.json' in adapter dir
*/
createAdapter: function createAdapter(pronghornProps, profileItem, sampleProperties, adapterPronghorn) {
const iapDir = this.getIAPHome();
const packageFile = path.join(iapDir, 'package.json');
const info = JSON.parse(fs.readFileSync(packageFile));
const version = parseInt(info.version.split('.')[0], 10);
let adapter = {};
if (version >= 2020) {
adapter = {
isEncrypted: pronghornProps.pathProps.encrypted,
model: adapterPronghorn.id,
name: sampleProperties.id,
type: adapterPronghorn.type,
properties: sampleProperties,
loggerProps: profileItem.loggerProps
};
} else {
adapter = {
mongoProps: pronghornProps.mongoProps,
isEncrypted: pronghornProps.pathProps.encrypted,
model: adapterPronghorn.id,
name: sampleProperties.id,
type: adapterPronghorn.type,
properties: sampleProperties,
redisProps: profileItem.redisProps,
loggerProps: profileItem.loggerProps,
rabbitmq: profileItem.rabbitmq
};
adapter.mongoProps.pdb = true;
}
adapter.loggerProps.log_filename = `adapter-${adapter.name}.log`;
return adapter;
},
getPronghornProps: function getPronghornProps() {
const iapDir = this.getIAPHome();
console.log('Retrieving properties.json file...');
const rawProps = require(path.join(iapDir, 'properties.json'));
console.log('Decrypting properties...');
const pronghornProps = this.decryptProperties(rawProps, iapDir);
console.log('Found properties.\n');
return pronghornProps;
},
getAllAdapterInstances: async function getAllAdapterInstances() {
const database = await this.getIAPDatabaseConnection();
const { name } = require(path.join(__dirname, '..', 'package.json'));
const query = { model: name };
const options = { projection: { name: 1 } };
const adapterInstancesNames = await database.collection(this.SERVICE_CONFIGS_COLLECTION).find(
query,
options
).toArray();
return adapterInstancesNames;
},
// get database connection and existing adapter config
getAdapterConfig: async function getAdapterConfig(adapterId) {
const database = await this.getIAPDatabaseConnection();
const { name } = require(path.join(__dirname, '..', 'package.json'));
let query = {};
if (!adapterId) {
query = { model: name };
} else {
query = { _id: adapterId };
}
const serviceItem = await database.collection(this.SERVICE_CONFIGS_COLLECTION).findOne(
query
);
const pronghornProps = await this.getPronghornProps();
return { database, serviceItem, pronghornProps };
},
/**
* @summary return async healthcheck result as a Promise
*
* @function request
* @param {Adapter} a - Adapter instance
*/
request: function request(a) {
return new Promise((resolve, reject) => {
a.healthCheck(null, (data) => {
if (!data) reject(new Error('healthCheckEndpoint failed'));
resolve(data);
});
});
},
/**
* @summary deal with healthcheck response returned from adapter instace
*
* @function healthCheck
* @param {Adapter} a - Adapter instance
*/
healthCheck: async function healthCheck(a) {
const result = await this.request(a)
.then((res) => {
console.log('healthCheckEndpoint OK');
return res;
})
.catch((error) => {
console.error(error.message);
return false;
});
return result;
},
/**
* @summary Obtain the IAP installation directory depending on how adapter is used:
* by IAP, or by npm run CLI interface
* @returns IAP installation directory or null if adapter running without IAP
* @function getIAPHome
*/
getIAPHome: function getIAPHome() {
let IAPHomePath = null;
// check if adapter started via IAP, use path injected by core
if (process.env.iap_home) IAPHomePath = process.env.iap_home;
// check if adapter started via CLI `npm run <command>` so we have to be located under
// <IAP_HOME>/node_modules/@itentialopensource/<adapter_name>/ directory
const currentExecutionPath = this.getCurrentExecutionPath();
if (currentExecutionPath.indexOf('/node_modules') >= 0) {
[IAPHomePath] = currentExecutionPath.split('/node_modules');
}
return IAPHomePath;
},
/**
* @summary get current execution path without resolving symbolic links,
* use `pwd` command wihout '-P' option (resolving symlinks) https://linux.die.net/man/1/pwd
* @returns
* @function getCurrentExecutionPAth
*/
getCurrentExecutionPath: function getCurrentExecutionPAth() {
const { stdout } = this.systemSync('pwd', true);
return stdout.trim();
},
/**
* @summary checks if command executed from <IAP_HOME>/node_modules/@itentialopensource/<adapter_name>/ location
* @returns true if command executed under <IAP_HOME>/node_modules/@itentialopensource/<adapter_name>/ path
* @function areWeUnderIAPinstallationDirectory
*/
areWeUnderIAPinstallationDirectory: function areWeUnderIAPinstallationDirectory() {
return path.join(this.getCurrentExecutionPath(), '../../..') === this.getIAPHome();
},
getIAPDatabaseConnection: async function getIAPDatabaseConnection() {
const pronghornProps = await this.getPronghornProps();
const database = await this.connect(pronghornProps);
return database;
},
/**
* @summary connect to mongodb
*
* @function connect
* @param {Object} properties - pronghornProps
*/
connect: async function connect(properties) {
let dbConnectionProperties = {};
if (properties.mongoProps) {
dbConnectionProperties = properties.mongoProps;
} else if (properties.mongo) {
if (properties.mongo.url) {
dbConnectionProperties.url = properties.mongo.url;
} else {
dbConnectionProperties.url = `mongodb://${properties.mongo.host}:${properties.mongo.port}`;
}
dbConnectionProperties.db = properties.mongo.database;
}
if (!dbConnectionProperties.url || !dbConnectionProperties.db) {
throw new Error('Mongo properties are not specified in IAP configuration!');
}
const iapDir = this.getIAPHome();
const { MongoDBConnection } = require(path.join(iapDir, 'node_modules/@itential/database'));
const connection = new MongoDBConnection(dbConnectionProperties);
const database = await connection.connect(true);
return database;
}
};