aws-ssm
Version:
240 lines (239 loc) • 9.31 kB
JavaScript
import { SSM as AwsSSM } from "aws-sdk";
import { getAwsCredentials } from "./getAwsCredentials";
import { parseForNameComponents } from "./parseForNameComponents";
import { coerceValueToString, findPriorDescription, getLatestVersion, addModuleName, getSpecificVersion } from "./utils";
export * from "./parseForNameComponents";
export * from "./types";
export class SSM {
constructor(config = {}) {
this._cli = false;
if (config.profile && typeof config.profile === "string") {
const credentials = config.credentialsDirectory
? getAwsCredentials(config.profile, config.credentialsDirectory)
: getAwsCredentials(config.profile);
config.region = config.region ? config.region : credentials.region;
delete credentials.region;
this._credentials = credentials;
}
else if (config.profile) {
this._credentials = config.profile;
}
this._cli = config.cli ? config.cli : false;
this._region = config.region;
this._defaultType = config.defaultType || "SecureString";
const awsSsmConfig = {
apiVersion: "2014-11-06",
region: this._region
};
if (this._credentials) {
awsSsmConfig.credentials = this._credentials;
}
this._ssm = new AwsSSM(awsSsmConfig);
}
/**
* Instantiates SSM using the default configuration and calls the `module` instance method.
*/
static async modules(mods, options = {}) {
return options.config
? new SSM(options.config).modules(mods, options)
: new SSM().modules(mods, options);
}
get configuration() {
return {
credentials: this._credentials,
cli: this._cli,
region: this._region,
defaultType: this._defaultType
};
}
async get(Name, options = {}) {
const request = {
Name: options.nonStandardPath
? Name
: buildPathFromNameComponents(parseForNameComponents(Name))
};
if (options.decrypt) {
request.WithDecryption = true;
}
const data = await this._ssm.getParameter(request).promise();
const result = {
path: data.Parameter.Name,
type: data.Parameter.Type,
arn: data.Parameter.ARN,
version: data.Parameter.Version,
value: data.Parameter.Value,
encrypted: !options.decrypt && data.Parameter.Type === "SecureString"
? true
: false,
lastUpdated: data.Parameter.LastModifiedDate
};
return result;
}
async convertToEnv(Name, options = {}) {
const result = await this.get(Name, options);
if (result.encrypted) {
const e = new Error(`Can not convert to ENV an encrypted value at "${Name}"; always use { decrypt: true } config.`);
e.name = "NotAllowed";
throw e;
}
const varName = `${result.parts.module.toUpperCase()}_${result.parts.name.toUpperCase()}`;
process.env[varName] = String(result.value);
}
/**
* put
*
* Puts a value into SSM and then returns the version number
* on success.
*
* @param Name The name/path of the variable
* @param Value The value to set to
* @param options Any additional options needed
*/
async put(Name, Value, options = {}) {
const parts = parseForNameComponents(Name);
Value = coerceValueToString(Value);
const Description = options.description
? options.description
: Boolean(options.override)
? await findPriorDescription(Name)
: "";
const request = {
Name,
Value,
Description,
Overwrite: options.override || false,
Type: options.encrypt !== undefined
? options.encrypt
? "SecureString"
: "String"
: this._defaultType
};
if (options.encryptionKey) {
request.KeyId = options.encryptionKey;
}
const result = await this._ssm.putParameter(request).promise();
return result.Version;
}
/**
* list
*
* Lists the SSM Parameters available. If you wish to have a scoped/subset of parameters
* you should use optional "options" parameter. When you pass in a "string" this is
* the same as setting the "path" option but does remove your ability to set other options.
*
* @param pathOrOptions the hierarchical path position to start the recursive
* search for parameters, OR a configuration object
*/
async list(pathOrOptions = { path: "/" }) {
const o = typeof pathOrOptions === "string"
? { path: pathOrOptions }
: pathOrOptions;
const request = {
Path: o.path || "/",
Recursive: true,
WithDecryption: o.decrypt
};
const ssmParameters = [];
let response;
do {
response = await this._ssm.getParametersByPath(request).promise();
ssmParameters.push(...response.Parameters);
} while (response.NextToken && (request.NextToken = response.NextToken));
const parameters = ssmParameters.map(i => (Object.assign(Object.assign({}, i), { encrypted: o.decrypt ? false : true })));
return parameters;
}
/**
* Produces a list of SSM parameters; this is very similar to the `list()` method
* but the data that is returned includes the last person/user to touch the value
* and _does not_ include the value of the secret.
*/
async describeParameters(options = { MaxResults: 50 }) {
const describeParameters = this._ssm.describeParameters(options).promise();
let results = await describeParameters;
if (results.NextToken) {
options.NextToken = results.NextToken;
const r2 = await this._ssm.describeParameters(options).promise();
results.Parameters = results.Parameters.concat(r2.Parameters);
}
return results.Parameters;
}
/**
* values
*
* An "alias" to the list() function except that values are decrypted
*
* @param path the hierarchical path position to start the recursive
* search for parameters
*/
async values(path = "/") {
return this.list({ path, decrypt: true });
}
/**
* modules
*
* returns the list of parameters in the current STAGE which are related to a given
* module/app. You can optionally specify a specific version.
*
* @param mods an array of modules/apps to look for
* @param options state the explicit app version to use or
* @return returns a hash who's keys are the module name; each module will be a hash who's keys are the key's property name
*/
async modules(mods, options = {}) {
mods = Array.isArray(mods) ? mods : [mods];
if (!process.env.AWS_STAGE) {
const err = new Error(`Can not use ssm.modules() without having set AWS_STAGE environment variable first!`);
err.name = "NotAllowed";
throw err;
}
const params = await this.values("/" + process.env.AWS_STAGE);
const intermediate = {};
params.forEach(p => {
const parts = parseForNameComponents(p.Name);
const { version, module, name } = parts;
if (mods.indexOf(module) !== -1) {
if (!intermediate[module]) {
intermediate[module] = {};
}
if (!intermediate[module][version]) {
intermediate[module][version] = {};
}
intermediate[module][version][name] = p;
}
});
return mods.reduce((prev, mod) => {
return Object.assign(Object.assign({}, prev), {
[mod]: options.version
? addModuleName(mod, getSpecificVersion(intermediate[mod], options.version, options.verbose), options.verbose)
: addModuleName(mod, getLatestVersion(intermediate[mod], options.verbose), options.verbose)
});
}, {});
}
async delete(Name, options = {}) {
const request = {
Name: options.nonStandardPath
? Name
: buildPathFromNameComponents(parseForNameComponents(Name))
};
try {
await this._ssm.deleteParameter(request).promise();
}
catch (e) {
if (e.name === "ParameterNotFound") {
const err = new Error(`The parameter "${Name}" could not be found!`);
err.name = "ParameterNotFound";
err.stack = e.stack;
throw err;
}
else {
throw e;
}
}
}
}
export function buildPathFromNameComponents(parts) {
const base = `${parts.stage}/${String(parts.version)}`;
const remaining = parts.module
? `/${parts.module}/${parts.name}`
: `/${parts.name}`;
return "/" + base + remaining;
}