@leverege/build-tools 2.96.1 → 2.96.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leverege/build-tools",
|
|
3
|
-
"version": "2.96.
|
|
3
|
+
"version": "2.96.2",
|
|
4
4
|
"description": "A collection of build / support tools for Leverege developers",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"execa": "^9.6.1",
|
|
90
90
|
"find-yarn-workspace-root": "^2.0.0",
|
|
91
91
|
"glob": "^13.0.0",
|
|
92
|
+
"googleapis": "^170.1.0",
|
|
92
93
|
"handlebars": "^4.7.8",
|
|
93
94
|
"ignore": "^7.0.5",
|
|
94
95
|
"inquirer": "^13.2.0",
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander'
|
|
4
|
+
import { google } from 'googleapis'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
debug,
|
|
8
|
+
errorExit,
|
|
9
|
+
log,
|
|
10
|
+
err,
|
|
11
|
+
} from './Utils.mjs'
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name( 'gcp-snapshots' )
|
|
15
|
+
.description( 'Simple GCP wrapper to ease the pain of snapshot management' )
|
|
16
|
+
.requiredOption( '--project <id>', 'GCP project id (required)' )
|
|
17
|
+
.option( '--age <days>', 'Number of days (default: 180)', '180' )
|
|
18
|
+
.option( '--list', 'List snapshots older than --age days' )
|
|
19
|
+
.option( '--delete', 'Delete snapshots older than --age days' )
|
|
20
|
+
.parse()
|
|
21
|
+
|
|
22
|
+
const options = program.opts()
|
|
23
|
+
|
|
24
|
+
const parseAgeDays = ( value ) => {
|
|
25
|
+
const n = Number( value )
|
|
26
|
+
if ( !Number.isFinite( n ) || n <= 0 ) {
|
|
27
|
+
errorExit( `--age must be a positive number. Got: ${value}` )
|
|
28
|
+
}
|
|
29
|
+
return n
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cutoffDateFromAgeDays = ( days ) => {
|
|
33
|
+
return new Date( Date.now() - days * 24 * 60 * 60 * 1000 )
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const bytesToGb = ( bytes ) => {
|
|
37
|
+
if ( !Number.isFinite( bytes ) || bytes <= 0 ) return 0
|
|
38
|
+
return Math.round( ( bytes / ( 1024 ** 3 ) ) * 10 ) / 10 // 1 decimal GB
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const formatRow = ( cols, widths ) => {
|
|
42
|
+
return cols.map( ( c, i ) => {
|
|
43
|
+
const s = String( c ?? '' )
|
|
44
|
+
return s.length > widths[i] ?
|
|
45
|
+
`${s.slice( 0, widths[i] - 1 )}…` : s.padEnd( widths[i] )
|
|
46
|
+
} ).join( ' ' )
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const getComputeClient = async () => {
|
|
50
|
+
const auth = await google.auth.getClient( {
|
|
51
|
+
scopes : [ 'https://www.googleapis.com/auth/cloud-platform' ],
|
|
52
|
+
} )
|
|
53
|
+
|
|
54
|
+
return google.compute( { version : 'v1', auth } )
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fetchSnapshotsOlderThan = async ( { compute, projectId, cutoff } ) => {
|
|
58
|
+
const snapshots = []
|
|
59
|
+
let pageToken
|
|
60
|
+
|
|
61
|
+
debug( `Listing snapshots for project=${projectId}` )
|
|
62
|
+
debug( `Cutoff date=${cutoff.toISOString()}` )
|
|
63
|
+
|
|
64
|
+
do {
|
|
65
|
+
// eslint-disable-next-line no-await-in-loop
|
|
66
|
+
const res = await compute.snapshots.list( {
|
|
67
|
+
project : projectId,
|
|
68
|
+
maxResults : 500,
|
|
69
|
+
pageToken,
|
|
70
|
+
} )
|
|
71
|
+
|
|
72
|
+
const items = res.data.items ?? []
|
|
73
|
+
for ( const s of items ) {
|
|
74
|
+
if ( !s.creationTimestamp ) continue
|
|
75
|
+
const created = new Date( s.creationTimestamp )
|
|
76
|
+
if ( created < cutoff ) snapshots.push( s )
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pageToken = res.data.nextPageToken
|
|
80
|
+
} while ( pageToken )
|
|
81
|
+
|
|
82
|
+
// Oldest -> newest
|
|
83
|
+
snapshots.sort( ( a, b ) => (
|
|
84
|
+
new Date( a.creationTimestamp ) - new Date( b.creationTimestamp )
|
|
85
|
+
) )
|
|
86
|
+
|
|
87
|
+
return snapshots
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const printSnapshotTable = async ( { snapshots, ageDays, cutoff } ) => {
|
|
91
|
+
log(
|
|
92
|
+
`Snapshots older than ${ageDays} days ` +
|
|
93
|
+
`(cutoff ${cutoff.toISOString().slice( 0, 10 )}) — ${snapshots.length} found`
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
const widths = [ 40, 20, 10, 8, 32 ]
|
|
97
|
+
log( formatRow(
|
|
98
|
+
[ 'NAME', 'CREATED', 'SIZE_GB', 'DISK_GB', 'SOURCE_DISK' ],
|
|
99
|
+
widths
|
|
100
|
+
) )
|
|
101
|
+
|
|
102
|
+
for ( const s of snapshots ) {
|
|
103
|
+
const sourceDisk = s.sourceDisk ? s.sourceDisk.split( '/' ).pop() : ''
|
|
104
|
+
|
|
105
|
+
log( formatRow(
|
|
106
|
+
[
|
|
107
|
+
s.name,
|
|
108
|
+
s.creationTimestamp.replace( 'T', ' ' ).replace( 'Z', '' ),
|
|
109
|
+
bytesToGb( Number( s.storageBytes ) ),
|
|
110
|
+
s.diskSizeGb ?? '',
|
|
111
|
+
sourceDisk,
|
|
112
|
+
],
|
|
113
|
+
widths
|
|
114
|
+
) )
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const deleteSnapshots = async ( { compute, projectId, snapshots } ) => {
|
|
119
|
+
if ( snapshots.length === 0 ) {
|
|
120
|
+
log( 'Nothing to delete.' )
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
log( `Deleting ${snapshots.length} snapshot(s)...` )
|
|
125
|
+
|
|
126
|
+
let ok = 0
|
|
127
|
+
let failed = 0
|
|
128
|
+
|
|
129
|
+
for ( const s of snapshots ) {
|
|
130
|
+
const name = s.name
|
|
131
|
+
if ( !name ) continue
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
log( `- deleting: ${name}` )
|
|
135
|
+
// eslint-disable-next-line no-await-in-loop
|
|
136
|
+
await compute.snapshots.delete( {
|
|
137
|
+
project : projectId,
|
|
138
|
+
snapshot : name,
|
|
139
|
+
} )
|
|
140
|
+
ok++
|
|
141
|
+
} catch ( e ) {
|
|
142
|
+
failed++
|
|
143
|
+
err( `! failed: ${name} — ${e?.message ?? e}` )
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
log( `Done. Deleted: ${ok}. Failed: ${failed}.` )
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const main = async () => {
|
|
151
|
+
if ( !options.list && !options.delete ) {
|
|
152
|
+
program.help( { error : false } )
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const ageDays = parseAgeDays( options.age )
|
|
157
|
+
const cutoff = cutoffDateFromAgeDays( ageDays )
|
|
158
|
+
|
|
159
|
+
const compute = await getComputeClient()
|
|
160
|
+
|
|
161
|
+
debug( `project=${options.project}` )
|
|
162
|
+
debug( `cutoff=${cutoff.toISOString()}` )
|
|
163
|
+
|
|
164
|
+
const snapshots = await fetchSnapshotsOlderThan( {
|
|
165
|
+
compute,
|
|
166
|
+
projectId : options.project,
|
|
167
|
+
cutoff,
|
|
168
|
+
} )
|
|
169
|
+
|
|
170
|
+
// Always show what we're about to act on.
|
|
171
|
+
await printSnapshotTable( { snapshots, ageDays, cutoff } )
|
|
172
|
+
|
|
173
|
+
if ( options.delete ) {
|
|
174
|
+
await deleteSnapshots( {
|
|
175
|
+
compute,
|
|
176
|
+
projectId : options.project,
|
|
177
|
+
snapshots,
|
|
178
|
+
} )
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
await main()
|
|
Binary file
|