orchard/test/load/vm-lifecycle.js

151 lines
3.8 KiB
JavaScript

import {check} from 'k6';
import http from 'k6/http';
import {WebSocket} from 'k6/websockets';
import crypto from 'k6/crypto';
import encoding from 'k6/encoding';
import {uuidv4} from 'https://jslib.k6.io/k6-utils/1.6.0/index.js';
const BASE_URL = __ENV.BASE_URL || 'http://127.0.0.1:6120/v1';
const WS_BASE_URL = __ENV.WS_BASE_URL || BASE_URL.replace(/^http/, 'ws');
const WS_BYTES = Number(__ENV.WS_BYTES) || 64 * 1024;
const SERVICE_ACCOUNT_NAME = __ENV.SERVICE_ACCOUNT_NAME;
const SERVICE_ACCOUNT_TOKEN = __ENV.SERVICE_ACCOUNT_TOKEN;
export const options = {
scenarios: {
vmLifecycle: {
executor: 'ramping-vus',
stages: [
{duration: '5m', target: 2_000},
],
},
},
};
export default async function () {
const vmName = `k6-${uuidv4()}`;
createVM(vmName);
await portForward(vmName);
deleteVM(vmName);
}
function createVM(vmName) {
const loremIpsum = `Lorem ipsum
dolor sit amet,
consectetur adipiscing elit.
Fusce at orci nisi.
Donec lacinia neque et risus elementum,
ut interdum lacus pretium.
`;
const body = JSON.stringify({
name: vmName,
image: 'ghcr.io/cirruslabs/macos-tahoe-base:latest',
headless: true,
startup_script: {
script_content: loremIpsum
},
});
const resp = http.post(`${BASE_URL}/vms`, body, {
headers: {
'Content-Type': 'application/json',
...authHeaders(),
},
});
check(resp, {
'VM creation succeeded': (r) => r.status === 200,
});
}
async function portForward(vmName) {
const url = `${WS_BASE_URL}/vms/${vmName}/port-forward?port=22&wait=60`;
console.debug(`connecting to ${url}`);
const ws = new WebSocket(url, [], {
headers: {
...authHeaders(),
}
});
ws.binaryType = 'arraybuffer';
const sentBytes = crypto.randomBytes(WS_BYTES);
const sentHash = crypto.sha256(sentBytes, 'hex');
let numReceivedBytes = 0;
const receivedHasher = crypto.createHash('sha256');
const result = await new Promise((resolve, reject) => {
ws.onopen = () => {
ws.send(sentBytes);
};
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
numReceivedBytes += event.data.byteLength;
receivedHasher.update(event.data);
}
if (numReceivedBytes >= WS_BYTES) {
ws.close();
}
};
ws.onerror = (evt) => {
resolve({
error: evt.error,
});
};
ws.onclose = (evt) => {
resolve({
code: evt.code,
});
};
});
check(result, {
'WebSocket closed normally (1000)': (r) => r.code === 1000,
'WebSocket had no errors': (r) => !r.error,
});
check(numReceivedBytes, {
'WebSocket received exactly WS_BYTES back': (r) => WS_BYTES === r,
});
const receivedHash = receivedHasher.digest('hex');
check(receivedHash, {
'WebSocket received exactly the same data back': (r) => sentHash === receivedHash,
});
}
function deleteVM(vmName) {
const resp = http.del(`${BASE_URL}/vms/${vmName}`, null, {
headers: {
'Content-Type': 'application/json',
...authHeaders(),
}
});
check(resp, {
'VM deletion succeeded': (r) => r.status === 200,
});
}
function authHeaders() {
if (!SERVICE_ACCOUNT_NAME || !SERVICE_ACCOUNT_TOKEN) {
return {}
}
const credentials = `${SERVICE_ACCOUNT_NAME}:${SERVICE_ACCOUNT_TOKEN}`;
const encodedCredentials = encoding.b64encode(credentials);
return {
Authorization: `Basic ${encodedCredentials}`
}
}