-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathconfirm.ts
More file actions
338 lines (302 loc) · 11.2 KB
/
Copy pathconfirm.ts
File metadata and controls
338 lines (302 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
import { formatMigrationNumber } from "#/cli/commands/tailordb/migrate/migration-number";
import { styles, logger } from "#/cli/shared/logger";
import { prompt } from "#/cli/shared/prompt";
import ml from "#/utils/multiline";
import type { MigrationCheckpointRepair } from "#/cli/commands/tailordb/migrate/types";
export interface OwnerConflict {
resourceType: string;
resourceName: string;
currentOwner: string;
}
export interface UnmanagedResource {
resourceType: string;
resourceName: string;
}
export async function confirmMigrationCheckpointRepairs(
repairs: ReadonlyArray<MigrationCheckpointRepair>,
yes: boolean,
): Promise<void> {
if (repairs.length === 0) return;
logger.warn(
"TailorDB migration checkpoints and history IDs need to be updated before this deployment:",
);
for (const repair of repairs.toSorted((a, b) => a.namespace.localeCompare(b.namespace))) {
logger.log(` ${repair.namespace}:`);
logger.log(
` Checkpoint: ${formatMigrationNumber(repair.from)} → ${formatMigrationNumber(repair.to)}`,
);
logger.log(
` Migration history ID: ${repair.fromHistoryId ?? "<unset>"} → ${repair.toHistoryId}`,
);
}
logger.log(" The checkpoint reset itself changes only metadata.");
logger.log(" This deployment may still apply pending schema or data migrations afterward.");
if (yes) return;
const confirmed = await prompt.confirm({
message:
"Reset these migration checkpoints, align their history IDs, and continue with the deployment?",
default: false,
});
if (!confirmed) {
throw new Error("Apply cancelled: migration checkpoint reset was not confirmed.");
}
}
/**
* Confirm reassignment of resources when owner conflicts are detected.
* Splits into three scenarios, each with its own prompt because the
* user-facing meaning is different: the resource carries the same sdk-name and
* an sdk-app-id the config does not match, either because the config now holds
* a different id (regeneration) or because it holds none at all; or the
* resource carries a different sdk-name (name mismatch).
* @param conflicts - Detected owner conflicts
* @param appName - Target application name
* @param yes - Whether to auto-confirm without prompting
* @param appId - Target application id, when the config resolves to one
* @returns Promise that resolves when confirmation completes
*/
export async function confirmOwnerConflict(
conflicts: OwnerConflict[],
appName: string,
yes: boolean,
appId?: string,
): Promise<void> {
if (conflicts.length === 0) return;
// Same sdk-name as the target app -> the resources carry an id this config
// does not: either a new one replaced it, or the config has none at all.
const idMismatches = conflicts.filter((c) => c.currentOwner === appName);
const nameMismatches = conflicts.filter((c) => c.currentOwner !== appName);
if (idMismatches.length > 0) {
await (appId
? confirmIdRegeneration(idMismatches, appName, yes)
: confirmMissingConfigId(idMismatches, appName, yes));
}
if (nameMismatches.length > 0) {
await confirmNameMismatch(nameMismatches, appName, yes);
}
}
async function confirmIdRegeneration(
conflicts: OwnerConflict[],
appName: string,
yes: boolean,
): Promise<void> {
logIdMismatch(`Application id was regenerated for "${appName}":`, conflicts);
if (yes) {
logger.success("Re-tagging resources with the new id (--yes flag specified)...", {
mode: "plain",
});
return;
}
const confirmed = await prompt.confirm({
message: `Re-tag these resources with the new id for "${appName}"?\n${styles.dim("(The id in tailor.config.ts was removed since the previous deploy, so a new one was generated)")}`,
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Resources remain tagged with the previous id.
To override, run again and confirm, or use --yes flag.
`);
}
}
function logIdMismatch(heading: string, conflicts: OwnerConflict[]): void {
logger.warn(heading);
logger.log(" These resources are tagged with an id from an earlier deploy.");
logger.newline();
logger.log(` ${styles.info("Resources")}:`);
for (const c of conflicts) {
logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
}
}
async function confirmMissingConfigId(
conflicts: OwnerConflict[],
appName: string,
yes: boolean,
): Promise<void> {
logIdMismatch(`No application id resolved for "${appName}":`, conflicts);
if (yes) {
logger.success("Managing these resources by name (--yes flag specified)...", { mode: "plain" });
return;
}
const confirmed = await prompt.confirm({
message: `Drop that id and manage these resources by name for "${appName}"?\n${styles.dim("(the config resolves without an 'id', so ownership falls back to the application name)")}`,
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Resources remain tagged with their current id.
Restore the 'id' in your config to keep owning them by id, or run again and confirm to own them by name.
`);
}
}
async function confirmNameMismatch(
conflicts: OwnerConflict[],
appName: string,
yes: boolean,
): Promise<void> {
const currentOwners = [...new Set(conflicts.map((c) => c.currentOwner))];
logger.warn("Application name mismatch detected:");
logger.log(
` ${styles.warning("Current application(s)")}: ${currentOwners.map((o) => styles.bold(`"${o}"`)).join(", ")}`,
);
logger.log(` ${styles.success("New application")}: ${styles.bold(`"${appName}"`)}`);
logger.newline();
logger.log(` ${styles.info("Resources")}:`);
for (const c of conflicts) {
logger.log(` • ${styles.bold(c.resourceType)} ${styles.info(`"${c.resourceName}"`)}`);
}
if (yes) {
logger.success("Updating resources (--yes flag specified)...", {
mode: "plain",
});
return;
}
const promptMessage =
currentOwners.length === 1
? `Update these resources to be managed by "${appName}"?\n${styles.dim("(Common when renaming your application)")}`
: `Update these resources to be managed by "${appName}"?`;
const confirmed = await prompt.confirm({
message: promptMessage,
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Resources remain managed by their current applications.
To override, run again and confirm, or use --yes flag.
`);
}
}
/**
* Confirm allowing tailor to manage previously unmanaged resources.
* @param resources - Unmanaged resources
* @param appName - Target application name
* @param yes - Whether to auto-confirm without prompting
* @returns Promise that resolves when confirmation completes
*/
export async function confirmUnmanagedResources(
resources: UnmanagedResource[],
appName: string,
yes: boolean,
): Promise<void> {
if (resources.length === 0) return;
logger.warn("Existing resources not tracked by tailor were found:");
logger.log(` ${styles.info("Resources")}:`);
for (const r of resources) {
logger.log(` • ${styles.bold(r.resourceType)} ${styles.info(`"${r.resourceName}"`)}`);
}
logger.newline();
logger.log(" These resources may have been created by older SDK versions, Terraform, or CUE.");
logger.log(" To continue, confirm that tailor should manage them.");
logger.log(
" If they are managed by another tool (e.g., Terraform), cancel and manage them there instead.",
);
if (yes) {
logger.success(`Adding to "${appName}" (--yes flag specified)...`, {
mode: "plain",
});
return;
}
const confirmed = await prompt.confirm({
message: `Allow tailor to manage these resources for "${appName}"?`,
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Resources remain unmanaged.
To override, run again and confirm, or use --yes flag.
`);
}
}
export interface ImportantResourceDeletion {
resourceType: string;
resourceName: string;
}
/**
* Confirm deletion of important resources.
* @param resources - Resources scheduled for deletion
* @param yes - Whether to auto-confirm without prompting
* @returns Promise that resolves when confirmation completes
*/
export async function confirmImportantResourceDeletion(
resources: ImportantResourceDeletion[],
yes: boolean,
): Promise<void> {
if (resources.length === 0) return;
logger.warn("The following resources will be deleted:");
logger.log(` ${styles.info("Resources")}:`);
for (const r of resources) {
logger.log(` • ${styles.bold(r.resourceType)} ${styles.error(`"${r.resourceName}"`)}`);
}
logger.newline();
logger.log(
styles.warning(" Deleting these resources will permanently remove all associated data."),
);
if (yes) {
logger.success("Deleting resources (--yes flag specified)...", {
mode: "plain",
});
return;
}
const confirmed = await prompt.confirm({
message: "Are you sure you want to delete these resources?",
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Resources will not be deleted.
To override, run again and confirm, or use --yes flag.
`);
}
}
/** An application recorded as needing to take part in this deploy, but absent. */
export interface MissingDependentApp {
/**
* The resource whose value is applied differently without the dependent, named
* as messages name it, e.g. `TailorDB type "Order"`. Records live on the
* resource, so this identifies one of those rather than an application.
*/
resource: string;
/** Stable id of the absent application. */
appId: string;
/** Why it has to take part in the same deploy. */
reason: string;
}
/**
* Confirm continuing without an application recorded as a dependency.
*
* A previous deploy recorded that another config's executors make this config's
* resources publish events. Applying this config alone resolves those flags from
* a smaller set of executors, which turns publishing off.
* @param missing - Recorded dependencies absent from this deploy
* @param yes - Whether `--yes` was passed
* @returns Promise that resolves when the deploy may continue
*/
export async function confirmMissingDependentApps(
missing: MissingDependentApp[],
yes: boolean,
): Promise<void> {
if (missing.length === 0) return;
logger.warn("Applications recorded as depending on this deploy are missing:");
for (const entry of missing) {
logger.log(
` • application id ${styles.info(entry.appId)} depends on ${styles.bold(entry.resource)} (${entry.reason})`,
);
}
logger.newline();
logger.log(" Applying without them turns off event publishing on the resources above:");
logger.log(" nothing in this deploy subscribes to them, so the value resolves to false.");
logger.log(" To keep it, add their configs to --config, or set publishEvents on");
logger.log(" the resources above.");
if (yes) {
logger.warn("Continuing without them (--yes flag specified); applying turns publishing off.");
return;
}
const confirmed = await prompt.confirm({
message: "Continue without them?",
default: false,
});
if (!confirmed) {
throw new Error(ml`
Apply cancelled. Add the missing configs to --config, or set publishEvents
explicitly on the resources that should keep publishing.
`);
}
}