Skip to content

chore: refactor test code and update debian #641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 184 additions & 63 deletions gce/test/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,90 +3,211 @@ 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;

async function pingVMExponential(address, count) {
await new Promise((r) => setTimeout(r, Math.pow(2, count) * 1000));
// 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, 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);
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');
});

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'}
);
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');
});
});
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down