@creact-labs/creact 0.1.8 → 0.2.1
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/README.md +85 -22
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +88 -0
- package/dist/index.d.ts +19 -44
- package/dist/index.js +20 -68
- package/dist/jsx/index.d.ts +2 -0
- package/dist/jsx/index.js +1 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +4 -0
- package/dist/jsx/jsx-dev-runtime.js +4 -0
- package/dist/jsx/jsx-runtime.d.ts +38 -0
- package/dist/jsx/jsx-runtime.js +38 -0
- package/dist/jsx/types.d.ts +12 -0
- package/dist/jsx/types.js +4 -0
- package/dist/primitives/context.d.ts +34 -0
- package/dist/primitives/context.js +63 -0
- package/dist/primitives/index.d.ts +3 -0
- package/dist/primitives/index.js +3 -0
- package/dist/primitives/instance.d.ts +72 -0
- package/dist/primitives/instance.js +235 -0
- package/dist/primitives/store.d.ts +22 -0
- package/dist/primitives/store.js +97 -0
- package/dist/provider/backend.d.ts +110 -0
- package/dist/provider/backend.js +37 -0
- package/dist/provider/interface.d.ts +48 -0
- package/dist/provider/interface.js +39 -0
- package/dist/reactive/effect.d.ts +11 -0
- package/dist/reactive/effect.js +42 -0
- package/dist/reactive/index.d.ts +3 -0
- package/dist/reactive/index.js +3 -0
- package/dist/reactive/signal.d.ts +32 -0
- package/dist/reactive/signal.js +60 -0
- package/dist/reactive/tracking.d.ts +41 -0
- package/dist/reactive/tracking.js +161 -0
- package/dist/runtime/fiber.d.ts +21 -0
- package/dist/runtime/fiber.js +16 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/reconcile.d.ts +66 -0
- package/dist/runtime/reconcile.js +210 -0
- package/dist/runtime/render.d.ts +42 -0
- package/dist/runtime/render.js +231 -0
- package/dist/runtime/run.d.ts +119 -0
- package/dist/runtime/run.js +334 -0
- package/dist/runtime/state-machine.d.ts +95 -0
- package/dist/runtime/state-machine.js +209 -0
- package/dist/types.d.ts +13 -0
- package/dist/types.js +4 -0
- package/package.json +29 -24
- package/dist/cli/commands/BuildCommand.d.ts +0 -40
- package/dist/cli/commands/BuildCommand.js +0 -151
- package/dist/cli/commands/DeployCommand.d.ts +0 -38
- package/dist/cli/commands/DeployCommand.js +0 -194
- package/dist/cli/commands/DevCommand.d.ts +0 -52
- package/dist/cli/commands/DevCommand.js +0 -394
- package/dist/cli/commands/PlanCommand.d.ts +0 -39
- package/dist/cli/commands/PlanCommand.js +0 -164
- package/dist/cli/commands/index.d.ts +0 -36
- package/dist/cli/commands/index.js +0 -43
- package/dist/cli/core/ArgumentParser.d.ts +0 -46
- package/dist/cli/core/ArgumentParser.js +0 -127
- package/dist/cli/core/BaseCommand.d.ts +0 -75
- package/dist/cli/core/BaseCommand.js +0 -95
- package/dist/cli/core/CLIContext.d.ts +0 -68
- package/dist/cli/core/CLIContext.js +0 -183
- package/dist/cli/core/CommandRegistry.d.ts +0 -64
- package/dist/cli/core/CommandRegistry.js +0 -89
- package/dist/cli/core/index.d.ts +0 -36
- package/dist/cli/core/index.js +0 -43
- package/dist/cli/index.d.ts +0 -35
- package/dist/cli/index.js +0 -100
- package/dist/cli/output.d.ts +0 -204
- package/dist/cli/output.js +0 -437
- package/dist/cli/utils.d.ts +0 -59
- package/dist/cli/utils.js +0 -76
- package/dist/context/createContext.d.ts +0 -90
- package/dist/context/createContext.js +0 -113
- package/dist/context/index.d.ts +0 -30
- package/dist/context/index.js +0 -35
- package/dist/core/CReact.d.ts +0 -409
- package/dist/core/CReact.js +0 -1151
- package/dist/core/CloudDOMBuilder.d.ts +0 -447
- package/dist/core/CloudDOMBuilder.js +0 -1234
- package/dist/core/ContextDependencyTracker.d.ts +0 -165
- package/dist/core/ContextDependencyTracker.js +0 -448
- package/dist/core/ErrorRecoveryManager.d.ts +0 -145
- package/dist/core/ErrorRecoveryManager.js +0 -443
- package/dist/core/EventBus.d.ts +0 -91
- package/dist/core/EventBus.js +0 -185
- package/dist/core/ProviderOutputTracker.d.ts +0 -211
- package/dist/core/ProviderOutputTracker.js +0 -476
- package/dist/core/ReactiveUpdateQueue.d.ts +0 -76
- package/dist/core/ReactiveUpdateQueue.js +0 -121
- package/dist/core/Reconciler.d.ts +0 -415
- package/dist/core/Reconciler.js +0 -1044
- package/dist/core/RenderScheduler.d.ts +0 -153
- package/dist/core/RenderScheduler.js +0 -519
- package/dist/core/Renderer.d.ts +0 -336
- package/dist/core/Renderer.js +0 -944
- package/dist/core/Runtime.d.ts +0 -246
- package/dist/core/Runtime.js +0 -640
- package/dist/core/StateBindingManager.d.ts +0 -121
- package/dist/core/StateBindingManager.js +0 -309
- package/dist/core/StateMachine.d.ts +0 -441
- package/dist/core/StateMachine.js +0 -883
- package/dist/core/StructuralChangeDetector.d.ts +0 -140
- package/dist/core/StructuralChangeDetector.js +0 -363
- package/dist/core/Validator.d.ts +0 -127
- package/dist/core/Validator.js +0 -279
- package/dist/core/errors.d.ts +0 -153
- package/dist/core/errors.js +0 -202
- package/dist/core/index.d.ts +0 -38
- package/dist/core/index.js +0 -64
- package/dist/core/types.d.ts +0 -265
- package/dist/core/types.js +0 -48
- package/dist/hooks/context.d.ts +0 -147
- package/dist/hooks/context.js +0 -334
- package/dist/hooks/useContext.d.ts +0 -113
- package/dist/hooks/useContext.js +0 -169
- package/dist/hooks/useEffect.d.ts +0 -105
- package/dist/hooks/useEffect.js +0 -540
- package/dist/hooks/useInstance.d.ts +0 -139
- package/dist/hooks/useInstance.js +0 -455
- package/dist/hooks/useState.d.ts +0 -120
- package/dist/hooks/useState.js +0 -298
- package/dist/jsx.d.ts +0 -143
- package/dist/jsx.js +0 -76
- package/dist/providers/DummyBackendProvider.d.ts +0 -193
- package/dist/providers/DummyBackendProvider.js +0 -189
- package/dist/providers/DummyCloudProvider.d.ts +0 -128
- package/dist/providers/DummyCloudProvider.js +0 -157
- package/dist/providers/IBackendProvider.d.ts +0 -177
- package/dist/providers/IBackendProvider.js +0 -31
- package/dist/providers/ICloudProvider.d.ts +0 -230
- package/dist/providers/ICloudProvider.js +0 -31
- package/dist/providers/index.d.ts +0 -31
- package/dist/providers/index.js +0 -31
- package/dist/test-event-callbacks.d.ts +0 -0
- package/dist/test-event-callbacks.js +0 -1
- package/dist/utils/Logger.d.ts +0 -144
- package/dist/utils/Logger.js +0 -220
- package/dist/utils/Output.d.ts +0 -161
- package/dist/utils/Output.js +0 -401
- package/dist/utils/deepEqual.d.ts +0 -71
- package/dist/utils/deepEqual.js +0 -276
- package/dist/utils/naming.d.ts +0 -241
- package/dist/utils/naming.js +0 -376
|
@@ -1,883 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
|
|
8
|
-
* You may obtain a copy of the License at
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
17
|
-
|
|
18
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
-
|
|
20
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
21
|
-
|
|
22
|
-
* See the License for the specific language governing permissions and
|
|
23
|
-
|
|
24
|
-
* limitations under the License.
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
* Copyright 2025 Daniel Coutinho Ribeiro
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
-
exports.StateMachine = void 0;
|
|
33
|
-
const errors_1 = require("./errors");
|
|
34
|
-
const Logger_1 = require("../utils/Logger");
|
|
35
|
-
const logger = Logger_1.LoggerFactory.getLogger('state-machine');
|
|
36
|
-
/**
|
|
37
|
-
* StateMachine manages deployment lifecycle with crash recovery
|
|
38
|
-
*
|
|
39
|
-
* Responsibilities:
|
|
40
|
-
* - Track deployment state transitions
|
|
41
|
-
* - Save checkpoints after each resource deploys
|
|
42
|
-
* - Enable crash recovery (resume or rollback)
|
|
43
|
-
* - Provide atomic state persistence via BackendProvider
|
|
44
|
-
* - Prevent concurrent deployments via locking (REQ-O02)
|
|
45
|
-
* - Emit audit trail for compliance (REQ-O05)
|
|
46
|
-
* - Retry transient backend failures (REQ-O03)
|
|
47
|
-
*
|
|
48
|
-
* Design principles:
|
|
49
|
-
* - All state changes are atomic (saved to BackendProvider immediately)
|
|
50
|
-
* - Checkpoints enable resume from any point
|
|
51
|
-
* - Rollback applies reverse change set
|
|
52
|
-
* - State machine is universal (works with all providers)
|
|
53
|
-
* - Locking prevents race conditions
|
|
54
|
-
* - Audit trail provides compliance and debugging
|
|
55
|
-
*
|
|
56
|
-
* REQ-O01: CloudDOM State Machine
|
|
57
|
-
* REQ-O02: State Locking
|
|
58
|
-
* REQ-O03: Retry Logic
|
|
59
|
-
* REQ-O05: Audit Log
|
|
60
|
-
* REQ-ARCH-01: Provider-Orchestration Separation
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```typescript
|
|
64
|
-
* const stateMachine = new StateMachine(backendProvider);
|
|
65
|
-
*
|
|
66
|
-
* // Listen to events
|
|
67
|
-
* stateMachine.on('checkpoint', (state) => {
|
|
68
|
-
* logger.info(`Checkpoint: ${state.checkpoint}`);
|
|
69
|
-
* });
|
|
70
|
-
*
|
|
71
|
-
* // Start deployment (acquires lock)
|
|
72
|
-
* await stateMachine.startDeployment('my-stack', changeSet, cloudDOM, 'user@example.com');
|
|
73
|
-
*
|
|
74
|
-
* // Update checkpoint after each resource
|
|
75
|
-
* await stateMachine.updateCheckpoint('my-stack', 0);
|
|
76
|
-
* await stateMachine.updateCheckpoint('my-stack', 1);
|
|
77
|
-
*
|
|
78
|
-
* // Complete deployment (releases lock)
|
|
79
|
-
* await stateMachine.completeDeployment('my-stack');
|
|
80
|
-
*
|
|
81
|
-
* // Or handle failure (releases lock)
|
|
82
|
-
* await stateMachine.failDeployment('my-stack', new DeploymentError('Provider timeout'));
|
|
83
|
-
* ```
|
|
84
|
-
*/
|
|
85
|
-
class StateMachine {
|
|
86
|
-
constructor(backendProvider, options = {}) {
|
|
87
|
-
this.backendProvider = backendProvider;
|
|
88
|
-
this.options = options;
|
|
89
|
-
this.listeners = {
|
|
90
|
-
started: [],
|
|
91
|
-
checkpoint: [],
|
|
92
|
-
completed: [],
|
|
93
|
-
failed: [],
|
|
94
|
-
rolled_back: [],
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Map of active lock renewal timers by stack name
|
|
98
|
-
* Used to clean up timers when deployment completes
|
|
99
|
-
*/
|
|
100
|
-
this.lockRenewalTimers = new Map();
|
|
101
|
-
this.options = {
|
|
102
|
-
maxRetries: 3,
|
|
103
|
-
enableAuditLog: true,
|
|
104
|
-
enableSnapshots: false,
|
|
105
|
-
lockTTL: 600,
|
|
106
|
-
...options,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Subscribe to state machine events
|
|
111
|
-
*
|
|
112
|
-
* Enables observability and telemetry integration.
|
|
113
|
-
*
|
|
114
|
-
* @param event - Event type to listen for
|
|
115
|
-
* @param handler - Callback function receiving structured payload
|
|
116
|
-
*/
|
|
117
|
-
on(event, handler) {
|
|
118
|
-
this.listeners[event].push(handler);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Unsubscribe from state machine events
|
|
122
|
-
*
|
|
123
|
-
* @param event - Event type to stop listening for
|
|
124
|
-
* @param handler - Callback function to remove
|
|
125
|
-
*/
|
|
126
|
-
off(event, handler) {
|
|
127
|
-
const index = this.listeners[event].indexOf(handler);
|
|
128
|
-
if (index !== -1) {
|
|
129
|
-
this.listeners[event].splice(index, 1);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Emit event to all listeners with structured payload
|
|
134
|
-
*
|
|
135
|
-
* @param event - Event type
|
|
136
|
-
* @param state - Current deployment state
|
|
137
|
-
* @param metadata - Additional metadata (optional)
|
|
138
|
-
*/
|
|
139
|
-
emit(event, state, metadata) {
|
|
140
|
-
const payload = {
|
|
141
|
-
event,
|
|
142
|
-
state,
|
|
143
|
-
metadata,
|
|
144
|
-
};
|
|
145
|
-
for (const handler of this.listeners[event]) {
|
|
146
|
-
try {
|
|
147
|
-
handler(payload);
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// Don't let listener errors break state machine
|
|
151
|
-
logger.error(`Error in StateMachine event handler for ${event}:`, error);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Start lock auto-renewal for a stack
|
|
157
|
-
*
|
|
158
|
-
* Renews the lock at 50% of the TTL interval to prevent expiration
|
|
159
|
-
* during long deployments. Only starts renewal if backend supports locking.
|
|
160
|
-
*
|
|
161
|
-
* @param stackName - Name of the stack
|
|
162
|
-
* @param holder - Lock holder identifier
|
|
163
|
-
* @param ttl - Lock TTL in seconds
|
|
164
|
-
*/
|
|
165
|
-
startLockRenewal(stackName, holder, ttl) {
|
|
166
|
-
// Only start renewal if backend supports locking
|
|
167
|
-
if (!this.backendProvider.acquireLock) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
// Clear any existing timer for this stack
|
|
171
|
-
this.stopLockRenewal(stackName);
|
|
172
|
-
// Renew at 50% of TTL (convert to milliseconds)
|
|
173
|
-
const renewalInterval = (ttl * 1000) / 2;
|
|
174
|
-
const timer = setInterval(async () => {
|
|
175
|
-
try {
|
|
176
|
-
// Renew the lock with the same TTL
|
|
177
|
-
await this.backendProvider.acquireLock(stackName, holder, ttl);
|
|
178
|
-
logger.debug(`Lock renewed for stack: ${stackName}`);
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
logger.error(`Failed to renew lock for ${stackName}:`, error);
|
|
182
|
-
// Stop renewal on failure to prevent spam
|
|
183
|
-
this.stopLockRenewal(stackName);
|
|
184
|
-
}
|
|
185
|
-
}, renewalInterval);
|
|
186
|
-
this.lockRenewalTimers.set(stackName, timer);
|
|
187
|
-
logger.debug(`Lock auto-renewal started for stack: ${stackName} (interval: ${renewalInterval}ms)`);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Stop lock auto-renewal for a stack
|
|
191
|
-
*
|
|
192
|
-
* Cleans up the renewal timer to prevent memory leaks.
|
|
193
|
-
*
|
|
194
|
-
* @param stackName - Name of the stack
|
|
195
|
-
*/
|
|
196
|
-
stopLockRenewal(stackName) {
|
|
197
|
-
const timer = this.lockRenewalTimers.get(stackName);
|
|
198
|
-
if (timer) {
|
|
199
|
-
clearInterval(timer);
|
|
200
|
-
this.lockRenewalTimers.delete(stackName);
|
|
201
|
-
logger.debug(`Lock auto-renewal stopped for stack: ${stackName}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Validate state transition
|
|
206
|
-
*
|
|
207
|
-
* Ensures state machine only transitions through valid states.
|
|
208
|
-
* Prevents accidental invalid state mutations.
|
|
209
|
-
*
|
|
210
|
-
* @param from - Current state
|
|
211
|
-
* @param to - Target state
|
|
212
|
-
* @throws DeploymentError if transition is invalid
|
|
213
|
-
*/
|
|
214
|
-
validateTransition(from, to) {
|
|
215
|
-
const allowedTransitions = StateMachine.VALID_TRANSITIONS[from];
|
|
216
|
-
if (!allowedTransitions || !allowedTransitions.includes(to)) {
|
|
217
|
-
throw new errors_1.DeploymentError(`Invalid state transition: ${from} → ${to}`, {
|
|
218
|
-
message: `Invalid state transition: ${from} → ${to}`,
|
|
219
|
-
code: 'INVALID_STATE_TRANSITION',
|
|
220
|
-
details: {
|
|
221
|
-
from,
|
|
222
|
-
to,
|
|
223
|
-
allowedTransitions,
|
|
224
|
-
},
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* Retry operation with exponential backoff
|
|
230
|
-
*
|
|
231
|
-
* Handles transient backend failures (network issues, timeouts, etc.)
|
|
232
|
-
*
|
|
233
|
-
* REQ-O03: Error handling and retry logic
|
|
234
|
-
*
|
|
235
|
-
* @param operation - Async operation to retry
|
|
236
|
-
* @param retries - Number of retries (default: from options)
|
|
237
|
-
* @returns Promise resolving to operation result
|
|
238
|
-
* @throws Error if all retries fail
|
|
239
|
-
*/
|
|
240
|
-
async withRetry(operation, retries = this.options.maxRetries ?? 3) {
|
|
241
|
-
let lastError;
|
|
242
|
-
for (let i = 0; i < retries; i++) {
|
|
243
|
-
try {
|
|
244
|
-
return await operation();
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
lastError = err;
|
|
248
|
-
// Don't retry on final attempt
|
|
249
|
-
if (i < retries - 1) {
|
|
250
|
-
// Exponential backoff: 100ms, 200ms, 400ms, etc.
|
|
251
|
-
const delay = Math.pow(2, i) * 100;
|
|
252
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
throw new errors_1.DeploymentError(`Backend operation failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`, {
|
|
257
|
-
message: `Backend operation failed after ${retries} attempts`,
|
|
258
|
-
code: 'BACKEND_OPERATION_FAILED',
|
|
259
|
-
details: { originalError: lastError, retries },
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Log action to audit trail
|
|
264
|
-
*
|
|
265
|
-
* REQ-O05: Audit log for compliance
|
|
266
|
-
*
|
|
267
|
-
* @param stackName - Stack name
|
|
268
|
-
* @param action - Action performed
|
|
269
|
-
* @param state - Current deployment state
|
|
270
|
-
* @param error - Error if action failed
|
|
271
|
-
*/
|
|
272
|
-
async logAction(stackName, action, state, error) {
|
|
273
|
-
if (!this.options.enableAuditLog) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (!this.backendProvider.appendAuditLog) {
|
|
277
|
-
// Backend doesn't support audit logging yet
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
const entry = {
|
|
281
|
-
timestamp: Date.now(),
|
|
282
|
-
user: state.user,
|
|
283
|
-
action,
|
|
284
|
-
stackName,
|
|
285
|
-
status: error ? 'failed' : 'completed',
|
|
286
|
-
error: error?.message,
|
|
287
|
-
changeSet: state.changeSet,
|
|
288
|
-
checkpoint: state.checkpoint,
|
|
289
|
-
};
|
|
290
|
-
try {
|
|
291
|
-
await this.backendProvider.appendAuditLog(stackName, entry);
|
|
292
|
-
}
|
|
293
|
-
catch (err) {
|
|
294
|
-
// Don't fail deployment if audit logging fails
|
|
295
|
-
logger.error(`Failed to append audit log for ${stackName}:`, err);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/**
|
|
299
|
-
* Save state snapshot for time-travel debugging
|
|
300
|
-
*
|
|
301
|
-
* Creates a deep copy to ensure immutability.
|
|
302
|
-
*
|
|
303
|
-
* REQ-O01: Immutable state snapshots
|
|
304
|
-
*
|
|
305
|
-
* @param stackName - Stack name
|
|
306
|
-
* @param state - Current deployment state
|
|
307
|
-
*/
|
|
308
|
-
async saveSnapshot(stackName, state) {
|
|
309
|
-
if (!this.options.enableSnapshots) {
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
if (!this.backendProvider.saveSnapshot) {
|
|
313
|
-
// Backend doesn't support snapshots yet
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
// Deep copy to ensure immutability
|
|
318
|
-
const snapshot = JSON.parse(JSON.stringify({
|
|
319
|
-
...state,
|
|
320
|
-
timestamp: Date.now(),
|
|
321
|
-
}));
|
|
322
|
-
await this.backendProvider.saveSnapshot(stackName, snapshot);
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
// Don't fail deployment if snapshot fails
|
|
326
|
-
logger.error(`Failed to save snapshot for ${stackName}:`, err);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Start a deployment transaction
|
|
331
|
-
*
|
|
332
|
-
* Transitions state from PENDING to APPLYING and saves to backend.
|
|
333
|
-
* Acquires lock to prevent concurrent deployments.
|
|
334
|
-
*
|
|
335
|
-
* REQ-O01.1: WHEN deployment starts THEN CloudDOM state SHALL transition to APPLYING
|
|
336
|
-
* REQ-O02: State locking to prevent concurrent deployments
|
|
337
|
-
*
|
|
338
|
-
* @param stackName - Name of the stack being deployed
|
|
339
|
-
* @param changeSet - Change set computed by Reconciler
|
|
340
|
-
* @param cloudDOM - CloudDOM tree being deployed
|
|
341
|
-
* @param user - User initiating the deployment
|
|
342
|
-
* @returns Promise that resolves when state is saved
|
|
343
|
-
* @throws DeploymentError if lock cannot be acquired or state save fails
|
|
344
|
-
*/
|
|
345
|
-
async startDeployment(stackName, changeSet, cloudDOM, user) {
|
|
346
|
-
// Validate changeSet is not empty
|
|
347
|
-
if (!changeSet.deploymentOrder || changeSet.deploymentOrder.length === 0) {
|
|
348
|
-
throw new errors_1.DeploymentError(`Cannot start deployment: no resources to deploy for stack ${stackName}`, {
|
|
349
|
-
message: `Cannot start deployment: no resources to deploy for stack ${stackName}`,
|
|
350
|
-
code: 'EMPTY_CHANGESET',
|
|
351
|
-
details: { stackName, changeSet },
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
// Acquire lock to prevent concurrent deployments
|
|
355
|
-
// NOTE: Lock acquisition should NOT be retried - it should fail immediately
|
|
356
|
-
if (this.backendProvider.acquireLock) {
|
|
357
|
-
const lockTTL = this.options.lockTTL ?? 600;
|
|
358
|
-
try {
|
|
359
|
-
await this.backendProvider.acquireLock(stackName, user, lockTTL);
|
|
360
|
-
// Start auto-renewal to prevent lock expiration during long deployments
|
|
361
|
-
this.startLockRenewal(stackName, user, lockTTL);
|
|
362
|
-
}
|
|
363
|
-
catch (error) {
|
|
364
|
-
throw new errors_1.DeploymentError(`Failed to acquire lock for stack ${stackName}. ` +
|
|
365
|
-
`Another deployment may be in progress.`, {
|
|
366
|
-
message: `Failed to acquire lock for stack ${stackName}`,
|
|
367
|
-
code: 'LOCK_ACQUISITION_FAILED',
|
|
368
|
-
details: { stackName, user },
|
|
369
|
-
cause: error instanceof Error ? error.message : String(error),
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
const state = {
|
|
374
|
-
status: 'APPLYING',
|
|
375
|
-
cloudDOM,
|
|
376
|
-
changeSet,
|
|
377
|
-
checkpoint: undefined,
|
|
378
|
-
timestamp: Date.now(),
|
|
379
|
-
user,
|
|
380
|
-
stackName,
|
|
381
|
-
};
|
|
382
|
-
try {
|
|
383
|
-
// Validate transition (PENDING → APPLYING)
|
|
384
|
-
// Note: We assume PENDING as initial state for new deployments
|
|
385
|
-
this.validateTransition('PENDING', 'APPLYING');
|
|
386
|
-
// Save state with retry
|
|
387
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
388
|
-
// Log action to audit trail
|
|
389
|
-
await this.logAction(stackName, 'start', state);
|
|
390
|
-
// Emit event with metadata
|
|
391
|
-
this.emit('started', state, {
|
|
392
|
-
resourceCount: changeSet.deploymentOrder.length,
|
|
393
|
-
creates: changeSet.creates.length,
|
|
394
|
-
updates: changeSet.updates.length,
|
|
395
|
-
deletes: changeSet.deletes.length,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
// Stop lock renewal and release lock if state save fails
|
|
400
|
-
this.stopLockRenewal(stackName);
|
|
401
|
-
if (this.backendProvider.releaseLock) {
|
|
402
|
-
try {
|
|
403
|
-
await this.backendProvider.releaseLock(stackName);
|
|
404
|
-
}
|
|
405
|
-
catch (releaseError) {
|
|
406
|
-
logger.error(`Failed to release lock after error:`, releaseError);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Update checkpoint after each resource deploys
|
|
414
|
-
*
|
|
415
|
-
* Saves progress to enable crash recovery. If deployment crashes,
|
|
416
|
-
* it can resume from the last checkpoint.
|
|
417
|
-
*
|
|
418
|
-
* REQ-O01.5: WHEN any provider executes THEN StateMachine SHALL checkpoint after each resource
|
|
419
|
-
*
|
|
420
|
-
* @param stackName - Name of the stack being deployed
|
|
421
|
-
* @param checkpoint - Index of last successfully deployed resource
|
|
422
|
-
* @returns Promise that resolves when checkpoint is saved
|
|
423
|
-
* @throws DeploymentError if state is invalid or save fails
|
|
424
|
-
*/
|
|
425
|
-
async updateCheckpoint(stackName, checkpoint) {
|
|
426
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
427
|
-
if (!state) {
|
|
428
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
429
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
430
|
-
code: 'STATE_NOT_FOUND',
|
|
431
|
-
details: { stackName, checkpoint },
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
if (state.status !== 'APPLYING') {
|
|
435
|
-
throw new errors_1.DeploymentError(`Cannot update checkpoint for stack in ${state.status} state. ` +
|
|
436
|
-
`Checkpoints can only be updated during APPLYING state.`, {
|
|
437
|
-
message: `Cannot update checkpoint for stack in ${state.status} state`,
|
|
438
|
-
code: 'INVALID_STATE_FOR_CHECKPOINT',
|
|
439
|
-
details: { stackName, currentStatus: state.status, checkpoint },
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
// Save snapshot before updating checkpoint (for time-travel debugging)
|
|
443
|
-
await this.saveSnapshot(stackName, state);
|
|
444
|
-
state.checkpoint = checkpoint;
|
|
445
|
-
state.timestamp = Date.now();
|
|
446
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
447
|
-
// Log checkpoint to audit trail
|
|
448
|
-
await this.logAction(stackName, 'checkpoint', state);
|
|
449
|
-
// Emit event
|
|
450
|
-
this.emit('checkpoint', state);
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Mark deployment as complete
|
|
454
|
-
*
|
|
455
|
-
* Transitions state from APPLYING to DEPLOYED and releases lock.
|
|
456
|
-
*
|
|
457
|
-
* REQ-O01.2: WHEN deployment succeeds THEN state SHALL transition to DEPLOYED
|
|
458
|
-
* REQ-O02: Release lock after deployment completes
|
|
459
|
-
*
|
|
460
|
-
* @param stackName - Name of the stack that was deployed
|
|
461
|
-
* @returns Promise that resolves when state is saved
|
|
462
|
-
* @throws DeploymentError if state is invalid or save fails
|
|
463
|
-
*/
|
|
464
|
-
async completeDeployment(stackName) {
|
|
465
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
466
|
-
if (!state) {
|
|
467
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
468
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
469
|
-
code: 'STATE_NOT_FOUND',
|
|
470
|
-
details: { stackName },
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
// Validate transition
|
|
474
|
-
this.validateTransition(state.status, 'DEPLOYED');
|
|
475
|
-
// Save snapshot before completing
|
|
476
|
-
await this.saveSnapshot(stackName, state);
|
|
477
|
-
state.status = 'DEPLOYED';
|
|
478
|
-
state.timestamp = Date.now();
|
|
479
|
-
// Clear checkpoint and changeSet after successful deployment
|
|
480
|
-
state.checkpoint = undefined;
|
|
481
|
-
state.changeSet = undefined;
|
|
482
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
483
|
-
// Log action to audit trail
|
|
484
|
-
await this.logAction(stackName, 'complete', state);
|
|
485
|
-
// Stop lock renewal and release lock
|
|
486
|
-
this.stopLockRenewal(stackName);
|
|
487
|
-
if (this.backendProvider.releaseLock) {
|
|
488
|
-
try {
|
|
489
|
-
await this.backendProvider.releaseLock(stackName);
|
|
490
|
-
}
|
|
491
|
-
catch (error) {
|
|
492
|
-
logger.error(`Failed to release lock for ${stackName}:`, error);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
// Emit event
|
|
496
|
-
this.emit('completed', state);
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Mark deployment as failed
|
|
500
|
-
*
|
|
501
|
-
* Transitions state from APPLYING to FAILED, stores error details, and releases lock.
|
|
502
|
-
*
|
|
503
|
-
* @param stackName - Name of the stack that failed
|
|
504
|
-
* @param error - Error that caused the failure (should be DeploymentError or subclass)
|
|
505
|
-
* @returns Promise that resolves when state is saved
|
|
506
|
-
* @throws DeploymentError if state is invalid or save fails
|
|
507
|
-
*/
|
|
508
|
-
async failDeployment(stackName, error) {
|
|
509
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
510
|
-
if (!state) {
|
|
511
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
512
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
513
|
-
code: 'STATE_NOT_FOUND',
|
|
514
|
-
details: { stackName },
|
|
515
|
-
cause: error.message,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
// Validate transition
|
|
519
|
-
this.validateTransition(state.status, 'FAILED');
|
|
520
|
-
// Save snapshot before failing
|
|
521
|
-
await this.saveSnapshot(stackName, state);
|
|
522
|
-
state.status = 'FAILED';
|
|
523
|
-
// Store structured error data
|
|
524
|
-
if (error instanceof errors_1.DeploymentError) {
|
|
525
|
-
state.error = error.data;
|
|
526
|
-
}
|
|
527
|
-
else {
|
|
528
|
-
state.error = {
|
|
529
|
-
message: error.message,
|
|
530
|
-
stack: error.stack,
|
|
531
|
-
code: error.code,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
state.timestamp = Date.now();
|
|
535
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
536
|
-
// Log action to audit trail
|
|
537
|
-
await this.logAction(stackName, 'fail', state, error);
|
|
538
|
-
// Stop lock renewal and release lock
|
|
539
|
-
this.stopLockRenewal(stackName);
|
|
540
|
-
if (this.backendProvider.releaseLock) {
|
|
541
|
-
try {
|
|
542
|
-
await this.backendProvider.releaseLock(stackName);
|
|
543
|
-
}
|
|
544
|
-
catch (releaseError) {
|
|
545
|
-
logger.error(`Failed to release lock for ${stackName}:`, releaseError);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
// Emit event with error metadata
|
|
549
|
-
this.emit('failed', state, {
|
|
550
|
-
errorCode: state.error?.code,
|
|
551
|
-
errorMessage: state.error?.message,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Resume interrupted deployment from checkpoint
|
|
556
|
-
*
|
|
557
|
-
* Returns the change set and checkpoint so deployment can continue
|
|
558
|
-
* from where it left off.
|
|
559
|
-
*
|
|
560
|
-
* REQ-O01.4: WHEN CReact restarts THEN it SHALL detect incomplete transactions
|
|
561
|
-
* and offer resume/rollback
|
|
562
|
-
*
|
|
563
|
-
* @param stackName - Name of the stack to resume
|
|
564
|
-
* @returns Promise resolving to change set, checkpoint index, and cloudDOM
|
|
565
|
-
* @throws DeploymentError if no incomplete deployment found
|
|
566
|
-
*/
|
|
567
|
-
async resumeDeployment(stackName) {
|
|
568
|
-
// Check lock status before resuming
|
|
569
|
-
if (this.backendProvider.checkLock) {
|
|
570
|
-
const lockInfo = await this.backendProvider.checkLock(stackName);
|
|
571
|
-
if (lockInfo) {
|
|
572
|
-
const lockAge = Date.now() - lockInfo.acquiredAt;
|
|
573
|
-
const lockTTL = lockInfo.ttl * 1000; // Convert to milliseconds
|
|
574
|
-
if (lockAge < lockTTL) {
|
|
575
|
-
throw new errors_1.DeploymentError(`Cannot resume: lock held by ${lockInfo.holder}`, {
|
|
576
|
-
message: `Cannot resume: lock held by ${lockInfo.holder}`,
|
|
577
|
-
code: 'LOCK_HELD',
|
|
578
|
-
details: {
|
|
579
|
-
stackName,
|
|
580
|
-
lockHolder: lockInfo.holder,
|
|
581
|
-
lockAcquiredAt: new Date(lockInfo.acquiredAt).toISOString(),
|
|
582
|
-
lockExpiresAt: new Date(lockInfo.acquiredAt + lockTTL).toISOString(),
|
|
583
|
-
},
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
589
|
-
if (!state) {
|
|
590
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
591
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
592
|
-
code: 'STATE_NOT_FOUND',
|
|
593
|
-
details: { stackName },
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
if (state.status !== 'APPLYING') {
|
|
597
|
-
throw new errors_1.DeploymentError(`Cannot resume deployment for stack in ${state.status} state. ` +
|
|
598
|
-
`Only APPLYING deployments can be resumed.`, {
|
|
599
|
-
message: `Cannot resume deployment for stack in ${state.status} state`,
|
|
600
|
-
code: 'INVALID_STATE_FOR_RESUME',
|
|
601
|
-
details: { stackName, currentStatus: state.status },
|
|
602
|
-
});
|
|
603
|
-
}
|
|
604
|
-
if (!state.changeSet ||
|
|
605
|
-
!state.changeSet.deploymentOrder ||
|
|
606
|
-
state.changeSet.deploymentOrder.length === 0) {
|
|
607
|
-
throw new errors_1.DeploymentError(`Cannot resume deployment: no change set found in state for stack ${stackName}`, {
|
|
608
|
-
message: `Cannot resume deployment: no change set found in state for stack ${stackName}`,
|
|
609
|
-
code: 'MISSING_CHANGESET',
|
|
610
|
-
details: { stackName },
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
return {
|
|
614
|
-
changeSet: state.changeSet,
|
|
615
|
-
checkpoint: state.checkpoint ?? -1,
|
|
616
|
-
cloudDOM: state.cloudDOM,
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* Rollback to previous state
|
|
621
|
-
*
|
|
622
|
-
* Applies reverse change set (deletes → creates, creates → deletes)
|
|
623
|
-
* and transitions state to ROLLED_BACK. Releases lock.
|
|
624
|
-
*
|
|
625
|
-
* Note: This method only updates the state machine status. The actual
|
|
626
|
-
* rollback logic (applying reverse change set) is handled by the caller
|
|
627
|
-
* (typically CReact or CLI).
|
|
628
|
-
*
|
|
629
|
-
* REQ-O01: Crash recovery with rollback
|
|
630
|
-
* REQ-O02: Release lock after rollback
|
|
631
|
-
*
|
|
632
|
-
* @param stackName - Name of the stack to rollback
|
|
633
|
-
* @returns Promise that resolves when state is saved
|
|
634
|
-
* @throws DeploymentError if state is invalid or save fails
|
|
635
|
-
*/
|
|
636
|
-
async rollback(stackName) {
|
|
637
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
638
|
-
if (!state) {
|
|
639
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
640
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
641
|
-
code: 'STATE_NOT_FOUND',
|
|
642
|
-
details: { stackName },
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
// Validate transition
|
|
646
|
-
this.validateTransition(state.status, 'ROLLED_BACK');
|
|
647
|
-
// Save snapshot before rollback
|
|
648
|
-
await this.saveSnapshot(stackName, state);
|
|
649
|
-
state.status = 'ROLLED_BACK';
|
|
650
|
-
state.timestamp = Date.now();
|
|
651
|
-
// Clear checkpoint and changeSet after rollback
|
|
652
|
-
state.checkpoint = undefined;
|
|
653
|
-
state.changeSet = undefined;
|
|
654
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
655
|
-
// Log action to audit trail
|
|
656
|
-
await this.logAction(stackName, 'rollback', state);
|
|
657
|
-
// Stop lock renewal and release lock
|
|
658
|
-
this.stopLockRenewal(stackName);
|
|
659
|
-
if (this.backendProvider.releaseLock) {
|
|
660
|
-
try {
|
|
661
|
-
await this.backendProvider.releaseLock(stackName);
|
|
662
|
-
}
|
|
663
|
-
catch (error) {
|
|
664
|
-
logger.error(`Failed to release lock for ${stackName}:`, error);
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
// Emit event
|
|
668
|
-
this.emit('rolled_back', state);
|
|
669
|
-
}
|
|
670
|
-
/**
|
|
671
|
-
* Get current deployment state for a stack
|
|
672
|
-
*
|
|
673
|
-
* @param stackName - Name of the stack
|
|
674
|
-
* @returns Promise resolving to deployment state, or undefined if not found
|
|
675
|
-
*/
|
|
676
|
-
async getState(stackName) {
|
|
677
|
-
return await this.backendProvider.getState(stackName);
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Update the CloudDOM in the deployment state (for post-deployment effects)
|
|
681
|
-
*
|
|
682
|
-
* @param stackName - Stack name
|
|
683
|
-
* @param cloudDOM - Updated CloudDOM with new outputs
|
|
684
|
-
* @throws DeploymentError if state update fails
|
|
685
|
-
*/
|
|
686
|
-
async updateCloudDOM(stackName, cloudDOM) {
|
|
687
|
-
const state = await this.withRetry(() => this.getState(stackName));
|
|
688
|
-
if (!state) {
|
|
689
|
-
throw new errors_1.DeploymentError(`No deployment state found for stack: ${stackName}`, {
|
|
690
|
-
message: `No deployment state found for stack: ${stackName}`,
|
|
691
|
-
code: 'STATE_NOT_FOUND',
|
|
692
|
-
details: { stackName },
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
// Update the CloudDOM in the state
|
|
696
|
-
state.cloudDOM = cloudDOM;
|
|
697
|
-
state.timestamp = Date.now();
|
|
698
|
-
// Save the updated state
|
|
699
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, state));
|
|
700
|
-
// Log action to audit trail
|
|
701
|
-
await this.logAction(stackName, 'checkpoint', state);
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Check if a stack has an incomplete deployment
|
|
705
|
-
*
|
|
706
|
-
* Used on startup to detect crashed deployments that need recovery.
|
|
707
|
-
*
|
|
708
|
-
* REQ-O01.3: WHEN CReact process crashes mid-deploy THEN state SHALL remain in APPLYING
|
|
709
|
-
* REQ-O01.4: WHEN CReact restarts THEN it SHALL detect incomplete transactions
|
|
710
|
-
*
|
|
711
|
-
* @param stackName - Name of the stack to check
|
|
712
|
-
* @returns Promise resolving to true if deployment is incomplete
|
|
713
|
-
*/
|
|
714
|
-
async hasIncompleteDeployment(stackName) {
|
|
715
|
-
const state = await this.getState(stackName);
|
|
716
|
-
return state?.status === 'APPLYING';
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Get checkpoint info for display
|
|
720
|
-
*
|
|
721
|
-
* Returns human-readable checkpoint information for CLI/UI.
|
|
722
|
-
* Includes resource ID for better UX.
|
|
723
|
-
*
|
|
724
|
-
* @param stackName - Name of the stack
|
|
725
|
-
* @returns Promise resolving to checkpoint info, or undefined if not found
|
|
726
|
-
*/
|
|
727
|
-
async getCheckpointInfo(stackName) {
|
|
728
|
-
const state = await this.getState(stackName);
|
|
729
|
-
if (!state || !state.changeSet) {
|
|
730
|
-
return undefined;
|
|
731
|
-
}
|
|
732
|
-
const checkpoint = state.checkpoint ?? -1;
|
|
733
|
-
const totalResources = state.changeSet.deploymentOrder.length;
|
|
734
|
-
const percentComplete = totalResources > 0 ? Math.round(((checkpoint + 1) / totalResources) * 100) : 0;
|
|
735
|
-
// Get resource ID at checkpoint for better CLI UX
|
|
736
|
-
const resourceId = checkpoint >= 0 && checkpoint < state.changeSet.deploymentOrder.length
|
|
737
|
-
? state.changeSet.deploymentOrder[checkpoint]
|
|
738
|
-
: undefined;
|
|
739
|
-
return {
|
|
740
|
-
checkpoint,
|
|
741
|
-
resourceId,
|
|
742
|
-
totalResources,
|
|
743
|
-
percentComplete,
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Auto-recover from incomplete deployment
|
|
748
|
-
*
|
|
749
|
-
* Convenience method for CLI/orchestrator to automatically resume or rollback
|
|
750
|
-
* based on configuration.
|
|
751
|
-
*
|
|
752
|
-
* @param stackName - Name of the stack to recover
|
|
753
|
-
* @param strategy - Recovery strategy ('resume' or 'rollback')
|
|
754
|
-
* @returns Promise resolving to recovery info, or undefined if no recovery needed
|
|
755
|
-
*/
|
|
756
|
-
async autoRecover(stackName, strategy) {
|
|
757
|
-
if (!(await this.hasIncompleteDeployment(stackName))) {
|
|
758
|
-
return { action: 'none' };
|
|
759
|
-
}
|
|
760
|
-
if (strategy === 'resume') {
|
|
761
|
-
const { changeSet, checkpoint, cloudDOM } = await this.resumeDeployment(stackName);
|
|
762
|
-
return {
|
|
763
|
-
action: 'resumed',
|
|
764
|
-
checkpoint,
|
|
765
|
-
changeSet,
|
|
766
|
-
cloudDOM,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
await this.rollback(stackName);
|
|
771
|
-
return { action: 'rolled_back' };
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
/**
|
|
775
|
-
* Detect and fix drift in deployed resources
|
|
776
|
-
*
|
|
777
|
-
* Checks if resources in the backend state still match reality.
|
|
778
|
-
* If drift is detected, refreshes the state to reflect actual cloud state.
|
|
779
|
-
*
|
|
780
|
-
* This is called automatically during state load to ensure state accuracy.
|
|
781
|
-
*
|
|
782
|
-
* @param stackName - Stack name to check for drift
|
|
783
|
-
* @param cloudProvider - Cloud provider with drift detection capabilities
|
|
784
|
-
* @returns Promise resolving to drift detection results
|
|
785
|
-
*/
|
|
786
|
-
async detectAndFixDrift(stackName, cloudProvider) {
|
|
787
|
-
const state = await this.getState(stackName);
|
|
788
|
-
if (!state?.cloudDOM) {
|
|
789
|
-
return { driftDetected: false, driftResults: [], resourcesFixed: 0 };
|
|
790
|
-
}
|
|
791
|
-
logger.debug(`Detecting drift for stack: ${stackName}`);
|
|
792
|
-
const driftResults = [];
|
|
793
|
-
let resourcesFixed = 0;
|
|
794
|
-
for (const node of state.cloudDOM) {
|
|
795
|
-
// Skip nodes without outputs (not yet deployed)
|
|
796
|
-
if (!node.outputs) {
|
|
797
|
-
continue;
|
|
798
|
-
}
|
|
799
|
-
// Detect drift (required method)
|
|
800
|
-
const result = await cloudProvider.detectDrift(node);
|
|
801
|
-
driftResults.push(result);
|
|
802
|
-
if (result.hasDrifted) {
|
|
803
|
-
logger.info(`Drift detected: ${node.id} - ${result.driftDescription || 'State mismatch'}`);
|
|
804
|
-
// Refresh state to fix drift (required method)
|
|
805
|
-
logger.debug(`Refreshing state for: ${node.id}`);
|
|
806
|
-
await cloudProvider.refreshState(node);
|
|
807
|
-
resourcesFixed++;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
// If any drift was detected, clear outputs for drifted resources and their children
|
|
811
|
-
// With the "one useInstance per component" constraint, dependencies = nesting
|
|
812
|
-
// So clearing a drifted node + its children ensures complete redeployment
|
|
813
|
-
if (resourcesFixed > 0) {
|
|
814
|
-
const driftedNodeIds = new Set(driftResults.filter(r => r.hasDrifted).map(r => r.nodeId));
|
|
815
|
-
logger.info(`Clearing outputs for ${driftedNodeIds.size} drifted resources and their children`);
|
|
816
|
-
// Clear outputs for drifted nodes and all their descendants
|
|
817
|
-
const clearDriftedOutputs = (nodes) => {
|
|
818
|
-
for (const node of nodes) {
|
|
819
|
-
const isDrifted = driftedNodeIds.has(node.id);
|
|
820
|
-
if (isDrifted && node.outputs) {
|
|
821
|
-
logger.debug(`Clearing outputs for drifted resource: ${node.id}`);
|
|
822
|
-
node.outputs = undefined;
|
|
823
|
-
}
|
|
824
|
-
// If this node is drifted, clear all its children too
|
|
825
|
-
if (node.children) {
|
|
826
|
-
if (isDrifted) {
|
|
827
|
-
// Clear all children of drifted nodes
|
|
828
|
-
const clearAllChildren = (childNodes) => {
|
|
829
|
-
for (const child of childNodes) {
|
|
830
|
-
if (child.outputs) {
|
|
831
|
-
logger.debug(`Clearing outputs for child of drifted resource: ${child.id}`);
|
|
832
|
-
child.outputs = undefined;
|
|
833
|
-
}
|
|
834
|
-
if (child.children) {
|
|
835
|
-
clearAllChildren(child.children);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
};
|
|
839
|
-
clearAllChildren(node.children);
|
|
840
|
-
}
|
|
841
|
-
else {
|
|
842
|
-
// Continue searching for drifted nodes in children
|
|
843
|
-
clearDriftedOutputs(node.children);
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
};
|
|
848
|
-
clearDriftedOutputs(state.cloudDOM);
|
|
849
|
-
}
|
|
850
|
-
// If drift was detected and state was refreshed, save updated state
|
|
851
|
-
const driftDetected = driftResults.some(r => r.hasDrifted);
|
|
852
|
-
if (driftDetected && resourcesFixed > 0) {
|
|
853
|
-
logger.info(`Saving refreshed state after fixing ${resourcesFixed} drifted resources`);
|
|
854
|
-
await this.withRetry(() => this.backendProvider.saveState(stackName, {
|
|
855
|
-
...state,
|
|
856
|
-
cloudDOM: state.cloudDOM,
|
|
857
|
-
timestamp: Date.now(),
|
|
858
|
-
}));
|
|
859
|
-
// Log drift detection to audit trail
|
|
860
|
-
await this.logAction(stackName, 'checkpoint', state);
|
|
861
|
-
}
|
|
862
|
-
if (driftDetected) {
|
|
863
|
-
logger.info(`Drift detection complete: ${driftResults.filter(r => r.hasDrifted).length} resources drifted, ${resourcesFixed} fixed`);
|
|
864
|
-
}
|
|
865
|
-
else {
|
|
866
|
-
logger.debug('No drift detected');
|
|
867
|
-
}
|
|
868
|
-
return { driftDetected, driftResults, resourcesFixed };
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
exports.StateMachine = StateMachine;
|
|
872
|
-
/**
|
|
873
|
-
* Valid state transitions
|
|
874
|
-
*
|
|
875
|
-
* Defines allowed transitions for state machine validation.
|
|
876
|
-
*/
|
|
877
|
-
StateMachine.VALID_TRANSITIONS = {
|
|
878
|
-
PENDING: ['APPLYING'],
|
|
879
|
-
APPLYING: ['DEPLOYED', 'FAILED', 'ROLLED_BACK'],
|
|
880
|
-
FAILED: ['ROLLED_BACK'],
|
|
881
|
-
DEPLOYED: [],
|
|
882
|
-
ROLLED_BACK: [],
|
|
883
|
-
};
|