1
1
/*
2
- Copyright © 2024 Google LLC
2
+ Copyright © 2024-2025 Google LLC
3
3
4
4
Licensed under the Apache License, Version 2.0 (the "License");
5
5
you may not use this file except in compliance with the License.
@@ -23,75 +23,228 @@ const fs = require("fs"),
23
23
debug = require ( "debug" ) ( "apigeelint:download" ) ;
24
24
25
25
const downloadBundle = async ( downloadSpec ) => {
26
- // 0. validate the input. it should be org:ORGNAME,api:APINAME or org:ORGNAME,sf:SHAREDFLOWNAME
27
- let parts = downloadSpec . split ( "," ) ;
28
- let invalidArgument = ( ) => {
26
+ // 0. validate the input. it should be one of the following formats:
27
+ let validSegmentExamples = [
28
+ "org:ORGNAME" ,
29
+ "api:APINAME" ,
30
+ "sf:SHAREDFLOWNAME" ,
31
+ "rev:REVISION" ,
32
+ "env:ENVIRONMENT" ,
33
+ "org:ORGNAME,sf:SHAREDFLOWNAME,rev:REVISION" ,
34
+ "org:ORGNAME,sf:SHAREDFLOWNAME,env:ENVIRONMENT" ,
35
+ ] ;
36
+
37
+ const segments = downloadSpec . split ( "," ) ;
38
+
39
+ const invalidArgument = ( casenum_for_diagnostics ) => {
29
40
console . log (
30
- "Specify the value in the form org:ORGNAME,api:APINAME or org:ORGNAME,sf:SHAREDFLOWNAME" ,
41
+ `Invalid download argument (${ downloadSpec } ).\n` +
42
+ "The value should be a set of 2 or more comma-separated segments of this form:\n " +
43
+ validSegmentExamples . join ( "\n " ) +
44
+ "\n\n" +
45
+ "Specify segments in any order. You must always specify the org.\n" +
46
+ "Specify at least one of {api,sf}. Specify at most one of {rev,env}.\n" +
47
+ "The token segment is optional. Multiple segments of the same type are not allowed." ,
31
48
) ;
32
49
process . exit ( 1 ) ;
33
50
} ;
34
- if ( ! parts || ( parts . length != 2 && parts . length != 3 ) ) {
35
- invalidArgument ( ) ;
36
- }
37
- let orgparts = parts [ 0 ] . split ( ":" ) ;
38
- if ( ! orgparts || orgparts . length != 2 || orgparts [ 0 ] != "org" ) {
39
- invalidArgument ( ) ;
40
- }
41
- let assetparts = parts [ 1 ] . split ( ":" ) ;
42
- if ( ! assetparts || assetparts . length != 2 ) {
43
- invalidArgument ( ) ;
44
- }
45
- if ( assetparts [ 0 ] != "api" && assetparts [ 0 ] != "sf" ) {
46
- invalidArgument ( ) ;
51
+
52
+ if ( ! segments || segments . length < 2 || segments . length > 4 ) {
53
+ invalidArgument ( 1 ) ;
47
54
}
48
55
49
- let providedToken = null ;
50
- if ( parts . length == 3 ) {
51
- let tokenParts = parts [ 2 ] . split ( ":" ) ;
52
- if ( ! tokenParts || tokenParts . length != 2 || tokenParts [ 0 ] != "token" ) {
53
- invalidArgument ( ) ;
56
+ const processSegment = ( acc , segment ) => {
57
+ const parts = segment . split ( ":" ) ;
58
+ if ( ! parts || parts . length != 2 ) {
59
+ invalidArgument ( 2 ) ;
54
60
}
55
- providedToken = tokenParts [ 1 ] ;
61
+ switch ( parts [ 0 ] ) {
62
+ case "org" :
63
+ if ( acc . org || ! parts [ 1 ] ) {
64
+ invalidArgument ( 4 ) ;
65
+ }
66
+ acc . org = parts [ 1 ] ;
67
+ break ;
68
+ case "api" :
69
+ case "sf" :
70
+ if ( acc . assetName || ! parts [ 1 ] ) {
71
+ invalidArgument ( 5 ) ;
72
+ }
73
+ acc . assetName = parts [ 1 ] ;
74
+ acc . assetFlavor = parts [ 0 ] ;
75
+ break ;
76
+ case "rev" :
77
+ if ( acc . revision || acc . environment || ! parts [ 1 ] ) {
78
+ invalidArgument ( 6 ) ;
79
+ }
80
+ acc . revision = parts [ 1 ] ;
81
+ break ;
82
+ case "env" :
83
+ if ( acc . revision || acc . environment || ! parts [ 1 ] ) {
84
+ invalidArgument ( 7 ) ;
85
+ }
86
+ acc . environment = parts [ 1 ] ;
87
+ break ;
88
+ case "token" :
89
+ if ( acc . token || ! parts [ 1 ] ) {
90
+ invalidArgument ( 8 ) ;
91
+ }
92
+ acc . token = parts [ 1 ] ;
93
+ break ;
94
+ default :
95
+ invalidArgument ( 3 ) ;
96
+ break ;
97
+ }
98
+ return acc ;
99
+ } ;
100
+
101
+ const digest = segments . reduce ( processSegment , { } ) ;
102
+ // make sure we got enough information
103
+ if ( ! digest . assetName || ! digest . assetFlavor || ! digest . org ) {
104
+ invalidArgument ( 9 ) ;
56
105
}
57
106
58
- const execOptions = {
59
- // cwd: proxyDir, // I think i do not care
60
- encoding : "utf8" ,
61
- } ;
62
107
try {
63
- // 1. use the provided token, or get a new one using gcloud. This may fail.
64
- let accessToken =
65
- providedToken ||
108
+ // 1. figure the access token. Use the provided one, or try to get a new one
109
+ // using gcloud, which may fail.
110
+ const execOptions = {
111
+ encoding : "utf8" ,
112
+ } ;
113
+ const accessToken =
114
+ digest . token ||
66
115
child_process . execSync ( "gcloud auth print-access-token" , execOptions ) ;
67
- // 2. inquire the revisions
68
- let flavor = assetparts [ 0 ] == "api" ? "apis" : "sharedflows" ;
69
- const urlbase = `https://apigee.googleapis.com/v1/organizations/${ orgparts [ 1 ] } /${ flavor } ` ;
116
+
117
+ // 2. set up some basic stuff.
118
+ const collectionName = digest . assetFlavor == "api" ? "apis" : "sharedflows" ;
119
+ const urlbase = `https://apigee.googleapis.com/v1/organizations/${ digest . org } ` ;
70
120
const headers = {
71
121
Accept : "application/json" ,
72
122
Authorization : `Bearer ${ accessToken } ` ,
73
123
} ;
74
124
75
- let url = `${ urlbase } /${ assetparts [ 1 ] } /revisions` ;
76
- let revisionsResponse = await fetch ( url , { method : "GET" , headers } ) ;
125
+ const determineRevision = async ( ) => {
126
+ const rev = digest . revision ,
127
+ env = digest . environment ;
128
+ const getLatestRevision = async ( ) => {
129
+ const url = `${ urlbase } /${ collectionName } /${ digest . assetName } /revisions` ;
130
+ const revisionsResponse = await fetch ( url , { method : "GET" , headers } ) ;
131
+ if ( ! revisionsResponse . ok ) {
132
+ throw new Error (
133
+ `HTTP error: ${ revisionsResponse . status } , on GET ${ url } ` ,
134
+ ) ;
135
+ }
136
+ const revisions = await revisionsResponse . json ( ) ;
137
+ revisions . sort ( ( a , b ) => a - b ) ;
138
+ return revisions [ revisions . length - 1 ] ;
139
+ } ;
140
+ const getLatestDeployedRevision = async ( environment ) => {
141
+ // find latest deployed revision in environment (could be more than one!)
142
+ // verify that the environment exists
143
+ let url = `${ urlbase } /environments/${ environment } ` ;
144
+ const envResponse = await fetch ( url , { method : "GET" , headers } ) ;
145
+ if ( envResponse . status == 404 ) {
146
+ throw new Error (
147
+ `The environment ${ environment } does not appear to exist` ,
148
+ ) ;
149
+ }
150
+ if ( ! envResponse . ok ) {
151
+ throw new Error (
152
+ `cannot inquire environment ${ environment } , on GET ${ url } ` ,
153
+ ) ;
154
+ }
77
155
78
- // 3. export the latest revision
79
- if ( ! revisionsResponse . ok ) {
80
- throw new Error ( `HTTP error: ${ revisionsResponse . status } , on GET ${ url } ` ) ;
156
+ url = `${ urlbase } /environments/${ environment } /${ collectionName } /${ digest . assetName } /deployments` ;
157
+ const deploymentsResponse = await fetch ( url , {
158
+ method : "GET" ,
159
+ headers,
160
+ } ) ;
161
+ if ( ! deploymentsResponse . ok ) {
162
+ throw new Error (
163
+ `HTTP error: ${ deploymentsResponse . status } , on GET ${ url } ` ,
164
+ ) ;
165
+ }
166
+ const r = await deploymentsResponse . json ( ) ;
167
+ // {
168
+ // "deployments": [
169
+ // {
170
+ // "environment": "eval",
171
+ // "apiProxy": "vjwt-b292612131",
172
+ // "revision": "3",
173
+ // "deployStartTime": "1695131144728",
174
+ // "proxyDeploymentType": "EXTENSIBLE"
175
+ // }
176
+ // ]
177
+ // }
178
+ if ( ! r . deployments || ! r . deployments . length ) {
179
+ throw new Error (
180
+ `That ${ digest . assetFlavor } is not deployed in ${ digest . environment } ` ,
181
+ ) ;
182
+ }
183
+ r . deployments . sort ( ( a , b ) => Number ( a . revision ) - Number ( b . revision ) ) ;
184
+ return r . deployments [ r . deployments . length - 1 ] . revision ;
185
+ } ;
186
+
187
+ if ( rev && env ) {
188
+ // both revision or environment specified
189
+ throw new Error ( "overspecified arguments" ) ; // should never happen
190
+ }
191
+
192
+ if ( ( ! rev && ! env ) || ( rev && rev . toLowerCase ( ) == "latest" ) ) {
193
+ // no revision or environment specified,
194
+ // the keyword 'latest' is specified; get the latest revision (deployed or not).
195
+ const rev = await getLatestRevision ( ) ;
196
+ console . log ( `Downloading revision ${ rev } ` ) ;
197
+ return Number ( rev ) ;
198
+ }
199
+
200
+ if ( env ) {
201
+ // an environment is specified
202
+ const rev = await getLatestDeployedRevision ( env ) ;
203
+ console . log ( `Downloading revision ${ rev } ` ) ;
204
+ return Number ( rev ) ;
205
+ }
206
+
207
+ // a revision number is specified; return it.
208
+ return ! isNaN ( rev ) && Number ( rev ) ;
209
+ } ;
210
+
211
+ // 3. determine the revision. Use the provided one, or select the right one.
212
+ const revision = await determineRevision ( ) ;
213
+
214
+ if ( ! revision || revision < 0 ) {
215
+ throw new Error ( `Invalid revision number` ) ;
216
+ }
217
+ // 4. verify that the revision exists
218
+ let url = `${ urlbase } /${ collectionName } /${ digest . assetName } /revisions/${ revision } ` ;
219
+ const revisionResponse = await fetch ( url , { method : "GET" , headers } ) ;
220
+ if ( revisionResponse . status == 404 ) {
221
+ throw new Error (
222
+ `Revision ${ revision } of ${ digest . assetFlavor } ${ digest . assetName } does not appear to exist` ,
223
+ ) ;
224
+ }
225
+ if ( ! revisionResponse . ok ) {
226
+ throw new Error (
227
+ `cannot inquire revision ${ revision } of ${ digest . assetFlavor } ${ digest . assetName } , on GET ${ url } ` ,
228
+ ) ;
81
229
}
82
- const revisions = await revisionsResponse . json ( ) ;
83
- revisions . sort ( ( a , b ) => a - b ) ;
84
- const rev = revisions [ revisions . length - 1 ] ;
85
- url = `${ urlbase } /${ assetparts [ 1 ] } /revisions/${ rev } ?format=bundle` ;
86
230
231
+ // 5. export the revision.
232
+ url = `${ urlbase } /${ collectionName } /${ digest . assetName } /revisions/${ revision } ?format=bundle` ;
87
233
const tmpdir = tmp . dirSync ( {
88
- prefix : `apigeelint-download-${ assetparts [ 0 ] } ` ,
234
+ prefix : `apigeelint-download-${ digest . assetFlavor } ` ,
89
235
keep : false ,
236
+ unsafeCleanup : true , // this does not seem to work in apigeelint
90
237
} ) ;
238
+ // make sure to cleanup when the process exits
239
+ process . on ( "exit" , function ( ) {
240
+ tmpdir . removeCallback ( ) ;
241
+ } ) ;
242
+
91
243
const pathToDownloadedAsset = path . join (
92
244
tmpdir . name ,
93
- `${ assetparts [ 1 ] } -rev ${ rev } .zip` ,
245
+ `${ digest . assetName } -r ${ revision } .zip` ,
94
246
) ;
247
+
95
248
const stream = fs . createWriteStream ( pathToDownloadedAsset ) ;
96
249
const { body } = await fetch ( url , { method : "GET" , headers } ) ;
97
250
await finished ( Readable . fromWeb ( body ) . pipe ( stream ) ) ;
0 commit comments