@@ -6,22 +6,23 @@ import { needProjectId } from "../projectUtils";
6
6
import { confirm } from "../prompt" ;
7
7
import { requirePermissions } from "../requirePermissions" ;
8
8
import { requireAuth } from "../requireAuth" ;
9
- import { logBullet , logSuccess } from "../utils" ;
9
+ import { logBullet , logError , logSuccess , logWarning } from "../utils" ;
10
10
import * as artifactregistry from "../gcp/artifactregistry" ;
11
11
import * as artifacts from "../functions/artifacts" ;
12
+ import * as prompts from "../deploy/functions/prompts" ;
12
13
13
14
/**
14
15
* Command to set up a cleanup policy for Cloud Run functions container images in Artifact Registry
15
16
*/
16
17
export const command = new Command ( "functions:artifacts:setpolicy" )
17
18
. description (
18
19
"Set up a cleanup policy for Cloud Run functions container images in Artifact Registry. " +
19
- "This policy will automatically delete old container images created during functions deployment." ,
20
+ "This policy will automatically delete old container images created during functions deployment." ,
20
21
)
21
22
. option (
22
23
"--location <location>" ,
23
24
"Specify location to set up the cleanup policy. " +
24
- "If omitted, uses the default functions location." ,
25
+ "If omitted, uses the default functions location." ,
25
26
"us-central1" ,
26
27
)
27
28
. option (
@@ -46,65 +47,48 @@ export const command = new Command("functions:artifacts:setpolicy")
46
47
if ( options . days && options . none ) {
47
48
throw new FirebaseError ( "Cannot specify both --days and --none options." ) ;
48
49
}
50
+
49
51
const projectId = needProjectId ( options ) ;
50
- const location = options . location || "us-central1" ;
51
- let daysToKeep = parseInt ( options . days || artifacts . DEFAULT_CLEANUP_DAYS , 10 ) ;
52
+ const locationInput = options . location || "us-central1" ;
53
+ const locations : string [ ] = locationInput . split ( ',' ) . map ( ( loc : string ) => loc . trim ( ) ) ;
54
+ const uniqueLocations : string [ ] = [ ...new Set ( locations ) ] ;
52
55
53
- const repoPath = artifacts . makeRepoPath ( projectId , location ) ;
54
- let repository : artifactregistry . Repository ;
55
- try {
56
- repository = await artifactregistry . getRepository ( repoPath ) ;
57
- } catch ( err : any ) {
58
- if ( err . status === 404 ) {
59
- logBullet ( `Repository '${ repoPath } ' does not exist in Artifact Registry.` ) ;
60
- logBullet (
61
- `Please deploy your functions first using: ` +
62
- `${ clc . bold ( `firebase deploy --only functions` ) } ` ,
63
- ) ;
64
- return ;
65
- }
66
- throw err ;
56
+ if ( uniqueLocations . length === 0 ) {
57
+ throw new FirebaseError ( "No valid locations specified" ) ;
67
58
}
68
59
69
- if ( options . none ) {
70
- const existingPolicy = artifacts . findExistingPolicy ( repository ) ;
60
+ const checkResults = await artifacts . checkCleanupPolicy ( projectId , uniqueLocations ) ;
71
61
72
- if ( artifacts . hasCleanupOptOut ( repository ) && ! existingPolicy ) {
73
- logBullet ( `Repository '${ repoPath } ' is already opted out from cleanup policies.` ) ;
74
- logBullet ( `No changes needed.` ) ;
75
- return ;
76
- }
62
+ const statusToLocations = Object . entries ( checkResults ) . reduce < Record < artifacts . CheckPolicyResult , string [ ] > > (
63
+ ( acc , [ location , status ] ) => {
64
+ acc [ status ] = acc [ status ] || [ ] ;
65
+ acc [ status ] ! . push ( location ) ;
66
+ return acc ;
67
+ } ,
68
+ { } as Record < artifacts . CheckPolicyResult , string [ ] >
69
+ ) ;
77
70
78
- logBullet ( `You are about to opt-out from cleanup policy for repository '${ repoPath } '.` ) ;
71
+ const repoNotFound = statusToLocations [ 'notFound' ] || [ ] ;
72
+ if ( repoNotFound . length > 0 ) {
73
+ logWarning ( `Repository not found in ${ repoNotFound . length > 1 ? 'locations' : 'location' } ${ repoNotFound . join ( ", " ) } ` ) ;
79
74
logBullet (
80
- `This will prevent suggestions to set up cleanup policy during initialization and deployment.` ,
75
+ `Please deploy your functions first using: ` +
76
+ `${ clc . bold ( `firebase deploy --only functions` ) } `
81
77
) ;
78
+ }
82
79
83
- if ( existingPolicy ) {
84
- logBullet ( `Note: This will remove the existing cleanup policy from the repository.` ) ;
85
- }
86
-
87
- const confirmOptOut = await confirm ( {
88
- ...options ,
89
- default : true ,
90
- message : "Do you want to continue?" ,
91
- } ) ;
92
-
93
- if ( ! confirmOptOut ) {
94
- throw new FirebaseError ( "Command aborted." , { exit : 1 } ) ;
95
- }
96
-
97
- try {
98
- await artifacts . optOutRepository ( repository ) ;
99
- logSuccess ( `Successfully opted out from cleanup policy for ${ clc . bold ( repoPath ) } ` ) ;
100
- return ;
101
- } catch ( err : unknown ) {
102
- throw new FirebaseError ( "Failed to opt-out from artifact registry cleanup policy" , {
103
- original : err as Error ,
104
- } ) ;
105
- }
80
+ const repoErred = statusToLocations [ 'errored' ] || [ ] ;
81
+ if ( repoErred . length > 0 ) {
82
+ logWarning ( `Failed to retrieve state of ${ repoErred . length > 1 ? 'repositories' : 'repository' } ${ repoErred . join ( ", " ) } ` ) ;
83
+ logWarning ( `Skipping setting up cleanup policy. Please try again later.` ) ;
106
84
}
107
85
86
+ if ( options . none ) {
87
+ return await handleOptOut ( projectId , statusToLocations , options ) ;
88
+ }
89
+
90
+ let daysToKeep = parseInt ( options . days || artifacts . DEFAULT_CLEANUP_DAYS , 10 ) ;
91
+
108
92
if ( isNaN ( daysToKeep ) || daysToKeep < 0 ) {
109
93
throw new FirebaseError ( "Days must be a non-negative number" ) ;
110
94
}
@@ -113,43 +97,131 @@ export const command = new Command("functions:artifacts:setpolicy")
113
97
daysToKeep = 0.003472 ; // ~5 minutes in days
114
98
}
115
99
116
- if ( artifacts . hasSameCleanupPolicy ( repository , daysToKeep ) ) {
100
+ return await handleSetupPolicies ( projectId , statusToLocations , daysToKeep , options ) ;
101
+
102
+ } ) ;
103
+
104
+ async function handleOptOut ( projectId : string , checkResults : Record < artifacts . CheckPolicyResult , string [ ] > , options : any ) {
105
+ const locationsToOptOut = ( [ 'foundPolicy' , 'noPolicy' , 'optedOut' ] as artifacts . CheckPolicyResult [ ] )
106
+ . flatMap ( status => checkResults [ status ] || [ ] ) ;
107
+
108
+ if ( locationsToOptOut . length === 0 ) {
109
+ logBullet ( "No repositories to opt-out from cleanup policy" ) ;
110
+ return ;
111
+ }
112
+
113
+ logBullet (
114
+ `You are about to opt-out from cleanup policies for ${ prompts . formatMany ( locationsToOptOut , "repository" , "repositories" ) } `
115
+ ) ;
116
+ logBullet (
117
+ `This will prevent suggestions to set up cleanup policy during initialization and deployment.` ,
118
+ ) ;
119
+
120
+ const reposWithPolicy = checkResults [ 'foundPolicy' ] || [ ] ;
121
+ if ( reposWithPolicy . length > 0 ) {
122
+ logBullet ( `Note: This will remove the existing cleanup policy for ${ prompts . formatMany ( locationsToOptOut , "repository" , "repositories" ) } .` ) ;
123
+ }
124
+
125
+ const confirmOptOut = await confirm ( {
126
+ ...options ,
127
+ default : true ,
128
+ message : `Do you want to opt-out from cleanup policies for ${ locationsToOptOut . length } repositories?` ,
129
+ } ) ;
130
+
131
+ if ( ! confirmOptOut ) {
132
+ throw new FirebaseError ( "Command aborted." , { exit : 1 } ) ;
133
+ }
134
+
135
+ const results = await artifacts . optOutRepositories ( projectId , locationsToOptOut ) ;
136
+
137
+ const locationsOptedOutSuccessfully = Object . entries ( results )
138
+ . filter ( ( [ _ , result ] ) => result . status === 'success' )
139
+ . map ( ( [ location , _ ] ) => location ) ;
140
+
141
+ const locationsWithErrors = Object . entries ( results )
142
+ . filter ( ( [ _ , result ] ) => result . status === 'errored' )
143
+ . map ( ( [ location , _ ] ) => location ) ;
144
+
145
+ if ( locationsOptedOutSuccessfully . length > 0 ) {
146
+ logSuccess ( `Successfully opted out ${ prompts . formatMany ( locationsOptedOutSuccessfully , "location" ) } from cleanup policies.` ) ;
147
+ }
148
+
149
+ if ( locationsWithErrors . length > 0 ) {
150
+ const errs = Object . entries ( results )
151
+ . filter ( ( [ _ , result ] ) => result . status === "errored" )
152
+ . map ( ( [ _ , result ] ) => result . error )
153
+ . filter ( err => ! ! err ) ;
154
+ throw new FirebaseError (
155
+ `Failed to complete opt-out for all repositories in ${ prompts . formatMany ( locationsWithErrors , "location" ) } .` ,
156
+ { children : errs } ,
157
+ ) ;
158
+ }
159
+ }
160
+
161
+ async function handleSetupPolicies (
162
+ projectId : string ,
163
+ checkResults : Record < artifacts . CheckPolicyResult , string [ ] > ,
164
+ daysToKeep : number ,
165
+ options : any
166
+ ) {
167
+ const locationsNoPolicy = checkResults [ 'noPolicy' ] || [ ] ;
168
+ const locationsWithPolicy = checkResults [ 'foundPolicy' ] || [ ] ;
169
+ const locationsOptedOut = checkResults [ 'optedOut' ] || [ ] ;
170
+
171
+ const locationsToSetup : string [ ] = [ ] ;
172
+ const locationsWithSamePolicy : string [ ] = [ ] ;
173
+ const locationsNeedingUpdate : string [ ] = [ ] ;
174
+
175
+ for ( const location of locationsWithPolicy ) {
176
+ const repo = await artifacts . getRepo ( projectId , location ) ;
177
+
178
+ if ( artifacts . hasSameCleanupPolicy ( repo , daysToKeep ) ) {
179
+ locationsWithSamePolicy . push ( location ) ;
180
+ continue ;
181
+ }
182
+ locationsNeedingUpdate . push ( location )
183
+ locationsToSetup . push ( location ) ;
184
+ }
185
+
186
+ locationsToSetup . push ( ...locationsNoPolicy , ...locationsOptedOut ) ;
187
+
188
+ if ( locationsToSetup . length === 0 ) {
189
+ if ( locationsWithSamePolicy . length > 0 ) {
117
190
logBullet (
118
- `A cleanup policy already exists that deletes images older than ${ clc . bold ( daysToKeep ) } days.` ,
191
+ `A cleanup policy already exists that deletes images older than ${ daysToKeep } days for ${ prompts . formatMany ( locationsWithSamePolicy , "repository" , "repositories" )
192
+ } .`
119
193
) ;
120
194
logBullet ( `No changes needed.` ) ;
121
- return ;
195
+ } else {
196
+ logBullet ( "No repositories need cleanup policy setup." ) ;
122
197
}
198
+ return ;
199
+ }
123
200
201
+ logBullet (
202
+ `You are about to set up cleanup policies for ${ prompts . formatMany ( locationsToSetup , "repository" , "repositories" ) } `
203
+ ) ;
204
+ logBullet (
205
+ `This will automatically delete container images that are older than ${ daysToKeep } days`
206
+ ) ;
207
+ logBullet (
208
+ "This helps reduce storage costs by removing old container images that are no longer needed"
209
+ ) ;
210
+
211
+ if ( locationsNeedingUpdate . length > 0 ) {
124
212
logBullet (
125
- `You are about to set up a cleanup policy for Cloud Run functions container images in location ${ clc . bold ( location ) } ` ,
126
- ) ;
127
- logBullet (
128
- `This policy will automatically delete container images that are older than ${ clc . bold ( daysToKeep ) } days` ,
213
+ `Note: This will update existing policies for ${ prompts . formatMany ( locationsNeedingUpdate , "repository" , "repositories" ) } `
129
214
) ;
215
+ }
216
+
217
+ if ( locationsOptedOut . length > 0 ) {
130
218
logBullet (
131
- "This helps reduce storage costs by removing old container images that are no longer needed" ,
219
+ `Note: ${ prompts . formatMany ( locationsOptedOut , "Repository" , "Repositories" ) } ${ locationsOptedOut . length === 1 ? "was" : "were"
220
+ } previously opted out from cleanup policy. This action will remove the opt-out status.`
132
221
) ;
222
+ }
133
223
134
- const existingPolicy = artifacts . findExistingPolicy ( repository ) ;
135
-
136
- let isUpdate = false ;
137
- if ( existingPolicy && existingPolicy . condition ?. olderThan ) {
138
- const existingDays = artifacts . parseDaysFromPolicy ( existingPolicy . condition . olderThan ) ;
139
- if ( existingDays ) {
140
- isUpdate = true ;
141
- logBullet (
142
- `Note: This will update an existing policy that currently deletes images older than ${ clc . bold ( existingDays ) } days` ,
143
- ) ;
144
- }
145
- }
146
-
147
- if ( artifacts . hasCleanupOptOut ( repository ) ) {
148
- logBullet (
149
- `Note: This repository was previously opted out from cleanup policy. This action will remove the opt-out status.` ,
150
- ) ;
151
- }
152
-
224
+ if ( ! options . force ) {
153
225
const confirmSetup = await confirm ( {
154
226
...options ,
155
227
default : true ,
@@ -159,17 +231,33 @@ export const command = new Command("functions:artifacts:setpolicy")
159
231
if ( ! confirmSetup ) {
160
232
throw new FirebaseError ( "Command aborted." , { exit : 1 } ) ;
161
233
}
234
+ }
162
235
163
- try {
164
- await artifacts . setCleanupPolicy ( repository , daysToKeep ) ;
165
- const successMessage = isUpdate
166
- ? `Successfully updated cleanup policy to delete images older than ${ clc . bold ( daysToKeep ) } days`
167
- : `Successfully set up cleanup policy that deletes images older than ${ clc . bold ( daysToKeep ) } days` ;
168
- logSuccess ( successMessage ) ;
169
- logBullet ( `Cleanup policy has been set for ${ clc . bold ( repoPath ) } ` ) ;
170
- } catch ( err : unknown ) {
171
- throw new FirebaseError ( "Failed to set up artifact registry cleanup policy" , {
172
- original : err as Error ,
173
- } ) ;
174
- }
175
- } ) ;
236
+ const setPolicyResults = await artifacts . setCleanupPolicies ( projectId , locationsToSetup , daysToKeep ) ;
237
+
238
+ const locationsConfiguredSuccessfully = Object . entries ( setPolicyResults )
239
+ . filter ( ( [ _ , result ] ) => result . status === "success" )
240
+ . map ( ( [ location , _ ] ) => location ) ;
241
+
242
+ const locationsWithSetupErrors = Object . entries ( setPolicyResults )
243
+ . filter ( ( [ _ , result ] ) => result . status === "errored" )
244
+ . map ( ( [ location , _ ] ) => location ) ;
245
+
246
+ if ( locationsConfiguredSuccessfully . length > 0 ) {
247
+ logSuccess (
248
+ `Successfully updated cleanup policy to delete images older than ${ daysToKeep } days for ${ prompts . formatMany ( locationsConfiguredSuccessfully , "repository" , "repositories" )
249
+ } `
250
+ ) ;
251
+ }
252
+ if ( locationsWithSetupErrors . length > 0 ) {
253
+ const errs = Object . entries ( setPolicyResults )
254
+ . filter ( ( [ _ , result ] ) => result . status === "errored" )
255
+ . map ( ( [ _ , result ] ) => result . error )
256
+ . filter ( err => ! ! err ) ;
257
+
258
+ throw new FirebaseError (
259
+ `Failed to set up cleanup policy in ${ prompts . formatMany ( locationsWithSetupErrors , "location" ) } . ` +
260
+ { children : errs }
261
+ ) ;
262
+ }
263
+ }
0 commit comments