-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlagoon-api.mjs
More file actions
339 lines (298 loc) · 13 KB
/
lagoon-api.mjs
File metadata and controls
339 lines (298 loc) · 13 KB
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
import { exec } from 'child_process';
import { promisify } from 'util';
import chalk from 'chalk';
import { logAction, logError } from './logger.mjs';
const execAsync = promisify(exec);
// Helper function to execute and log Lagoon commands
export async function execLagoonCommand(command, action = 'Unknown Action') {
console.log(chalk.blue(`Executing: ${chalk.bold(command)}`));
try {
const result = await execAsync(command);
logAction(action, command, 'Success');
return result;
} catch (error) {
logError(action, command, error);
throw error;
}
}
// Get all Lagoon instances from the config
export async function getLagoonInstances() {
try {
const command = 'lagoon config list --output-json';
const { stdout } = await execLagoonCommand(command, 'List Lagoon Instances');
// Parse the JSON output
const configData = JSON.parse(stdout);
// Extract instance names from the JSON data and clean them
const instances = configData.data.map(instance => {
// Extract just the base name without (default)(current) or other annotations
const fullName = instance.name;
// Extract just the base name before any parentheses or whitespace
const baseName = fullName.split(/\s+|\(/).shift().trim();
return baseName;
});
return instances;
} catch (error) {
throw new Error(`Failed to get Lagoon instances: ${error.message}`);
}
}
// Get all projects for a Lagoon instance with full details
export async function getProjectsWithDetails(instance) {
try {
const command = `lagoon -l ${instance} list projects --output-json`;
const { stdout } = await execLagoonCommand(command, `List Projects for ${instance}`);
// Parse the JSON output
const projectsData = JSON.parse(stdout);
// Return the full project data
return projectsData.data;
} catch (error) {
throw new Error(`Failed to get projects for instance ${instance}: ${error.message}`);
}
}
// Get all projects for a Lagoon instance
export async function getProjects(instance) {
try {
const command = `lagoon -l ${instance} list projects --output-json`;
const { stdout } = await execLagoonCommand(command, `List Projects for ${instance}`);
// Parse the JSON output
const projectsData = JSON.parse(stdout);
// Extract project names from the JSON data
const projects = projectsData.data.map(project => project.projectname);
return projects;
} catch (error) {
throw new Error(`Failed to get projects for instance ${instance}: ${error.message}`);
}
}
// Get all environments for a project
export async function getEnvironments(instance, project) {
try {
const command = `lagoon -l ${instance} -p ${project} list environments --output-json`;
const { stdout } = await execLagoonCommand(command, `List Environments for ${project}`);
// Parse the JSON output
const environmentsData = JSON.parse(stdout);
// Extract environment names from the JSON data
const environments = environmentsData.data.map(env => env.name);
return environments;
} catch (error) {
throw new Error(`Failed to get environments for project ${project}: ${error.message}`);
}
}
/**
* Retrieves the list of usernames associated with a Lagoon project.
*
* Executes the Lagoon CLI to list all users for the specified project and parses the output to extract usernames.
*
* @param {string} instance - The Lagoon instance name.
* @param {string} project - The project name.
* @returns {Promise<string[]>} An array of usernames for the project.
*
* @throws {Error} If the command fails or output cannot be parsed.
*/
export async function getUsers(instance, project) {
try {
const command = `lagoon -l ${instance} -p ${project} list all-users`;
const { stdout } = await execLagoonCommand(command, `List Users for ${project}`);
const lines = stdout.split('\n').filter(line => line.trim() !== '');
// Skip the header line and extract user names
const users = lines.slice(1).map(line => {
const parts = line.split('|').map(part => part.trim());
return parts[0]; // First column is the user name
});
return users;
} catch (error) {
throw new Error(`Failed to get users for project ${project}: ${error.message}`);
}
}
/**
* Deletes a Lagoon environment unless it is protected.
*
* Prevents deletion of protected environments such as 'production', 'master', 'develop', or any environment whose name starts with 'project/'. Executes the Lagoon CLI to delete the specified environment and returns true if the operation succeeds.
*
* @param {string} instance - Name of the Lagoon instance.
* @param {string} project - Name of the project.
* @param {string} environment - Name of the environment to delete.
* @returns {boolean} True if the environment was deleted successfully.
*
* @throws {Error} If the environment is protected or if the deletion fails.
*/
export async function deleteEnvironment(instance, project, environment) {
// Check if environment is protected
if (
environment === 'production' ||
environment === 'master' ||
environment === 'develop' ||
environment.startsWith('project/')
) {
throw new Error(`Cannot delete protected environment: ${environment}`);
}
try {
const command = `lagoon -l ${instance} -p ${project} delete environment --environment ${environment} --force --output-json`;
// response if successful is a JSON {"result":"success"}
const { stdout } = await execLagoonCommand(command, `Delete Environment ${environment} from ${project}`);
const response = JSON.parse(stdout);
if (response.result === 'success') {
console.log(chalk.green(`Environment ${environment} deleted successfully`));
return true;
} else {
throw new Error(`Failed to delete environment ${environment}: ${response}`);
}
} catch (error) {
throw new Error(`Failed to delete environment ${environment}: ${error.message}`);
}
}
/**
* Generates a one-time login link for a specified Lagoon environment using Drush.
*
* Throws an error if the environment is protected (e.g., 'production' or 'master').
*
* @param {string} instance - The Lagoon instance name.
* @param {string} project - The Lagoon project name.
* @param {string} environment - The environment for which to generate the login link.
* @returns {string} The generated one-time login URL.
*
* @throws {Error} If the environment is protected or if the login link generation fails.
*/
export async function generateLoginLink(instance, project, environment) {
// Check if environment is protected
if (environment === 'production' || environment === 'master') {
throw new Error(`Cannot generate login link for protected environment: ${environment}`);
}
try {
const command = `lagoon ssh -l ${instance} -p ${project} -e ${environment} -C "drush user:unblock --uid=1 && drush uli"`;
const { stdout } = await execLagoonCommand(command, `Generate Login Link for ${environment} in ${project}`);
return stdout.trim();
} catch (error) {
throw new Error(`Failed to generate login link for environment ${environment}: ${error.message}`);
}
}
/**
* Converts a GitHub SSH or HTTPS repository URL to a standard HTTPS URL without the `.git` suffix.
*
* Returns `null` if the input is not a GitHub URL.
*
* @param {string} gitUrl - The Git repository URL to convert.
* @returns {string|null} The normalized GitHub HTTPS URL, or `null` if the input is not a GitHub URL.
*/
export function gitUrlToGithubUrl(gitUrl) {
// Handle SSH URLs like git@github.com:org/repo.git
if (gitUrl.startsWith('git@github.com:')) {
const path = gitUrl.replace('git@github.com:', '').replace('.git', '');
return `https://github.com/${path}`;
} else if (gitUrl.includes('github.com')) {
return gitUrl.replace('.git', '');
}
// Return null if not a GitHub URL
return null;
}
// Helper function to extract PR number from environment name
export function extractPrNumber(environmentName) {
const match = environmentName.match(/^pr-(\d+)$/i);
return match ? match[1] : null;
}
/**
* Clears the Drupal cache for a specified environment using Lagoon CLI and Drush.
*
* @param {string} instance - The Lagoon instance name.
* @param {string} project - The project name within the Lagoon instance.
* @param {string} environment - The environment name to clear the cache for.
* @returns {string} The trimmed output from the Drush cache clear command.
*
* @throws {Error} If the cache clearing operation fails for the specified environment.
*/
export async function clearDrupalCache(instance, project, environment) {
try {
const command = `lagoon ssh -l ${instance} -p ${project} -e ${environment} -C "drush cr"`;
const { stdout } = await execLagoonCommand(command, `Clear Cache for ${environment} in ${project}`);
return stdout.trim();
} catch (error) {
throw new Error(`Failed to clear cache for environment ${environment}: ${error.message}`);
}
}
// No helper functions needed for the more efficient git ls-remote approach
/**
* Retrieves all branch names from a remote Git repository.
*
* Executes `git ls-remote --heads` on the provided repository URL and parses the output to return an array of branch names.
*
* @param {string} gitUrl - The URL of the Git repository.
* @returns {Promise<string[]>} An array of branch names found in the repository.
*
* @throws {Error} If the Git URL is missing, invalid, authentication fails, the repository is not found, or another error occurs during command execution.
*/
export async function getGitBranches(gitUrl) {
try {
if (!gitUrl) {
throw new Error('Git URL not provided or invalid');
}
// Use git ls-remote to list all references (only the heads/branches)
const command = `git ls-remote --heads ${gitUrl}`;
console.log(chalk.blue(`Executing: ${chalk.bold(command)}`));
try {
const { stdout, stderr } = await execAsync(command);
// Log any warnings from stderr
if (stderr) {
console.log(chalk.yellow(`Warning: ${stderr}`));
}
// Parse the branch names from the output
const branches = stdout
.split('\n')
.filter(line => line.trim().length > 0)
.map(line => {
// Extract the branch name from lines like:
// d7b0a24b046d00b6aeac1280e4d1a74297551444 refs/heads/main
const match = line.match(/refs\/heads\/(.+)$/);
return match ? match[1] : null;
})
.filter(branch => branch !== null);
// Log the number of branches found
console.log(chalk.green(`Found ${branches.length} branches in repository`));
return branches;
} catch (error) {
// If the Git URL is using SSH, offer a hint about authentication
if (gitUrl.startsWith('git@') && error.message.includes('Permission denied')) {
throw new Error(`Authentication failed for ${gitUrl}. Please check your SSH key configuration.`);
} else if (error.message.includes('not found')) {
throw new Error(`Repository not found: ${gitUrl}. Please check if the URL is correct.`);
} else {
throw error;
}
}
} catch (error) {
logError('List Branches', `git ls-remote ${gitUrl}`, error);
throw new Error(`Failed to get git branches: ${error.message}`);
}
}
/**
* Deploys a specified branch to a Lagoon project environment.
*
* Validates the branch name for allowed characters and escapes special shell characters to prevent command injection. Executes the Lagoon CLI to initiate deployment and parses the JSON response.
*
* @param {string} branch - The name of the branch to deploy. Must contain only alphanumeric characters, slashes, underscores, hyphens, and periods.
* @returns {{ success: true, message: string }} An object indicating successful initiation of the deployment and a descriptive message.
*
* @throws {Error} If the branch name is invalid, the Lagoon CLI command fails, or the deployment is unsuccessful.
*/
export async function deployBranch(instance, project, branch) {
try {
// Validate branch name to prevent command injection - allow slashes in addition to other safe characters
if (!/^[a-zA-Z0-9_./\-]+$/.test(branch)) {
throw new Error('Invalid branch name. Branch names must contain only alphanumeric characters, slashes, underscores, hyphens, and periods.');
}
// Properly escape the branch name for use in command line
// More comprehensive escaping for shell safety
const escapedBranch = branch.replace(/["\\$`]/g, '\\$&');
const command = `lagoon -l ${instance} -p ${project} deploy branch --branch "${escapedBranch}" --output-json`;
const { stdout } = await execLagoonCommand(command, `Deploy Branch ${branch} to ${project}`);
// Parse the JSON response
const response = JSON.parse(stdout);
if (response.result === 'success') {
return {
success: true,
message: `Branch ${branch} is being deployed to ${project}`
};
} else {
throw new Error(`Failed to deploy branch ${branch}: ${JSON.stringify(response)}`);
}
} catch (error) {
throw new Error(`Failed to deploy branch ${branch}: ${error.message}`);
}
}