-
-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathclient.js
192 lines (172 loc) · 7.16 KB
/
client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
'use strict';
// Modules
const _ = require('lodash');
const fs = require('fs');
const axios = require('axios');
// Set a limit on amount of sites
const MAX_SITES = 5000;
/**
* Helper to collect relevant error data from API responses
*
* @param {Error} err - The error object from the API response
* @return {Object} Formatted error data object containing code, message and details
*/
const getErrorData = (err = {}) => ({
code: _.get(err, 'response.status', 200),
codeText: _.get(err, 'response.statusText'),
method: _.upperCase(_.get(err, 'response.config.method'), 'GET'),
path: _.get(err, 'response.config.url'),
response: _.get(err, 'response.data'),
});
/**
* Helper to make HTTP requests to the Pantheon API
*
* @param {Object} request - Axios request instance
* @param {Object} log - Logger instance
* @param {string} verb - HTTP method to use (get, post, etc)
* @param {Array} pathname - URL path segments to join
* @param {Object} data - Request payload data
* @param {Object} options - Additional request options
* @return {Promise<Object>} Response data from API
*/
const pantheonRequest = (request, log, verb, pathname, data = {}, options = {}) => {
// Log the actual request we are about to make
log.verbose('making %s request to %s', verb, `${_.get(request, 'defaults.baseURL')}${pathname.join('/')}`);
log.debug('request sent data with %j', options, _.clone(data));
// Attempt the request and retry a few times
return new Promise((resolve, reject) => request[verb](pathname.join('/'), data, options)
.then(response => {
log.verbose('response recieved: %s with code %s', response.statusText, response.status);
log.silly('response data', response.data);
resolve(response.data);
})
.catch(err => {
const data = getErrorData(err);
const msg = [
`${data.method} request to ${data.path} failed with code ${data.code}: ${data.codeText}.`,
`The server responded with the message ${data.response}.`,
];
// @NOTE: it's not clear to me why we make this into a message instead of passing through
// the entire data object, possibly the reason has been lost to the sands of time
reject(new Error(msg.join(' ')));
}));
};
/**
* Client for interacting with Pantheon's Terminus API
* Handles authentication and API requests for sites, environments, and user data
* @todo: add some validation around the session eg throw an error if we make a request
* with a unauthorized client
* @todo: we can remove the mode from here and just extend this in other things
*/
module.exports = class PantheonApiClient {
/**
* Create a new Pantheon API client instance
*
* @param {string} token - Pantheon machine token for authentication
* @param {Function} log - Logging function to use
* @param {string} mode - Client mode ('node' or 'browser')
*/
constructor(token = '', log = console.log, mode = 'node') {
this.baseURL = 'https://terminus.pantheon.io/api/';
this.log = log;
this.token = token;
this.mode = mode;
}
/**
* Authenticate with Pantheon using machine token
*
* @param {string} token - Pantheon machine token
* @return {Promise<Object>} Session data from successful auth
*/
async auth(token = this.token) {
const data = {machine_token: token, client: 'terminus'};
const options = (this.mode === 'node') ? {headers: {'User-Agent': 'Terminus/Lando'}} : {};
const upath = ['authorize', 'machine-token'];
// get the auth
const auth = await pantheonRequest(axios.create({baseURL: this.baseURL}), this.log, 'post', upath, data, options);
// and set stuff with it
this.token = token;
this.session = auth;
// set headers
const headers = {'Content-Type': 'application/json'};
// Add header if we are in node mode, otherwise assume its set upstream in the browser
if (this.mode === 'node') headers['X-Pantheon-Session'] = auth.session;
this.request = axios.create({baseURL: this.baseURL, headers});
return auth;
}
/**
* Get information about a specific Pantheon site
*
* @param {string} id - Site name or ID
* @param {boolean} full - Whether to return full site details
* @return {Promise<Object>} Site information
*/
async getSite(id, full = true) {
const site = await pantheonRequest(this.request, this.log, 'get', ['site-names', id]);
// if not full then just return the lookup
if (!full) return site;
// otherwise return the full site
return await pantheonRequest(this.request, this.log, 'get', ['sites', site.id]);
}
/**
* Get all sites available to the authenticated user
* Combines sites from both user memberships and organization memberships
*
* @return {Promise<Array>} Array of site objects
*/
async getSites() {
// Call to get user sites
const pantheonUserSites = async () => {
const getSites = ['users', _.get(this.session, 'user_id'), 'memberships', 'sites'];
const sites = await pantheonRequest(this.request, this.log, 'get', getSites, {params: {limit: MAX_SITES}});
return _.map(sites, site => _.merge(site, site.site));
};
// Call to get org sites
const pantheonOrgSites = async () => {
const getOrgs = ['users', _.get(this.session, 'user_id'), 'memberships', 'organizations'];
const orgs = await pantheonRequest(this.request, this.log, 'get', getOrgs);
return await Promise.all(orgs.filter(org => org.role !== 'unprivileged').map(async org => {
const getOrgsSites = ['organizations', org.id, 'memberships', 'sites'];
const sites = await pantheonRequest(this.request, this.log, 'get', getOrgsSites, {params: {limit: MAX_SITES}});
return sites.map(site => _.merge(site, site.site));
}))
.then(sites => _.flatten(sites));
};
// Run both requests
return await Promise.all([pantheonUserSites(), pantheonOrgSites()])
// Combine, cache and all the things
.then(sites => _.compact(_.sortBy(_.uniqBy(_.flatten(sites), 'name'), 'name')))
// Filter out any frozen sites
.then(sites => sites.filter(site => !site.frozen));
}
/**
* Get all environments for a specific site
*
* @param {string} site - Site name or ID
* @return {Promise<Array>} Array of environment objects
*/
async getSiteEnvs(site) {
const envs = await pantheonRequest(this.request, this.log, 'get', ['sites', site, 'environments']);
return _.map(envs, (data, id) => _.merge({}, data, {id}));
}
/**
* Get authenticated user's account information
*
* @return {Promise<Object>} User account data
*/
async getUser() {
return await pantheonRequest(this.request, this.log, 'get', ['users', _.get(this.session, 'user_id')]);
}
/**
* Upload an SSH public key to the user's Pantheon account
*
* @param {string} key - Path to SSH public key file
* @return {Promise<Object>} Response from key upload
*/
async postKey(key) {
const postKey = ['users', _.get(this.session, 'user_id'), 'keys'];
const options = (this.mode === 'node') ? {headers: {'User-Agent': 'Terminus/Lando'}} : {};
const data = _.trim(fs.readFileSync(key, 'utf8'));
return await pantheonRequest(this.request, this.log, 'post', postKey, JSON.stringify(data), options);
}
};