api/index.js (302 lines of code) (raw):
/* eslint-disable require-jsdoc */
// Copyright (c) 2022 EPAM Systems, Inc.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import {Storage} from '@google-cloud/storage';
import nodeGzip from 'node-gzip';
import {load} from 'js-yaml';
import fetch from 'node-fetch';
const {ungzip} = nodeGzip;
const storage = new Storage();
export async function reporting(file, context) {
if (context.eventType !== 'google.storage.object.finalize') {
return;
}
if (!file.name.includes('hub.state')) {
return;
}
const superhubs = await superhubBuckets();
const stack = await stackByID(file.name, superhubs, false);
const {
REPORTING_URL: reportingEndpoint = 'https://us-central1-superhub.cloudfunctions.net/event',
} = process.env;
const payload = {
stackId: stack.id,
name: stack.name,
status: stack.status,
initiator: stack.latestOperation ? stack.latestOperation.initiator : 'unknown',
project: stack.projectId,
gcpUserAccount: stack.userAccount,
};
try {
await fetch(reportingEndpoint, {
method: 'POST',
body: JSON.stringify(payload),
});
console.log(`Payload ${JSON.stringify(payload)} posted to ${reportingEndpoint}`);
} catch (error) {
console.error(error);
}
}
export async function stacks(req, res) {
const superhubs = await superhubBuckets();
const id = stackID(req.path);
switch (req.method) {
case 'GET':
if (id) {
let stack;
if (req.query.raw || req.query.raw === '') {
stack = await stackByID(id, superhubs, true);
} else {
stack = await stackByID(id, superhubs, false);
}
if (stack) {
res
.status(200)
.set('content-type', 'application/json')
.send(JSON.stringify(stack));
} else {
res
.status(404)
.send('Sorry, cant find that');
}
} else {
const stacks = await allStacks(superhubs);
res
.status(200)
.set('content-type', 'application/json')
.send(JSON.stringify(filter(stacks, req.query)));
}
break;
case 'DELETE':
stack = await stackByID(id, superhubs, false);
if (stack) {
await deleteByID(id, superhubs);
res
.status(202)
.send('');
} else {
res
.status(404)
.send('');
}
break;
default:
res
.status(400)
.send('Sorry, not supported');
}
}
function strEqualsIgnoreCase(s1, s2) {
return s1.toLowerCase() === s2.toLowerCase();
}
function filter(stacks, query) {
const {
name,
status,
latestOperation: {
name: lastOpName,
initiator,
timestamp: {
after: timestampAfter,
before: timestampBefore,
} = {},
} = {},
} = query;
let filtered = stacks;
if (name) {
filtered = stacks.filter((stack) => stack.name.toLowerCase().includes(name.toLowerCase()));
}
if (status) {
filtered = filtered.filter((stack) => strEqualsIgnoreCase(stack.status, status));
}
if (lastOpName) {
filtered = filtered.filter(({latestOperation: {name}}) => strEqualsIgnoreCase(name, lastOpName));
}
if (initiator) {
filtered = filtered.filter(({latestOperation: {initiator: stackInitiator}}) => {
stackInitiator = stackInitiator.toLowerCase;
initiator = initiator.toLowerCase();
return stackInitiator.includes(initiator);
});
}
if (timestampBefore) {
const dtBefore = Date.parse(timestampBefore);
if (dtBefore) {
filtered = filtered.filter((stack) => new Date(stack.latestOperation.timestamp) < dtBefore);
}
}
if (timestampAfter) {
const dtAfter = Date.parse(timestampAfter);
if (dtAfter) {
filtered = filtered.filter((stack) => new Date(stack.latestOperation.timestamp) >= dtAfter);
}
}
return filtered;
}
async function deleteByID(id, buckets) {
const stateFileMetas = [];
for (const bucket of buckets) {
const [files] = await bucket.getFiles();
stateFileMetas.push(...files.filter((bucketFile) => bucketFile.name.includes(id)));
}
const promises = [];
for (const stateFileMeta of stateFileMetas) {
promises.push(stateFileMeta.delete());
}
await Promise.all(promises);
}
async function stackByID(id, buckets, raw) {
let stateFileMeta;
for (const bucket of buckets) {
const [files] = await bucket.getFiles();
const found = files
.filter((bucketFile) => bucketFile.name.includes(id) && bucketFile.name.includes('hub.state'));
if (found.length > 0) {
stateFileMeta = found[0];
break;
}
}
if (!stateFileMeta) {
return null;
}
const states = await allStates([stateFileMeta]);
if (raw) {
return load(states[0].toString());
}
return stackMeta(load(states[0].toString()), false);
}
async function allStacks(buckets) {
const stateFileMetas = [];
for (const bucket of buckets) {
const [files] = await bucket.getFiles();
stateFileMetas.push(...files.filter((bucketFile) => bucketFile.name.includes('hub.state')));
}
console.log('Number of state files: ' + stateFileMetas.length);
const start = new Date().getTime();
const states = await allStates(stateFileMetas);
const end = new Date().getTime();
const time = (end - start) / 1000;
console.log('Time spent to unzip all state files: ' + time + ' seconds');
console.log('Average: ' + time / states.length + ' seconds per file');
stacks = [];
states.forEach((state) => {
stacks.push(stackMeta(load(state), true));
});
stacks.sort((x, y) => new Date(y.latestOperation.timestamp) - new Date(x.latestOperation.timestamp));
return stacks;
}
function stackID(path) {
if (!path || path === '/') {
return '';
} else if (path.startsWith('/')) {
return path.substring(1);
}
return path;
}
function stackMeta(yaml, light) {
const last = yaml.operations[yaml.operations.length - 1];
let stateLocation = 'unknown';
if (last.options.args) {
const stateArg = last.options.args.find((option) => option.includes('hub.state'));
const locations = stateArg.split(',');
if (locations.length === 2) {
stateLocation = locations[1];
}
}
let dnsDomainParam = {
value: 'unset',
};
let projectId = {
value: 'unset',
};
let userAccount = {
value: last.initiator || 'unset',
};
let sandboxDir = {
value: 'unset',
};
let sandboxCommit = {
value: 'unset',
};
const {stackParameters} = yaml;
if (stackParameters) {
dnsDomainParam = stackParameters.find(({name}) => name === 'dns.domain') ||
stackParameters.find(({name}) => name === 'dns.name') ||
dnsDomainParam;
if (dnsDomainParam.value === 'unset' && stateLocation !== 'unknown') {
const parts = stateLocation.split('/');
const index = parts.indexOf('hub');
if (index > 0) {
dnsDomainParam.value = parts[index - 1];
} else {
dnsDomainParam.value = stateLocation;
}
}
projectId = stackParameters.find(({name}) => name === 'projectId') || projectId;
userAccount = stackParameters.find(({name}) => name === 'hub.userAccount') || userAccount;
sandboxDir = stackParameters.find(({name}) => name === 'hub.sandboxDir') || sandboxDir;
sandboxCommit = stackParameters.find(({name}) => name === 'hub.sandboxCommit') || sandboxCommit;
}
const meta = {
id: dnsDomainParam.value,
projectId: projectId.value,
userAccount: userAccount.value,
name: yaml.meta.name,
stateLocation: {
uri: stateLocation,
kind: 'gcs',
},
sandbox: {
dir: sandboxDir.value,
commit: sandboxCommit.value,
},
status: yaml.status,
components: light ? undefined : Object.keys(yaml.components).map((key) => {
return {
name: key,
status: yaml.components[key].status,
timestamp: yaml.components[key].timestamp,
};
},
),
latestOperation: {
name: last.operation,
timestamp: last.timestamp,
status: last.status,
initiator: userAccount.value, // by default value is last.initiator
phases: last.phases,
},
};
return meta;
}
async function allStates(stateFileMetas) {
const promises = [];
for (const stateFileMeta of stateFileMetas) {
promises.push(stateFile(stateFileMeta).then((archive) => ungzip(archive)));
}
return Promise.all(promises);
}
function stateFile(file) {
return new Promise((resolve, reject) => {
const data = [];
file.createReadStream()
.on('data', (d) => {
data.push(d);
})
.on('end', () => {
resolve(Buffer.concat(data));
})
.on('error', (e) => reject(e));
});
}
async function superhubBuckets() {
const [buckets] = await storage.getBuckets();
if (!buckets || buckets.length === 0) {
res.send('Project does not have any buckets!');
}
return buckets.filter((bucket) => {
const labels = bucket.metadata.labels;
if (labels) {
const manager = labels['managed-by'];
return manager && manager === 'superhub';
}
return false;
});
}