@leverege/build-tools 2.96.1 → 2.96.3

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.1",
3
+ "version": "2.96.3",
4
4
  "description": "A collection of build / support tools for Leverege developers",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -35,6 +35,7 @@
35
35
  "encrypt-secrets": "src/encrypt-secrets.sh",
36
36
  "firebaseDeploy": "src/firebaseDeploy.mjs",
37
37
  "firebaseServe": "src/firebaseServe.mjs",
38
+ "gcp-snapshots": "src/gcp-snapshots.mjs",
38
39
  "generate-docs": "src/generate-docs.mjs",
39
40
  "getfbcfg": "src/getfbcfg.mjs",
40
41
  "getjson": "src/getjson.mjs",
@@ -89,6 +90,7 @@
89
90
  "execa": "^9.6.1",
90
91
  "find-yarn-workspace-root": "^2.0.0",
91
92
  "glob": "^13.0.0",
93
+ "googleapis": "^170.1.0",
92
94
  "handlebars": "^4.7.8",
93
95
  "ignore": "^7.0.5",
94
96
  "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()