From 15f17e9ac927721060b0e287f53bb1fc30da229f Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Mon, 12 May 2025 22:05:35 +0000 Subject: [PATCH 1/3] chore: refactor test code and update debian --- gce/test/app.test.js | 221 +++++++++++++++++++++++++++++++------------ 1 file changed, 158 insertions(+), 63 deletions(-) diff --git a/gce/test/app.test.js b/gce/test/app.test.js index e0e84b9aa..3608bed59 100644 --- a/gce/test/app.test.js +++ b/gce/test/app.test.js @@ -1,92 +1,187 @@ + const cp = require('child_process'); const path = require('path'); const fetch = require('node-fetch'); const {expect} = require('chai'); const {v4: uuidv4} = require('uuid'); -let testFlag = true; -let uniqueID; -let externalIP; + +// Configuration Constants +const GCP_ZONE = 'us-central1-f'; +const IMAGE_FAMILY = 'debian-12'; +const IMAGE_PROJECT = 'debian-cloud'; +const MACHINE_TYPE = 'g1-small'; +const APP_PORT = '8080'; +const STARTUP_SCRIPT_PATH = 'gce/startup-script.sh'; // Relative to project root +const MAX_PING_ATTEMPTS = 10; +const INITIAL_PING_DELAY_SECONDS = 2; async function pingVMExponential(address, count) { - await new Promise((r) => setTimeout(r, Math.pow(2, count) * 1000)); + if (attempt > MAX_PING_ATTEMPTS) { + throw new Error(`Failed to connect to ${address} after ${MAX_PING_ATTEMPTS} attempts.`); + } + const delaySeconds = Math.pow(INITIAL_PING_DELAY_SECONDS, attempt -1); // Start with 1s, then 2s, 4s, 8s etc. + console.log(`Ping attempt ${attempt}/${MAX_PING_ATTEMPTS}: Waiting ${delaySeconds}s before pinging ${address}...`); + await new Promise((r) => setTimeout(r, delaySeconds * 1000)); + try { - const res = await fetch(address); + const res = await fetch(address, { timeout: 15000 }); // Add a timeout to fetch itself if (res.status !== 200) { - throw new Error(res.status); + console.warn(`Ping attempt ${attempt} to ${address} failed with status: ${res.status}`); + throw new Error(`Status: ${res.status}`); } + console.log(`Successfully connected to ${address} on attempt ${attempt}.`); + return true; } catch (err) { process.stdout.write('.'); - await pingVMExponential(address, ++count); + if (attempt >= MAX_PING_ATTEMPTS) { + console.error(`\nFinal ping attempt to ${address} failed: ${err.message}`); + throw err; // Re-throw the error if max attempts reached + } + // Log the error for the current attempt but continue to retry + // console.warn(`Ping attempt ${attempt} to ${address} caught error: ${err.message}. Retrying...`); + return pingVMExponential(address, attempt + 1); } } -async function getIP(uniqueID) { - externalIP = cp - .execSync( - `gcloud compute instances describe my-app-instance-${uniqueID} \ ---format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=us-central1-f` - ) - .toString('utf8') - .trim(); - - await pingVMExponential(`http://${externalIP}:8080/`, 1); +async function getExternalIP(instanceName, zone) { + try { + // Retry a few times as IP address might take a moment to appear after instance is "RUNNING" + for (let i = 0; i < 5; i++) { + const ip = cp + .execSync( + `gcloud compute instances describe ${instanceName} --format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=${zone}` + ) + .toString('utf8') + .trim(); + if (ip) return ip; + console.log(`Attempt ${i+1} to get IP for ${instanceName}: IP not found yet. Waiting 5s...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + throw new Error(`Could not retrieve external IP for ${instanceName} after multiple attempts.`); + } catch (error) { + console.error(`Error getting external IP for ${instanceName}:`, error.message); + throw error; // Re-throw to fail the calling function (e.g., before hook) + } } -describe('spin up gce instance', async function () { - console.time('beforeHook'); - console.time('test'); - console.time('afterHook'); - this.timeout(250000); - uniqueID = uuidv4().split('-')[0]; +describe('spin up gce instance', function () { + // Increase timeout for the whole describe block if necessary, + // but individual hooks/tests have their own timeouts. + this.timeout(300000); // e.g., 5 minutes for the whole suite + + let uniqueID; + let instanceName; + let firewallRuleName; + // 'this.externalIP' will be used to store the IP in the Mocha context + before(async function () { - this.timeout(200000); - cp.execSync( - `gcloud compute instances create my-app-instance-${uniqueID} \ - --image-family=debian-10 \ - --image-project=debian-cloud \ - --machine-type=g1-small \ - --scopes userinfo-email,cloud-platform \ - --metadata app-location=us-central1-f \ - --metadata-from-file startup-script=gce/startup-script.sh \ - --zone us-central1-f \ - --tags http-server`, - {cwd: path.join(__dirname, '../../')} - ); - cp.execSync(`gcloud compute firewall-rules create default-allow-http-8080-${uniqueID} \ - --allow tcp:8080 \ - --source-ranges 0.0.0.0/0 \ - --target-tags http-server \ - --description "Allow port 8080 access to http-server"`); + this.timeout(240000); // Timeout for the before hook (e.g., 4 minutes) + console.time('beforeHookDuration'); - try { - const timeOutPromise = new Promise((resolve, reject) => { - setTimeout(() => reject('Timed out!'), 90000); - }); - await Promise.race([timeOutPromise, getIP(uniqueID)]); - } catch (err) { - testFlag = false; - } - console.timeEnd('beforeHook'); - }); + uniqueID = uuidv4().split('-')[0]; + instanceName = `my-app-instance-${uniqueID}`; + firewallRuleName = `default-allow-http-${APP_PORT}-${uniqueID}`; - after(function () { + console.log(`Creating GCE instance: ${instanceName}`); try { cp.execSync( - `gcloud compute instances delete my-app-instance-${uniqueID} --zone=us-central1-f --delete-disks=all` + `gcloud compute instances create ${instanceName} \ + --image-family=${IMAGE_FAMILY} \ + --image-project=${IMAGE_PROJECT} \ + --machine-type=${MACHINE_TYPE} \ + --scopes userinfo-email,cloud-platform \ + --metadata app-location=${GCP_ZONE} \ + --metadata-from-file startup-script=${STARTUP_SCRIPT_PATH} \ + --zone ${GCP_ZONE} \ + --tags http-server`, // Keep a generic tag if startup script handles specific app setup + { cwd: path.join(__dirname, '../../'), stdio: 'inherit' } // Show gcloud output ); + console.log(`Instance ${instanceName} created.`); + + console.log(`Creating firewall rule: ${firewallRuleName}`); + cp.execSync( + `gcloud compute firewall-rules create ${firewallRuleName} \ + --allow tcp:${APP_PORT} \ + --source-ranges 0.0.0.0/0 \ + --target-tags http-server \ + --description "Allow port ${APP_PORT} access for ${instanceName}"`, + { stdio: 'inherit' } + ); + console.log(`Firewall rule ${firewallRuleName} created.`); + + console.log('Attempting to get external IP...'); + this.externalIP = await getExternalIP(instanceName, GCP_ZONE); + console.log(`Instance IP: ${this.externalIP}`); + + const appAddress = `http://${this.externalIP}:${APP_PORT}/`; + console.log(`Pinging application at ${appAddress}...`); + await pingVMExponential(appAddress); // pingVMExponential will throw on failure + + console.log('Setup complete.'); } catch (err) { - console.log("wasn't able to delete the instance"); + console.error('Error in "before" hook:', err.message); + throw err; // Re-throw to make Mocha mark 'before' as failed + } finally { + console.timeEnd('beforeHookDuration'); } - console.timeEnd('afterHook'); }); - it('should get the instance', async () => { - if (testFlag) { - console.log(`http://${externalIP}:8080/`); - const response = await fetch(`http://${externalIP}:8080/`); - const body = await response.text(); - expect(body).to.include('Hello, world!'); + after(async function () { + // 'after' hooks run even if 'before' or tests fail. + this.timeout(120000); // Timeout for cleanup (e.g., 2 minutes) + console.time('afterHookDuration'); + console.log('Starting cleanup...'); + + await cleanupResources(instanceName, firewallRuleName, GCP_ZONE, this.externalIP); + + console.timeEnd('afterHookDuration'); + }); + + // Helper for cleanup to be used in 'after' and potentially in 'before' catch block + async function cleanupResources(instName, fwRuleName, zone, ip) { + if (instName) { + try { + console.log(`Deleting GCE instance: ${instName}`); + cp.execSync( + `gcloud compute instances delete ${instName} --zone=${zone} --delete-disks=all --quiet`, + { stdio: 'inherit' } + ); + console.log(`Instance ${instName} deleted.`); + } catch (err) { + console.warn(`Warning: Wasn't able to delete instance ${instName}. Error: ${err.message}`); + console.warn("You may need to delete it manually."); + } + } + + if (fwRuleName) { + try { + console.log(`Deleting firewall rule: ${fwRuleName}`); + cp.execSync(`gcloud compute firewall-rules delete ${fwRuleName} --quiet`, { stdio: 'inherit' }); + console.log(`Firewall rule ${fwRuleName} deleted.`); + } catch (err) { + console.warn(`Warning: Wasn't able to delete firewall rule ${fwRuleName}. Error: ${err.message}`); + console.warn("You may need to delete it manually."); + } } - console.timeEnd('test'); + // Optional: Release static IP if you were using one + // if (ip && IS_STATIC_IP) { /* gcloud compute addresses delete ... */ } + } + + it('should get the instance and verify content', async function() { + this.timeout(30000); // Timeout for this specific test + console.time('testExecutionTime'); + expect(this.externalIP, "External IP should be available").to.exist; + + const appUrl = `http://${this.externalIP}:${APP_PORT}/`; + console.log(`Testing application at: ${appUrl}`); + + const response = await fetch(appUrl); + expect(response.status, "Response status should be 200").to.equal(200); + + const body = await response.text(); + expect(body).to.include('Hello, world!'); + console.log('Test verification successful.'); + console.timeEnd('testExecutionTime'); }); -}); + +}); \ No newline at end of file From 8b36bed8c03f7b02756ed194bb50edb99143a5b1 Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Mon, 12 May 2025 22:15:00 +0000 Subject: [PATCH 2/3] fix: missed count -> attempt --- gce/test/app.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gce/test/app.test.js b/gce/test/app.test.js index 3608bed59..82f7263f5 100644 --- a/gce/test/app.test.js +++ b/gce/test/app.test.js @@ -15,11 +15,11 @@ const STARTUP_SCRIPT_PATH = 'gce/startup-script.sh'; // Relative to project root const MAX_PING_ATTEMPTS = 10; const INITIAL_PING_DELAY_SECONDS = 2; -async function pingVMExponential(address, count) { +async function pingVMExponential(address, attempt = 1 ) { if (attempt > MAX_PING_ATTEMPTS) { throw new Error(`Failed to connect to ${address} after ${MAX_PING_ATTEMPTS} attempts.`); } - const delaySeconds = Math.pow(INITIAL_PING_DELAY_SECONDS, attempt -1); // Start with 1s, then 2s, 4s, 8s etc. + const delaySeconds = Math.pow(INITIAL_PING_DELAY_SECONDS, attempt -1); console.log(`Ping attempt ${attempt}/${MAX_PING_ATTEMPTS}: Waiting ${delaySeconds}s before pinging ${address}...`); await new Promise((r) => setTimeout(r, delaySeconds * 1000)); From f59ad4043e4fba552f6ba7250223a12ef1261e1e Mon Sep 17 00:00:00 2001 From: Jennifer Davis Date: Wed, 14 May 2025 03:19:44 +0000 Subject: [PATCH 3/3] fix: clean up with prettier, remove ip (probably need some clean up there) --- gce/test/app.test.js | 92 ++++++++++++++++++++++++++++---------------- package.json | 3 +- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/gce/test/app.test.js b/gce/test/app.test.js index 82f7263f5..496bbfe04 100644 --- a/gce/test/app.test.js +++ b/gce/test/app.test.js @@ -1,4 +1,3 @@ - const cp = require('child_process'); const path = require('path'); const fetch = require('node-fetch'); @@ -15,27 +14,35 @@ const STARTUP_SCRIPT_PATH = 'gce/startup-script.sh'; // Relative to project root const MAX_PING_ATTEMPTS = 10; const INITIAL_PING_DELAY_SECONDS = 2; -async function pingVMExponential(address, attempt = 1 ) { +async function pingVMExponential(address, attempt = 1) { if (attempt > MAX_PING_ATTEMPTS) { - throw new Error(`Failed to connect to ${address} after ${MAX_PING_ATTEMPTS} attempts.`); + throw new Error( + `Failed to connect to ${address} after ${MAX_PING_ATTEMPTS} attempts.` + ); } - const delaySeconds = Math.pow(INITIAL_PING_DELAY_SECONDS, attempt -1); - console.log(`Ping attempt ${attempt}/${MAX_PING_ATTEMPTS}: Waiting ${delaySeconds}s before pinging ${address}...`); + const delaySeconds = Math.pow(INITIAL_PING_DELAY_SECONDS, attempt - 1); + console.log( + `Ping attempt ${attempt}/${MAX_PING_ATTEMPTS}: Waiting ${delaySeconds}s before pinging ${address}...` + ); await new Promise((r) => setTimeout(r, delaySeconds * 1000)); try { - const res = await fetch(address, { timeout: 15000 }); // Add a timeout to fetch itself + const res = await fetch(address, {timeout: 15000}); // Add a timeout to fetch itself if (res.status !== 200) { - console.warn(`Ping attempt ${attempt} to ${address} failed with status: ${res.status}`); + console.warn( + `Ping attempt ${attempt} to ${address} failed with status: ${res.status}` + ); throw new Error(`Status: ${res.status}`); } console.log(`Successfully connected to ${address} on attempt ${attempt}.`); - return true; + return true; } catch (err) { process.stdout.write('.'); if (attempt >= MAX_PING_ATTEMPTS) { - console.error(`\nFinal ping attempt to ${address} failed: ${err.message}`); - throw err; // Re-throw the error if max attempts reached + console.error( + `\nFinal ping attempt to ${address} failed: ${err.message}` + ); + throw err; // Re-throw the error if max attempts reached } // Log the error for the current attempt but continue to retry // console.warn(`Ping attempt ${attempt} to ${address} caught error: ${err.message}. Retrying...`); @@ -47,19 +54,28 @@ async function getExternalIP(instanceName, zone) { try { // Retry a few times as IP address might take a moment to appear after instance is "RUNNING" for (let i = 0; i < 5; i++) { - const ip = cp + const ip = cp .execSync( - `gcloud compute instances describe ${instanceName} --format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=${zone}` + `gcloud compute instances describe ${instanceName} --format='get(networkInterfaces[0].accessConfigs[0].natIP)' --zone=${zone}` ) .toString('utf8') .trim(); - if (ip) return ip; - console.log(`Attempt ${i+1} to get IP for ${instanceName}: IP not found yet. Waiting 5s...`); - await new Promise(resolve => setTimeout(resolve, 5000)); + if (ip) return ip; + console.log( + `Attempt ${ + i + 1 + } to get IP for ${instanceName}: IP not found yet. Waiting 5s...` + ); + await new Promise((resolve) => setTimeout(resolve, 5000)); } - throw new Error(`Could not retrieve external IP for ${instanceName} after multiple attempts.`); + throw new Error( + `Could not retrieve external IP for ${instanceName} after multiple attempts.` + ); } catch (error) { - console.error(`Error getting external IP for ${instanceName}:`, error.message); + console.error( + `Error getting external IP for ${instanceName}:`, + error.message + ); throw error; // Re-throw to fail the calling function (e.g., before hook) } } @@ -94,7 +110,7 @@ describe('spin up gce instance', function () { --metadata-from-file startup-script=${STARTUP_SCRIPT_PATH} \ --zone ${GCP_ZONE} \ --tags http-server`, // Keep a generic tag if startup script handles specific app setup - { cwd: path.join(__dirname, '../../'), stdio: 'inherit' } // Show gcloud output + {cwd: path.join(__dirname, '../../'), stdio: 'inherit'} // Show gcloud output ); console.log(`Instance ${instanceName} created.`); @@ -105,7 +121,7 @@ describe('spin up gce instance', function () { --source-ranges 0.0.0.0/0 \ --target-tags http-server \ --description "Allow port ${APP_PORT} access for ${instanceName}"`, - { stdio: 'inherit' } + {stdio: 'inherit'} ); console.log(`Firewall rule ${firewallRuleName} created.`); @@ -132,56 +148,66 @@ describe('spin up gce instance', function () { console.time('afterHookDuration'); console.log('Starting cleanup...'); - await cleanupResources(instanceName, firewallRuleName, GCP_ZONE, this.externalIP); + await cleanupResources( + instanceName, + firewallRuleName, + GCP_ZONE, + this.externalIP + ); console.timeEnd('afterHookDuration'); }); - // Helper for cleanup to be used in 'after' and potentially in 'before' catch block - async function cleanupResources(instName, fwRuleName, zone, ip) { + async function cleanupResources(instName, fwRuleName, zone) { if (instName) { try { console.log(`Deleting GCE instance: ${instName}`); cp.execSync( `gcloud compute instances delete ${instName} --zone=${zone} --delete-disks=all --quiet`, - { stdio: 'inherit' } + {stdio: 'inherit'} ); console.log(`Instance ${instName} deleted.`); } catch (err) { - console.warn(`Warning: Wasn't able to delete instance ${instName}. Error: ${err.message}`); - console.warn("You may need to delete it manually."); + console.warn( + `Warning: Wasn't able to delete instance ${instName}. Error: ${err.message}` + ); + console.warn('You may need to delete it manually.'); } } if (fwRuleName) { try { console.log(`Deleting firewall rule: ${fwRuleName}`); - cp.execSync(`gcloud compute firewall-rules delete ${fwRuleName} --quiet`, { stdio: 'inherit' }); + cp.execSync( + `gcloud compute firewall-rules delete ${fwRuleName} --quiet`, + {stdio: 'inherit'} + ); console.log(`Firewall rule ${fwRuleName} deleted.`); } catch (err) { - console.warn(`Warning: Wasn't able to delete firewall rule ${fwRuleName}. Error: ${err.message}`); - console.warn("You may need to delete it manually."); + console.warn( + `Warning: Wasn't able to delete firewall rule ${fwRuleName}. Error: ${err.message}` + ); + console.warn('You may need to delete it manually.'); } } // Optional: Release static IP if you were using one // if (ip && IS_STATIC_IP) { /* gcloud compute addresses delete ... */ } } - it('should get the instance and verify content', async function() { + it('should get the instance and verify content', async function () { this.timeout(30000); // Timeout for this specific test console.time('testExecutionTime'); - expect(this.externalIP, "External IP should be available").to.exist; + expect(this.externalIP, 'External IP should be available').to.exist; const appUrl = `http://${this.externalIP}:${APP_PORT}/`; console.log(`Testing application at: ${appUrl}`); const response = await fetch(appUrl); - expect(response.status, "Response status should be 200").to.equal(200); + expect(response.status, 'Response status should be 200').to.equal(200); const body = await response.text(); expect(body).to.include('Hello, world!'); console.log('Test verification successful.'); console.timeEnd('testExecutionTime'); }); - -}); \ No newline at end of file +}); diff --git a/package.json b/package.json index 304a2b332..e6109d4e9 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,9 @@ "name": "nodejs-getting-started", "private": true, "description": "End to end samples for running Node.js applications on Google Cloud Platform", - "dependencies": {}, "devDependencies": { "eslint": "^8.0.0", - "eslint-config-prettier": "^8.0.0", + "eslint-config-prettier": "^8.10.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", "prettier": "^2.0.0"