kibana/.buildkite/scripts/steps/cloud/purge_projects.ts
Luke Elmers b6287708f6
Adds AGPL 3.0 license (#192025)
Updates files outside of x-pack to be triple-licensed under Elastic
License 2.0, AGPL 3.0, or SSPL 1.0.
2024-09-06 19:02:41 -06:00

132 lines
4.1 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { execSync } from 'child_process';
import axios from 'axios';
import { getKibanaDir } from '#pipeline-utils';
async function getPrProjects() {
const match = /^(keep.?)?kibana-pr-([0-9]+)-(elasticsearch|security|observability)$/;
try {
return (
await Promise.all([
projectRequest.get('/api/v1/serverless/projects/elasticsearch'),
projectRequest.get('/api/v1/serverless/projects/security'),
projectRequest.get('/api/v1/serverless/projects/observability'),
])
)
.map((response) => response.data.items)
.flat()
.filter((project) => project.name.match(match))
.map((project) => {
const [, , prNumber, projectType] = project.name.match(match);
return {
id: project.id,
name: project.name,
prNumber,
type: projectType,
};
});
} catch (e) {
if (e.isAxiosError) {
const message = JSON.stringify(e.response.data) || 'unable to fetch projects';
throw new Error(message);
}
throw e;
}
}
async function deleteProject({
type,
id,
name,
}: {
type: 'elasticsearch' | 'observability' | 'security';
id: number;
name: string;
}) {
try {
await projectRequest.delete(`/api/v1/serverless/projects/${type}/${id}`);
execSync(`.buildkite/scripts/common/deployment_credentials.sh unset ${name}`, {
cwd: getKibanaDir(),
stdio: 'inherit',
});
} catch (e) {
if (e.isAxiosError) {
const message =
JSON.stringify(e.response.data) || `unable to delete ${type} project with id ${id}`;
throw new Error(message);
}
throw e;
}
}
async function purgeProjects() {
const prProjects = await getPrProjects();
const projectsToPurge: typeof prProjects = [];
for (const project of prProjects) {
const NOW = new Date().getTime() / 1000;
const DAY_IN_SECONDS = 60 * 60 * 24;
const prJson = execSync(
`gh pr view '${project.prNumber}' --json state,labels,updatedAt`
).toString();
const pullRequest = JSON.parse(prJson);
const prOpen = pullRequest.state === 'OPEN';
const lastCommitTimestamp = new Date(pullRequest.updatedAt).getTime() / 1000;
const persistDeployment = Boolean(
pullRequest.labels.filter((label: any) => label.name === 'ci:project-persist-deployment')
.length
);
if (prOpen && persistDeployment) {
continue;
}
if (!prOpen) {
console.log(
`Pull Request #${project.prNumber} is no longer open, will delete associated ${project.type} project`
);
projectsToPurge.push(project);
} else if (
!Boolean(
pullRequest.labels.filter((label: any) =>
/^ci:project-deploy-(elasticsearch|security|observability)$/.test(label.name)
).length
)
) {
console.log(
`Pull Request #${project.prNumber} no longer has a project deployment label, will delete associated deployment`
);
projectsToPurge.push(project);
} else if (lastCommitTimestamp < NOW - DAY_IN_SECONDS * 2) {
console.log(
`Pull Request #${project.prNumber} has not been updated in more than 2 days, will delete associated deployment`
);
projectsToPurge.push(project);
}
}
await Promise.all(projectsToPurge.map((p) => deleteProject(p)));
}
if (!process.env.PROJECT_API_DOMAIN || !process.env.PROJECT_API_KEY) {
console.error('missing project authentication');
process.exit(1);
}
const projectRequest = axios.create({
baseURL: process.env.PROJECT_API_DOMAIN,
headers: {
Authorization: `ApiKey ${process.env.PROJECT_API_KEY}`,
},
});
purgeProjects().catch((e) => {
console.error(e.toString());
process.exitCode = 1;
});