From a41566e69e56325c2319264fe85fe54dae3f2e64 Mon Sep 17 00:00:00 2001 From: Todd Hainsworth Date: Wed, 9 Apr 2025 13:00:02 +0930 Subject: [PATCH 1/2] JAN-1120: Setup SSH configuration This allows projects with NPM dependencies that are bought in via Git SSH to be installable. --- bin/index.ts | 4 +++ lib/ssh.ts | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 lib/ssh.ts diff --git a/bin/index.ts b/bin/index.ts index 8f024c7..9173c6b 100644 --- a/bin/index.ts +++ b/bin/index.ts @@ -10,6 +10,7 @@ import { getInstallCommand, } from '../lib/packageManagers'; import { isNxServerlessMonorepo } from '../lib/serverlessProjectType'; +import { setupSshCredentials } from '../lib/ssh'; import { uploadDeploymentBadge } from '../lib/uploadDeploymentBadge'; async function main() { @@ -34,6 +35,9 @@ async function main() { ); } + // Setup SSH credentials for the pipeline, these are generated by Bitbucket Pipelines + await setupSshCredentials(); + const packageManager = detectPackageManager(bitbucketCloneDir); const isMonorepo = await isNxServerlessMonorepo(bitbucketCloneDir); diff --git a/lib/ssh.ts b/lib/ssh.ts new file mode 100644 index 0000000..7264da9 --- /dev/null +++ b/lib/ssh.ts @@ -0,0 +1,90 @@ +import fs from 'fs'; +import os from 'os'; + +/** + * Sets up the SSH credentials for the pipeline. + * This function copies the SSH identity file and known hosts file from the Bitbucket Pipelines agent to the local .ssh directory. + * It also updates the SSH configuration file to include the identity file. + * + * @throws {Error} If the SSH identity file cannot be copied or the SSH configuration file cannot be updated. + */ +export async function setupSshCredentials(): Promise { + const homeDir = os.homedir(); + const sshDir = `${homeDir}/.ssh/`; + const sshConfigDir = `/opt/atlassian/pipelines/agent/ssh`; + const identityFile = `${sshConfigDir}/id_rsa_tmp`; + const knownHostsFile = `${sshConfigDir}/known_hosts`; + + // Ensure the SSH directory exists + const sshDirExists = await fs.promises + .stat(sshDir) + .then((stat) => stat.isDirectory()); + if (!sshDirExists) { + await fs.promises.mkdir(sshDir, { recursive: true }); + } + + // Copy over the SSH identity file that Bitbucket has generated, if this fails then we should fail the whole pipeline + try { + console.log('Attempting to copy SSH identity file...'); + + const pipelinesIdFile = `${sshDir}/pipelines_id`; + await fs.promises.copyFile(identityFile, pipelinesIdFile); + + console.log(`Copied to ${pipelinesIdFile}`); + console.log(`Adding identity file config to config file`); + + const configFile = `${sshDir}/config`; + await fs.promises.appendFile( + configFile, + `IdentityFile ${pipelinesIdFile}` + ); + } catch (e) { + console.error( + 'Failed to update SSH configuration, check that SSH key configuration in Pipelines is valid. \n Check Pipelines -> SSH Keys.' + ); + throw e; + } + + // Copy over the known_hosts file that Bitbucket generated + try { + console.log('Piping known hosts into runtime ssh config'); + const knownHosts = await fs.promises + .readFile(knownHostsFile) + .then((buf) => buf.toString()); + const hostsFile = `${sshDir}/known_hosts`; + await fs.promises.appendFile(hostsFile, knownHosts); + } catch (e) { + console.error( + 'Failed to update hosts file. \n Check Pipelines configuration for known hosts.' + ); + throw e; + } + + console.log('Updating SSH directory permissions'); + + await chmodRecursive(sshDir, 0o700); +} + +/** + * Recursively changes the permissions of a directory and its contents. + * + * @param {fs.PathLike} path - The path to the directory to change permissions for. + * @param {fs.Mode} mode - The mode to set for the directory and its contents. + * @throws {Error} If the directory or its contents cannot be changed. + */ +async function chmodRecursive(path: fs.PathLike, mode: fs.Mode): Promise { + await fs.promises.chmod(path, mode); + + const entries = await fs.promises.readdir(path, { + withFileTypes: true, + }); + + for (const entry of entries) { + const fullPath = `${path}/${entry.name}`; + if (entry.isDirectory()) { + await chmodRecursive(fullPath, mode); + } else { + await fs.promises.chmod(fullPath, mode); + } + } +} From 5808dd6ee02794debd33cd07ea8a30239a3b65fc Mon Sep 17 00:00:00 2001 From: Todd Hainsworth Date: Thu, 10 Apr 2025 06:59:42 +0930 Subject: [PATCH 2/2] JAN-1120: Don't fail the pipeline if we cannot setup SSH --- lib/ssh.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/ssh.ts b/lib/ssh.ts index 7264da9..1a2f79a 100644 --- a/lib/ssh.ts +++ b/lib/ssh.ts @@ -6,7 +6,7 @@ import os from 'os'; * This function copies the SSH identity file and known hosts file from the Bitbucket Pipelines agent to the local .ssh directory. * It also updates the SSH configuration file to include the identity file. * - * @throws {Error} If the SSH identity file cannot be copied or the SSH configuration file cannot be updated. + * @returns {Promise} Resolves when the SSH credentials are setup or we fail to update the SSH configuration file (which doesn't fail the pipeline) */ export async function setupSshCredentials(): Promise { const homeDir = os.homedir(); @@ -42,7 +42,7 @@ export async function setupSshCredentials(): Promise { console.error( 'Failed to update SSH configuration, check that SSH key configuration in Pipelines is valid. \n Check Pipelines -> SSH Keys.' ); - throw e; + return; } // Copy over the known_hosts file that Bitbucket generated @@ -57,7 +57,7 @@ export async function setupSshCredentials(): Promise { console.error( 'Failed to update hosts file. \n Check Pipelines configuration for known hosts.' ); - throw e; + return; } console.log('Updating SSH directory permissions');