Skip to content

Commit 3db6c18

Browse files
committed
Merge branch 'feature/CLDSRV-867/clean-gcp-bucket' into tmp/octopus/w/9.4/feature/CLDSRV-867/clean-gcp-bucket
2 parents 9770cac + 24831d6 commit 3db6c18

20 files changed

Lines changed: 218 additions & 37 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* eslint-disable no-console */
2+
'use strict';
3+
4+
const {
5+
S3Client,
6+
ListBucketsCommand,
7+
ListObjectsV2Command,
8+
DeleteObjectCommand,
9+
DeleteBucketCommand,
10+
ListMultipartUploadsCommand,
11+
AbortMultipartUploadCommand,
12+
} = require('@aws-sdk/client-s3');
13+
14+
const GCP_ENDPOINT = 'https://storage.googleapis.com';
15+
const BUCKET_PREFIX = 'cldsrvci-';
16+
const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
17+
18+
function buildClient() {
19+
const accessKeyId = process.env.AWS_GCP_BACKEND_ACCESS_KEY;
20+
const secretAccessKey = process.env.AWS_GCP_BACKEND_SECRET_KEY;
21+
22+
if (!accessKeyId || !secretAccessKey) {
23+
console.error(
24+
'Missing required environment variables: ' +
25+
'AWS_GCP_BACKEND_ACCESS_KEY and AWS_GCP_BACKEND_SECRET_KEY'
26+
);
27+
process.exit(1);
28+
}
29+
30+
const client = new S3Client({
31+
endpoint: GCP_ENDPOINT,
32+
region: 'us-east-1',
33+
credentials: { accessKeyId, secretAccessKey },
34+
forcePathStyle: true,
35+
disableS3ExpressSessionAuth: true,
36+
requestChecksumCalculation: 'WHEN_REQUIRED',
37+
responseChecksumValidation: 'WHEN_REQUIRED',
38+
});
39+
40+
// GCP's S3-compatible API rejects the x-id query parameter that newer
41+
// versions of @aws-sdk/client-s3 append to every request URL.
42+
client.middlewareStack.add(
43+
next => async args => {
44+
// eslint-disable-next-line no-param-reassign
45+
delete args.request.query['x-id'];
46+
return next(args);
47+
},
48+
{ step: 'build', name: 'removeXIdParam', priority: 'low' },
49+
);
50+
51+
return client;
52+
}
53+
54+
async function abortMultipartUploads(client, bucketName) {
55+
let uploadIdMarker;
56+
let keyMarker;
57+
58+
do {
59+
const res = await client.send(new ListMultipartUploadsCommand({
60+
Bucket: bucketName,
61+
UploadIdMarker: uploadIdMarker,
62+
KeyMarker: keyMarker,
63+
}));
64+
65+
for (const upload of res.Uploads || []) {
66+
console.log(
67+
` Aborting MPU: ${upload.Key} (${upload.UploadId})`
68+
);
69+
await client.send(new AbortMultipartUploadCommand({
70+
Bucket: bucketName,
71+
Key: upload.Key,
72+
UploadId: upload.UploadId,
73+
}));
74+
}
75+
76+
uploadIdMarker = res.NextUploadIdMarker;
77+
keyMarker = res.NextKeyMarker;
78+
} while (uploadIdMarker);
79+
}
80+
81+
async function deleteAllObjects(client, bucketName) {
82+
let continuationToken;
83+
84+
do {
85+
const res = await client.send(new ListObjectsV2Command({
86+
Bucket: bucketName,
87+
ContinuationToken: continuationToken,
88+
}));
89+
90+
const objects = res.Contents || [];
91+
if (objects.length > 0) {
92+
console.log(` Deleting ${objects.length} object(s)...`);
93+
for (const obj of objects) {
94+
await client.send(new DeleteObjectCommand({
95+
Bucket: bucketName,
96+
Key: obj.Key,
97+
}));
98+
}
99+
}
100+
101+
continuationToken = res.NextContinuationToken;
102+
} while (continuationToken);
103+
}
104+
105+
async function cleanupBucket(client, bucketName) {
106+
console.log(`Cleaning up bucket: ${bucketName}`);
107+
try {
108+
await abortMultipartUploads(client, bucketName);
109+
await deleteAllObjects(client, bucketName);
110+
await client.send(new DeleteBucketCommand({ Bucket: bucketName }));
111+
console.log(`Deleted bucket: ${bucketName}`);
112+
} catch (err) {
113+
console.error(`Failed to delete bucket ${bucketName}: ${err.message}`);
114+
}
115+
}
116+
117+
async function main() {
118+
const client = buildClient();
119+
120+
console.log('Listing GCP buckets...');
121+
const { Buckets = [] } = await client.send(new ListBucketsCommand({}));
122+
123+
const now = Date.now();
124+
const stale = Buckets.filter(b =>
125+
b.Name.startsWith(BUCKET_PREFIX) &&
126+
now - new Date(b.CreationDate).getTime() > ONE_WEEK_MS
127+
);
128+
129+
if (stale.length === 0) {
130+
console.log('No stale GCP CI buckets found.');
131+
return;
132+
}
133+
134+
console.log(
135+
`Found ${stale.length} stale GCP CI bucket(s) to clean up: ${
136+
stale.map(b => b.Name).join(', ')}`
137+
);
138+
139+
for (const bucket of stale) {
140+
await cleanupBucket(client, bucket.Name);
141+
}
142+
143+
console.log('GCP CI bucket cleanup complete.');
144+
}
145+
146+
main().catch(err => {
147+
console.error('GCP cleanup script failed:', err);
148+
process.exit(1);
149+
});

.github/workflows/tests.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,3 +1026,26 @@ jobs:
10261026
password: ${{ secrets.ARTIFACTS_PASSWORD }}
10271027
source: /tmp/artifacts
10281028
if: always()
1029+
1030+
cleanup-gcp-buckets:
1031+
runs-on: ubuntu-24.04
1032+
needs:
1033+
- multiple-backend
1034+
- mongo-v0-ft-tests
1035+
- mongo-v1-ft-tests
1036+
- file-ft-tests
1037+
if: always()
1038+
steps:
1039+
- name: Checkout
1040+
uses: actions/checkout@v4
1041+
- uses: actions/setup-node@v4
1042+
with:
1043+
node-version: '22'
1044+
cache: yarn
1045+
- name: Install dependencies
1046+
run: yarn install --frozen-lockfile --network-concurrency 1
1047+
- name: Delete stale GCP CI buckets
1048+
run: yarn run cleanup_gcp_buckets
1049+
env:
1050+
AWS_GCP_BACKEND_ACCESS_KEY: ${{ secrets.AWS_GCP_BACKEND_ACCESS_KEY }}
1051+
AWS_GCP_BACKEND_SECRET_KEY: ${{ secrets.AWS_GCP_BACKEND_SECRET_KEY }}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"ft_node_routes": "cd tests/functional/raw-node && yarn run test-routes",
110110
"ft_route_backbeat": "cd tests/multipleBackend/routes && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 routeBackbeat.js routeBackbeatForReplication.js --exit",
111111
"ft_gcp": "cd tests/functional/raw-node && yarn run test-gcp",
112+
"cleanup_gcp_buckets": "node .github/scripts/cleanupOldGCPBuckets.js",
112113
"ft_healthchecks": "cd tests/functional/healthchecks && yarn test",
113114
"ft_s3cmd": "cd tests/functional/s3cmd && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",
114115
"ft_s3curl": "cd tests/functional/s3curl && mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 40000 *.js --exit",

tests/functional/raw-node/test/GCP/bucket/get.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ const {
1010
DeleteObjectCommand,
1111
} = require('@aws-sdk/client-s3');
1212
const { GCP } = arsenal.storage.data.external.GCP;
13-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
13+
const { genUniqID, genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
1414
const { getRealAwsConfig } =
1515
require('../../../../aws-node-sdk/test/support/awsConfig');
1616
const { listingHardLimit } = require('../../../../../../constants');
1717

1818
const credentialOne = 'gcpbackend';
19-
const bucketName = `cldsrvci-get-${genUniqID()}`;
19+
const bucketName = genBucketName('get');
2020
const smallSize = 20;
2121
const bigSize = listingHardLimit + 1;
2222
const config = getRealAwsConfig(credentialOne);

tests/functional/raw-node/test/GCP/bucket/getVersioning.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const {
77
DeleteBucketCommand,
88
} = require('@aws-sdk/client-s3');
99
const { GCP } = arsenal.storage.data.external.GCP;
10-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
10+
const { genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
1111
const { getRealAwsConfig } =
1212
require('../../../../aws-node-sdk/test/support/awsConfig');
1313

@@ -20,7 +20,7 @@ describe('GCP: GET Bucket Versioning', () => {
2020
const gcpClient = new GCP(config);
2121

2222
beforeEach(async function beforeFn() {
23-
this.currentTest.bucketName = `cldsrvci-getversioning-${genUniqID()}`;
23+
this.currentTest.bucketName = genBucketName('getversioning');
2424
await gcpRetry(
2525
gcpClient,
2626
new CreateBucketCommand({

tests/functional/raw-node/test/GCP/bucket/head.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const assert = require('assert');
22
const arsenal = require('arsenal');
33
const { GCP } = arsenal.storage.data.external.GCP;
4-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
4+
const { genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
55
const { getRealAwsConfig } =
66
require('../../../../aws-node-sdk/test/support/awsConfig');
77
const {
@@ -17,7 +17,7 @@ describe('GCP: HEAD Bucket', () => {
1717

1818
describe('without existing bucket', () => {
1919
beforeEach(async function beforeFn() {
20-
this.currentTest.bucketName = `cldsrvci-head-${genUniqID()}`;
20+
this.currentTest.bucketName = genBucketName('head');
2121
});
2222

2323
it('should return 404', async function testFn() {
@@ -37,7 +37,7 @@ describe('GCP: HEAD Bucket', () => {
3737

3838
describe('with existing bucket', () => {
3939
beforeEach(async function beforeFn() {
40-
this.currentTest.bucketName = `cldsrvci-head-${genUniqID()}`;
40+
this.currentTest.bucketName = genBucketName('head');
4141
process.stdout
4242
.write(`Creating test bucket ${this.currentTest.bucketName}\n`);
4343
await gcpRetry(

tests/functional/raw-node/test/GCP/bucket/putVersioning.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ const {
77
DeleteBucketCommand,
88
} = require('@aws-sdk/client-s3');
99
const { GCP } = arsenal.storage.data.external.GCP;
10-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
10+
const { genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
1111
const { getRealAwsConfig } =
1212
require('../../../../aws-node-sdk/test/support/awsConfig');
1313

1414
const credentialOne = 'gcpbackend';
1515
const verEnabledStatus = 'Enabled';
1616
const verDisabledStatus = 'Suspended';
17-
const bucketName = `cldsrvci-putversioning-${genUniqID()}`;
17+
const bucketName = genBucketName('putversioning');
1818

1919
describe('GCP: PUT Bucket Versioning', () => {
2020
const config = getRealAwsConfig(credentialOne);

tests/functional/raw-node/test/GCP/object/completeMpu.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { GCP, GcpUtils } = arsenal.storage.data.external.GCP;
77
const {
88
gcpMpuSetup,
99
genUniqID,
10+
genBucketName,
1011
gcpRetry,
1112
waitForBucketReady,
1213
} = require('../../../utils/gcpUtils');
@@ -20,11 +21,11 @@ const {
2021
const credentialOne = 'gcpbackend';
2122
const bucketNames = {
2223
main: {
23-
Name: `cldsrvci-completempu-${genUniqID()}`,
24+
Name: genBucketName('completempu'),
2425
Type: 'MULTI_REGIONAL',
2526
},
2627
mpu: {
27-
Name: `cldsrvci-mpu-completempu-${genUniqID()}`,
28+
Name: genBucketName('mpu-completempu'),
2829
Type: 'MULTI_REGIONAL',
2930
},
3031
};

tests/functional/raw-node/test/GCP/object/copy.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const assert = require('assert');
22
const async = require('async');
33
const arsenal = require('arsenal');
44
const { GCP } = arsenal.storage.data.external.GCP;
5-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
5+
const { genUniqID, genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
66
const { getRealAwsConfig } =
77
require('../../../../aws-node-sdk/test/support/awsConfig');
88
const {
@@ -12,7 +12,7 @@ const {
1212
} = require('@aws-sdk/client-s3');
1313

1414
const credentialOne = 'gcpbackend';
15-
const bucketName = `cldsrvci-copy-${genUniqID()}`;
15+
const bucketName = genBucketName('copy');
1616

1717
describe('GCP: COPY Object', function testSuite() {
1818
this.timeout(180000);

tests/functional/raw-node/test/GCP/object/delete.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const assert = require('assert');
22
const async = require('async');
33
const arsenal = require('arsenal');
44
const { GCP } = arsenal.storage.data.external.GCP;
5-
const { genUniqID, gcpRetry } = require('../../../utils/gcpUtils');
5+
const { genUniqID, genBucketName, gcpRetry } = require('../../../utils/gcpUtils');
66
const { getRealAwsConfig } =
77
require('../../../../aws-node-sdk/test/support/awsConfig');
88
const {
@@ -13,7 +13,7 @@ const {
1313
} = require('@aws-sdk/client-s3');
1414

1515
const credentialOne = 'gcpbackend';
16-
const bucketName = `cldsrvci-deleteobj-${genUniqID()}`;
16+
const bucketName = genBucketName('deleteobj');
1717
const objectKey = `somekey-${genUniqID()}`;
1818
const badObjectKey = `nonexistingkey-${genUniqID()}`;
1919

0 commit comments

Comments
 (0)