@knpkv/codecommit-core 0.15.0 → 0.17.0
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 +7 -3
- package/dist/AwsClient/createApprovalRule.d.ts +1 -1
- package/dist/AwsClient/createApprovalRule.d.ts.map +1 -1
- package/dist/AwsClient/createPullRequest.d.ts +1 -1
- package/dist/AwsClient/createPullRequest.d.ts.map +1 -1
- package/dist/AwsClient/deleteApprovalRule.d.ts +1 -1
- package/dist/AwsClient/deleteApprovalRule.d.ts.map +1 -1
- package/dist/AwsClient/getCallerIdentity.d.ts +1 -1
- package/dist/AwsClient/getCallerIdentity.d.ts.map +1 -1
- package/dist/AwsClient/getCommentsForPullRequest.d.ts +1 -1
- package/dist/AwsClient/getCommentsForPullRequest.d.ts.map +1 -1
- package/dist/AwsClient/getDifferences.d.ts +1 -1
- package/dist/AwsClient/getDifferences.d.ts.map +1 -1
- package/dist/AwsClient/getPullRequest.d.ts +1 -1
- package/dist/AwsClient/getPullRequest.d.ts.map +1 -1
- package/dist/AwsClient/getPullRequests.d.ts +1 -1
- package/dist/AwsClient/getPullRequests.d.ts.map +1 -1
- package/dist/AwsClient/getPullRequests.js +1 -1
- package/dist/AwsClient/getPullRequests.js.map +1 -1
- package/dist/AwsClient/index.d.ts +1 -1
- package/dist/AwsClient/index.d.ts.map +1 -1
- package/dist/AwsClient/index.js +1 -1
- package/dist/AwsClient/index.js.map +1 -1
- package/dist/AwsClient/internal.d.ts +1 -1
- package/dist/AwsClient/internal.d.ts.map +1 -1
- package/dist/AwsClient/internal.js +1 -1
- package/dist/AwsClient/internal.js.map +1 -1
- package/dist/AwsClient/listBranches.d.ts +1 -1
- package/dist/AwsClient/listBranches.d.ts.map +1 -1
- package/dist/AwsClient/updateApprovalRule.d.ts +1 -1
- package/dist/AwsClient/updateApprovalRule.d.ts.map +1 -1
- package/dist/AwsClient/updatePullRequestDescription.d.ts +1 -1
- package/dist/AwsClient/updatePullRequestDescription.d.ts.map +1 -1
- package/dist/AwsClient/updatePullRequestTitle.d.ts +1 -1
- package/dist/AwsClient/updatePullRequestTitle.d.ts.map +1 -1
- package/dist/CacheService/Database.d.ts +4 -4
- package/dist/CacheService/Database.d.ts.map +1 -1
- package/dist/CacheService/Database.js +5 -3
- package/dist/CacheService/Database.js.map +1 -1
- package/dist/CacheService/migrations/0001_initial.d.ts +2 -2
- package/dist/CacheService/migrations/0001_initial.d.ts.map +1 -1
- package/dist/CacheService/migrations/0001_initial.js +1 -1
- package/dist/CacheService/migrations/0001_initial.js.map +1 -1
- package/dist/CacheService/migrations/0002_indexes.d.ts +2 -2
- package/dist/CacheService/migrations/0002_indexes.d.ts.map +1 -1
- package/dist/CacheService/migrations/0002_indexes.js +1 -1
- package/dist/CacheService/migrations/0002_indexes.js.map +1 -1
- package/dist/CacheService/migrations/0003_add_health_score.d.ts +2 -2
- package/dist/CacheService/migrations/0003_add_health_score.d.ts.map +1 -1
- package/dist/CacheService/migrations/0003_add_health_score.js +1 -1
- package/dist/CacheService/migrations/0003_add_health_score.js.map +1 -1
- package/dist/CacheService/migrations/0004_unify_notifications.d.ts +1 -1
- package/dist/CacheService/migrations/0004_unify_notifications.d.ts.map +1 -1
- package/dist/CacheService/migrations/0004_unify_notifications.js +1 -1
- package/dist/CacheService/migrations/0004_unify_notifications.js.map +1 -1
- package/dist/CacheService/migrations/0005_add_sandboxes.d.ts +2 -2
- package/dist/CacheService/migrations/0005_add_sandboxes.d.ts.map +1 -1
- package/dist/CacheService/migrations/0005_add_sandboxes.js +1 -1
- package/dist/CacheService/migrations/0005_add_sandboxes.js.map +1 -1
- package/dist/CacheService/migrations/0006_sandbox_logs.d.ts +2 -2
- package/dist/CacheService/migrations/0006_sandbox_logs.d.ts.map +1 -1
- package/dist/CacheService/migrations/0006_sandbox_logs.js +1 -1
- package/dist/CacheService/migrations/0006_sandbox_logs.js.map +1 -1
- package/dist/CacheService/migrations/0007_stats_columns.d.ts +1 -1
- package/dist/CacheService/migrations/0007_stats_columns.d.ts.map +1 -1
- package/dist/CacheService/migrations/0007_stats_columns.js +1 -1
- package/dist/CacheService/migrations/0007_stats_columns.js.map +1 -1
- package/dist/CacheService/migrations/0008_merged_by.d.ts +2 -2
- package/dist/CacheService/migrations/0008_merged_by.d.ts.map +1 -1
- package/dist/CacheService/migrations/0008_merged_by.js +1 -1
- package/dist/CacheService/migrations/0008_merged_by.js.map +1 -1
- package/dist/CacheService/migrations/0009_approved_by.d.ts +2 -2
- package/dist/CacheService/migrations/0009_approved_by.d.ts.map +1 -1
- package/dist/CacheService/migrations/0009_approved_by.js +1 -1
- package/dist/CacheService/migrations/0009_approved_by.js.map +1 -1
- package/dist/CacheService/migrations/0010_commented_by.d.ts +2 -2
- package/dist/CacheService/migrations/0010_commented_by.d.ts.map +1 -1
- package/dist/CacheService/migrations/0010_commented_by.js +1 -1
- package/dist/CacheService/migrations/0010_commented_by.js.map +1 -1
- package/dist/CacheService/migrations/0011_audit_log.d.ts +2 -2
- package/dist/CacheService/migrations/0011_audit_log.d.ts.map +1 -1
- package/dist/CacheService/migrations/0011_audit_log.js +1 -1
- package/dist/CacheService/migrations/0011_audit_log.js.map +1 -1
- package/dist/CacheService/migrations/0012_audit_log_indexes.d.ts +2 -2
- package/dist/CacheService/migrations/0012_audit_log_indexes.d.ts.map +1 -1
- package/dist/CacheService/migrations/0012_audit_log_indexes.js +1 -1
- package/dist/CacheService/migrations/0012_audit_log_indexes.js.map +1 -1
- package/dist/CacheService/migrations/0013_approval_rules.d.ts +2 -2
- package/dist/CacheService/migrations/0013_approval_rules.d.ts.map +1 -1
- package/dist/CacheService/migrations/0013_approval_rules.js +1 -1
- package/dist/CacheService/migrations/0013_approval_rules.js.map +1 -1
- package/dist/CacheService/migrations/0014_approved_by_arns.d.ts +2 -2
- package/dist/CacheService/migrations/0014_approved_by_arns.d.ts.map +1 -1
- package/dist/CacheService/migrations/0014_approved_by_arns.js +1 -1
- package/dist/CacheService/migrations/0014_approved_by_arns.js.map +1 -1
- package/dist/CacheService/migrations/0015_repo_account_id.d.ts +2 -2
- package/dist/CacheService/migrations/0015_repo_account_id.d.ts.map +1 -1
- package/dist/CacheService/migrations/0015_repo_account_id.js +1 -1
- package/dist/CacheService/migrations/0015_repo_account_id.js.map +1 -1
- package/dist/CacheService/migrations/0016_sandbox_access_password.d.ts +2 -2
- package/dist/CacheService/migrations/0016_sandbox_access_password.d.ts.map +1 -1
- package/dist/CacheService/migrations/0016_sandbox_access_password.js +1 -1
- package/dist/CacheService/migrations/0016_sandbox_access_password.js.map +1 -1
- package/dist/CacheService/migrations/0017_sandbox_region.d.ts +2 -2
- package/dist/CacheService/migrations/0017_sandbox_region.d.ts.map +1 -1
- package/dist/CacheService/migrations/0017_sandbox_region.js +1 -1
- package/dist/CacheService/migrations/0017_sandbox_region.js.map +1 -1
- package/dist/CacheService/migrations/0018_pull_request_coordinates.d.ts +2 -2
- package/dist/CacheService/migrations/0018_pull_request_coordinates.d.ts.map +1 -1
- package/dist/CacheService/migrations/0018_pull_request_coordinates.js +1 -1
- package/dist/CacheService/migrations/0018_pull_request_coordinates.js.map +1 -1
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.d.ts +2 -2
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.d.ts.map +1 -1
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.js +1 -1
- package/dist/CacheService/migrations/0019_dependent_pr_coordinates.js.map +1 -1
- package/dist/CacheService/migrations/0020_notification_coordinates.d.ts +1 -1
- package/dist/CacheService/migrations/0020_notification_coordinates.d.ts.map +1 -1
- package/dist/CacheService/migrations/0020_notification_coordinates.js +2 -2
- package/dist/CacheService/migrations/0020_notification_coordinates.js.map +1 -1
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.d.ts +6 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.d.ts.map +1 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.js +5 -0
- package/dist/CacheService/migrations/0021_sandbox_legacy_retirement.js.map +1 -0
- package/dist/CacheService/repos/CommentRepo.d.ts +2 -2
- package/dist/CacheService/repos/CommentRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/CommentRepo.js +2 -2
- package/dist/CacheService/repos/CommentRepo.js.map +1 -1
- package/dist/CacheService/repos/NotificationRepo.d.ts +2 -2
- package/dist/CacheService/repos/NotificationRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/NotificationRepo.js +2 -2
- package/dist/CacheService/repos/NotificationRepo.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/index.d.ts +2 -2
- package/dist/CacheService/repos/PullRequestRepo/index.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/index.js +1 -1
- package/dist/CacheService/repos/PullRequestRepo/index.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.d.ts +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.js +1 -1
- package/dist/CacheService/repos/PullRequestRepo/mutations.js.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.d.ts +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.d.ts.map +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.js +1 -1
- package/dist/CacheService/repos/PullRequestRepo/queries.js.map +1 -1
- package/dist/CacheService/repos/SandboxRepo.d.ts +18 -10
- package/dist/CacheService/repos/SandboxRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/SandboxRepo.js +4 -2
- package/dist/CacheService/repos/SandboxRepo.js.map +1 -1
- package/dist/CacheService/repos/StatsRepo/index.d.ts +2 -2
- package/dist/CacheService/repos/StatsRepo/index.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/index.js +1 -1
- package/dist/CacheService/repos/StatsRepo/index.js.map +1 -1
- package/dist/CacheService/repos/StatsRepo/internal.d.ts +4 -4
- package/dist/CacheService/repos/StatsRepo/internal.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/queries.d.ts +1 -1
- package/dist/CacheService/repos/StatsRepo/queries.d.ts.map +1 -1
- package/dist/CacheService/repos/StatsRepo/reviewerData.d.ts +1 -1
- package/dist/CacheService/repos/StatsRepo/reviewerData.d.ts.map +1 -1
- package/dist/CacheService/repos/SubscriptionRepo.d.ts +2 -2
- package/dist/CacheService/repos/SubscriptionRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/SubscriptionRepo.js +2 -2
- package/dist/CacheService/repos/SubscriptionRepo.js.map +1 -1
- package/dist/CacheService/repos/SyncMetadataRepo.d.ts +2 -2
- package/dist/CacheService/repos/SyncMetadataRepo.d.ts.map +1 -1
- package/dist/CacheService/repos/SyncMetadataRepo.js +2 -2
- package/dist/CacheService/repos/SyncMetadataRepo.js.map +1 -1
- package/dist/ConfigService/index.d.ts +1 -1
- package/dist/ConfigService/index.d.ts.map +1 -1
- package/dist/ConfigService/index.js +3 -3
- package/dist/ConfigService/index.js.map +1 -1
- package/dist/ConfigService/internal.d.ts +3 -3
- package/dist/ConfigService/load.d.ts +3 -3
- package/dist/ConfigService/reset.d.ts +3 -3
- package/dist/ConfigService/save.d.ts +3 -3
- package/dist/Domain.d.ts +23 -0
- package/dist/Domain.d.ts.map +1 -1
- package/dist/Domain.js +22 -1
- package/dist/Domain.js.map +1 -1
- package/dist/MockTransport.d.ts +2 -2
- package/dist/MockTransport.d.ts.map +1 -1
- package/dist/MockTransport.js +2 -2
- package/dist/MockTransport.js.map +1 -1
- package/dist/PRService/index.d.ts +1 -0
- package/dist/PRService/index.d.ts.map +1 -1
- package/dist/PRService/index.js +14 -2
- package/dist/PRService/index.js.map +1 -1
- package/dist/PRService/refresh.d.ts.map +1 -1
- package/dist/PRService/refresh.js +28 -9
- package/dist/PRService/refresh.js.map +1 -1
- package/dist/PRService/refreshHistory.d.ts.map +1 -1
- package/dist/PRService/refreshHistory.js +22 -4
- package/dist/PRService/refreshHistory.js.map +1 -1
- package/dist/PRService/refreshResolve.d.ts.map +1 -1
- package/dist/PRService/refreshResolve.js +7 -3
- package/dist/PRService/refreshResolve.js.map +1 -1
- package/dist/PRService/visibility.d.ts +51 -0
- package/dist/PRService/visibility.d.ts.map +1 -0
- package/dist/PRService/visibility.js +46 -0
- package/dist/PRService/visibility.js.map +1 -0
- package/dist/PermissionService/AuditLog.d.ts +1 -1
- package/dist/PermissionService/AuditLog.d.ts.map +1 -1
- package/dist/PermissionService/AuditLog.js +3 -3
- package/dist/PermissionService/AuditLog.js.map +1 -1
- package/dist/PermissionService/index.js +1 -1
- package/dist/ReadClient/ReadClient.d.ts +1 -1
- package/dist/ReadClient/ReadClient.d.ts.map +1 -1
- package/dist/ReadClient/ReadClient.js +1 -1
- package/dist/ReadClient/ReadProvider.d.ts +1 -1
- package/dist/ReadClient/ReadProvider.d.ts.map +1 -1
- package/dist/ReadClient/ReadProvider.js +1 -1
- package/dist/ReadClient/ReadProvider.js.map +1 -1
- package/dist/ReviewClient/ReviewClient.d.ts +1 -1
- package/dist/ReviewClient/ReviewClient.d.ts.map +1 -1
- package/dist/ReviewClient/ReviewProvider.d.ts +1 -1
- package/dist/ReviewClient/ReviewProvider.d.ts.map +1 -1
- package/dist/ReviewClient/ReviewProvider.js +1 -1
- package/dist/ReviewClient/ReviewProvider.js.map +1 -1
- package/dist/ReviewProfile.d.ts +14 -12
- package/dist/ReviewProfile.d.ts.map +1 -1
- package/dist/ReviewProfile.js +10 -3
- package/dist/ReviewProfile.js.map +1 -1
- package/dist/SandboxService/DockerService.d.ts +3 -1
- package/dist/SandboxService/DockerService.d.ts.map +1 -1
- package/dist/SandboxService/DockerService.js +14 -1
- package/dist/SandboxService/DockerService.js.map +1 -1
- package/dist/SandboxService/SandboxService.d.ts +6 -2
- package/dist/SandboxService/SandboxService.d.ts.map +1 -1
- package/dist/SandboxService/SandboxService.js +384 -89
- package/dist/SandboxService/SandboxService.js.map +1 -1
- package/dist/SandboxService/SandboxWorkerScope.d.ts +6 -1
- package/dist/SandboxService/SandboxWorkerScope.d.ts.map +1 -1
- package/dist/SandboxService/SandboxWorkerScope.js +6 -2
- package/dist/SandboxService/SandboxWorkerScope.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import { Clock, Config, Context, Crypto, Duration, Effect, Layer, Option, Predicate, Random, Ref, Result, Schedule, Stream } from "effect";
|
|
9
|
+
import { Clock, Config, Context, Crypto, Duration, Effect, Fiber, Layer, Option, Predicate, Random, Ref, Result, Schedule, Semaphore, Stream } from "effect";
|
|
10
10
|
import * as FileSystem from "effect/FileSystem";
|
|
11
|
-
import { ChildProcess } from "effect/
|
|
11
|
+
import { ChildProcess } from "effect/process";
|
|
12
12
|
import { SandboxRepo } from "../CacheService/repos/SandboxRepo.js";
|
|
13
13
|
import * as ChildEnv from "../ChildEnv.js";
|
|
14
14
|
import { ConfigService, defaultSandboxConfig, validateSandboxConfig } from "../ConfigService/index.js";
|
|
15
15
|
import { PullRequestId, RepositoryName, SandboxId } from "../Domain.js";
|
|
16
16
|
import { SandboxError } from "../Errors.js";
|
|
17
|
-
import { DockerService, isMissingContainerError } from "./DockerService.js";
|
|
17
|
+
import { DockerService, isAlreadyStoppedContainerError, isMissingContainerError } from "./DockerService.js";
|
|
18
18
|
import { PluginService } from "./PluginService.js";
|
|
19
19
|
import { SandboxWorkerScope } from "./SandboxWorkerScope.js";
|
|
20
20
|
/** Keep the container non-root while matching the bind-mounted workspace owner when available. */
|
|
@@ -28,7 +28,13 @@ export const sandboxRuntimeHome = "/tmp";
|
|
|
28
28
|
export const sandboxRuntimeXdgDataHome = `${sandboxRuntimeHome}/.local/share`;
|
|
29
29
|
const isRegionlessSandbox = (row) => row.region === undefined || row.region === null || row.region === "";
|
|
30
30
|
const isPreContainerSandboxStatus = (status) => status === "creating" || status === "cloning" || status === "starting";
|
|
31
|
-
const
|
|
31
|
+
const isTerminalSandboxStatus = (status) => status === "stopped" || status === "error";
|
|
32
|
+
const isConfirmedStoppedContainer = (state) => state === "exited" || state === "dead";
|
|
33
|
+
const isCompletedLegacyRetirement = (row) => row.accessPassword !== null && row.status === "stopped" && row.legacyRetiredAt !== null;
|
|
34
|
+
const isPendingLegacyRetirement = (row) => row.accessPassword !== null && row.status !== "stopped" && row.legacyRetiredAt !== null;
|
|
35
|
+
const isOrdinaryStoppingSandbox = (row) => row.status === "stopping" && row.legacyRetiredAt === null;
|
|
36
|
+
const isDiscoveredAwsAccountId = (value) => /^\d{12}$/u.test(value);
|
|
37
|
+
const homeDir = Config.String("HOME").pipe(Config.orElse(() => Config.String("USERPROFILE")));
|
|
32
38
|
const sandboxesDir = homeDir.pipe(Config.map((h) => `${h}/.codecommit/sandboxes`));
|
|
33
39
|
const expandHome = (p, home) => p.startsWith("~/") ? `${home}${p.slice(1)}` : p;
|
|
34
40
|
export const makeContainerConfig = (workspacePath, port, sandboxId, pullRequestId, sandboxConfig, homePath, containerUser, accessPassword) => ({
|
|
@@ -66,14 +72,101 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
66
72
|
const cryptoService = yield* Crypto.Crypto;
|
|
67
73
|
const homePath = yield* homeDir.pipe(Effect.orDie);
|
|
68
74
|
const basePath = yield* sandboxesDir.pipe(Effect.orDie);
|
|
69
|
-
const activeWorkerIds = yield* Ref.make(new
|
|
75
|
+
const activeWorkerIds = yield* Ref.make(new Map());
|
|
76
|
+
const lifecycleAdmission = yield* Semaphore.make(1);
|
|
77
|
+
const containerAdmission = yield* Semaphore.make(1);
|
|
78
|
+
const createAdmission = yield* Semaphore.make(1);
|
|
79
|
+
const stopRequestedIds = yield* Ref.make(new Set());
|
|
80
|
+
const stopOwnerIds = yield* Ref.make(new Set());
|
|
81
|
+
const retiredLegacyIds = yield* Ref.make(new Set());
|
|
82
|
+
const markWorkerActive = (id) => Ref.update(activeWorkerIds, (workers) => {
|
|
83
|
+
const next = new Map(workers);
|
|
84
|
+
next.set(id, (next.get(id) ?? 0) + 1);
|
|
85
|
+
return next;
|
|
86
|
+
});
|
|
87
|
+
const releaseWorker = (id) => Ref.modify(activeWorkerIds, (workers) => {
|
|
88
|
+
const next = new Map(workers);
|
|
89
|
+
const count = next.get(id) ?? 0;
|
|
90
|
+
if (count <= 1)
|
|
91
|
+
next.delete(id);
|
|
92
|
+
else
|
|
93
|
+
next.set(id, count - 1);
|
|
94
|
+
return [count <= 1, next];
|
|
95
|
+
});
|
|
96
|
+
const hasActiveWorker = (id) => Ref.get(activeWorkerIds).pipe(Effect.map((workers) => (workers.get(id) ?? 0) > 0));
|
|
97
|
+
const isStopRequested = (id) => Ref.get(stopRequestedIds).pipe(Effect.map((ids) => ids.has(id)));
|
|
98
|
+
const requestWorkerStops = (ids) => Ref.update(stopRequestedIds, (requested) => new Set([...requested, ...ids]));
|
|
99
|
+
const claimStopOwners = (ids) => Ref.update(stopOwnerIds, (owners) => new Set([...owners, ...ids]));
|
|
100
|
+
const isStopOwned = (id) => Ref.get(stopOwnerIds).pipe(Effect.map((ids) => ids.has(id)));
|
|
101
|
+
const clearWorkerStopRequest = (id) => Ref.update(stopRequestedIds, (ids) => {
|
|
102
|
+
if (!ids.has(id))
|
|
103
|
+
return ids;
|
|
104
|
+
const next = new Set(ids);
|
|
105
|
+
next.delete(id);
|
|
106
|
+
return next;
|
|
107
|
+
});
|
|
108
|
+
const findFallbackCandidates = (params) => !isDiscoveredAwsAccountId(params.awsAccountId)
|
|
109
|
+
? Effect.gen(function* () {
|
|
110
|
+
const candidates = (yield* repo.findAll()).filter((row) => isDiscoveredAwsAccountId(row.awsAccountId) &&
|
|
111
|
+
row.awsAccountId !== params.profile &&
|
|
112
|
+
row.pullRequestId === params.pullRequestId &&
|
|
113
|
+
row.repositoryName === params.repositoryName &&
|
|
114
|
+
(row.region === params.region || row.region === null || row.region === undefined || row.region === ""));
|
|
115
|
+
const terminalCandidates = [];
|
|
116
|
+
for (const candidate of candidates) {
|
|
117
|
+
if (!isTerminalSandboxStatus(candidate.status)) {
|
|
118
|
+
return [{ row: candidate, blocksCreation: true }];
|
|
119
|
+
}
|
|
120
|
+
const containers = yield* docker.listContainersByLabel("codecommit.sandbox.id", candidate.id);
|
|
121
|
+
if (containers.some((container) => !isConfirmedStoppedContainer(container.State))) {
|
|
122
|
+
return [{ row: candidate, blocksCreation: true }];
|
|
123
|
+
}
|
|
124
|
+
terminalCandidates.push(candidate);
|
|
125
|
+
}
|
|
126
|
+
return terminalCandidates.map((row) => ({ row, blocksCreation: false }));
|
|
127
|
+
})
|
|
128
|
+
: Effect.succeed([]);
|
|
70
129
|
const loadSandboxConfig = configService.load.pipe(Effect.map((config) => config.sandbox), Effect.catch(() => Effect.succeed(defaultSandboxConfig)));
|
|
71
130
|
const updateStatus = (id, status, extra) => repo.updateStatus(id, status, extra);
|
|
72
|
-
const
|
|
131
|
+
const updateWorkerStatus = (id, status, extra) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
132
|
+
if (yield* isStopRequested(String(id)))
|
|
133
|
+
return;
|
|
134
|
+
yield* updateStatus(id, status, extra);
|
|
135
|
+
}));
|
|
136
|
+
const releaseWorkerReservation = (id) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
137
|
+
const lastWorker = yield* releaseWorker(id);
|
|
138
|
+
if (lastWorker && !(yield* isStopOwned(id))) {
|
|
139
|
+
if (yield* isStopRequested(id)) {
|
|
140
|
+
yield* updateStatus(SandboxId.make(id), "stopped").pipe(Effect.catch((error) => Effect.logError(`Failed to finalize stopped sandbox ${id}`, error)));
|
|
141
|
+
}
|
|
142
|
+
yield* clearWorkerStopRequest(id);
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
const releaseStopOwner = (id, finalizeUnowned = false) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
146
|
+
const hadStopRequest = yield* isStopRequested(id);
|
|
147
|
+
yield* Ref.update(stopOwnerIds, (ids) => {
|
|
148
|
+
if (!ids.has(id))
|
|
149
|
+
return ids;
|
|
150
|
+
const next = new Set(ids);
|
|
151
|
+
next.delete(id);
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
if (!(yield* hasActiveWorker(id))) {
|
|
155
|
+
if (finalizeUnowned && hadStopRequest) {
|
|
156
|
+
yield* repo.findById(SandboxId.make(id)).pipe(Effect.flatMap((row) => (row.containerId === null || row.containerId.length === 0) && isPreContainerSandboxStatus(row.status)
|
|
157
|
+
? updateStatus(SandboxId.make(id), "stopped")
|
|
158
|
+
: Effect.void), Effect.catch((error) => Effect.logError(`Failed to finalize stopped sandbox ${id}`, error)));
|
|
159
|
+
}
|
|
160
|
+
yield* clearWorkerStopRequest(id);
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
const recordCreationFailure = (id, error) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
164
|
+
if (yield* isStopRequested(String(id)))
|
|
165
|
+
return;
|
|
73
166
|
yield* Effect.logError(`Sandbox ${id} creation failed`, error);
|
|
74
167
|
const errorDetail = Result.try(() => String(Predicate.isError(error) ? error.message : error)).pipe(Result.getOrElse(() => "Unknown error"));
|
|
75
168
|
yield* updateStatus(id, "error", { error: errorDetail.slice(0, 500) }).pipe(Effect.catch((statusError) => Effect.logError("Failed to update sandbox error status", statusError)), Effect.catchDefect((statusDefect) => Effect.logError("Defect while updating sandbox error status", statusDefect)));
|
|
76
|
-
});
|
|
169
|
+
}));
|
|
77
170
|
const progress = (id, detail) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => {
|
|
78
171
|
const ts = new Date(ms).toISOString();
|
|
79
172
|
return repo.updateDetail(id, detail).pipe(Effect.tap(() => repo.appendLog(id, `[${ts}] ${detail}`)), Effect.tap(() => Effect.logInfo(`Sandbox ${id}: ${detail}`)));
|
|
@@ -90,15 +183,90 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
90
183
|
sourceBranch: row.sourceBranch
|
|
91
184
|
}
|
|
92
185
|
});
|
|
186
|
+
const retireLegacySandbox = (legacy) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
187
|
+
const activeWorker = yield* hasActiveWorker(legacy.id);
|
|
188
|
+
if (activeWorker) {
|
|
189
|
+
return yield* new SandboxError({
|
|
190
|
+
sandboxId: SandboxId.make(legacy.id),
|
|
191
|
+
message: "Legacy sandbox is still active; retry after its worker exits"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", legacy.id);
|
|
195
|
+
const containerIds = new Set([
|
|
196
|
+
...(legacy.containerId === null || legacy.containerId.length === 0 ? [] : [legacy.containerId]),
|
|
197
|
+
...discovered.map((container) => container.Id)
|
|
198
|
+
]);
|
|
199
|
+
if (legacy.accessPassword === null) {
|
|
200
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
201
|
+
yield* updateStatus(SandboxId.make(legacy.id), "error", {
|
|
202
|
+
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
yield* markLegacyRetirementStarted(SandboxId.make(legacy.id));
|
|
207
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
208
|
+
yield* Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(SandboxId.make(legacy.id), "stopped", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
209
|
+
yield* Ref.update(retiredLegacyIds, (ids) => new Set(ids).add(legacy.id));
|
|
210
|
+
}));
|
|
211
|
+
const markLegacyRetired = (id) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(id, "stopped", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
212
|
+
const markLegacyRetirementStarted = (id) => Clock.currentTimeMillis.pipe(Effect.flatMap((ms) => updateStatus(id, "stopping", { legacyRetiredAt: new Date(ms).toISOString() })));
|
|
93
213
|
const service = {
|
|
94
214
|
create: (params) => Effect.gen(function* () {
|
|
215
|
+
yield* Effect.uninterruptible(createAdmission.take(1));
|
|
95
216
|
// Singleton check — one active sandbox per PR
|
|
96
217
|
const existing = yield* repo.findByPr(params.awsAccountId, params.pullRequestId, params.repositoryName, params.region);
|
|
97
|
-
const
|
|
218
|
+
const exactCandidate = Option.isSome(existing) && existing.value.region === params.region
|
|
98
219
|
? existing.value
|
|
99
220
|
: undefined;
|
|
100
|
-
const
|
|
101
|
-
|
|
221
|
+
const exactExisting = exactCandidate !== undefined &&
|
|
222
|
+
!isCompletedLegacyRetirement(exactCandidate) &&
|
|
223
|
+
!isPendingLegacyRetirement(exactCandidate)
|
|
224
|
+
? exactCandidate
|
|
225
|
+
: undefined;
|
|
226
|
+
const pendingExact = exactCandidate !== undefined && isPendingLegacyRetirement(exactCandidate)
|
|
227
|
+
? [exactCandidate]
|
|
228
|
+
: [];
|
|
229
|
+
const emptyAccountRows = params.awsAccountId.length === 0
|
|
230
|
+
? []
|
|
231
|
+
: (yield* repo.findAll()).filter((row) => row.awsAccountId === "" &&
|
|
232
|
+
row.pullRequestId === params.pullRequestId &&
|
|
233
|
+
row.repositoryName === params.repositoryName &&
|
|
234
|
+
row.region === params.region);
|
|
235
|
+
const profileRows = isDiscoveredAwsAccountId(params.awsAccountId) &&
|
|
236
|
+
!isDiscoveredAwsAccountId(params.profile) &&
|
|
237
|
+
params.profile !== params.awsAccountId
|
|
238
|
+
? (yield* repo.findAll()).filter((row) => row.awsAccountId === params.profile &&
|
|
239
|
+
row.pullRequestId === params.pullRequestId &&
|
|
240
|
+
row.repositoryName === params.repositoryName &&
|
|
241
|
+
row.region === params.region)
|
|
242
|
+
: [];
|
|
243
|
+
const effectiveExisting = yield* Effect.forEach([...emptyAccountRows, ...profileRows, ...pendingExact]
|
|
244
|
+
.filter((row) => !isCompletedLegacyRetirement(row)), retireLegacySandbox, { discard: true }).pipe(Effect.as(exactExisting));
|
|
245
|
+
const regionlessKeys = Array.from(new Set([
|
|
246
|
+
params.awsAccountId,
|
|
247
|
+
isDiscoveredAwsAccountId(params.awsAccountId)
|
|
248
|
+
? params.profile
|
|
249
|
+
: undefined
|
|
250
|
+
])).filter((key) => key !== undefined && key.length > 0);
|
|
251
|
+
const regionlessCandidates = (yield* Effect.forEach(regionlessKeys, (key) => repo.findRegionlessByPrAll(key, params.pullRequestId, params.repositoryName), { concurrency: 1 })).flat()
|
|
252
|
+
.filter((row) => regionlessKeys.includes(row.awsAccountId) && !isCompletedLegacyRetirement(row));
|
|
253
|
+
const regionless = regionlessCandidates.filter((row, index, candidates) => candidates.findIndex((candidate) => candidate.id === row.id) === index);
|
|
254
|
+
const ambiguousNumericProfile = isDiscoveredAwsAccountId(params.profile)
|
|
255
|
+
? regionless.find((row) => row.awsAccountId === params.profile)
|
|
256
|
+
: undefined;
|
|
257
|
+
if (ambiguousNumericProfile !== undefined) {
|
|
258
|
+
return yield* new SandboxError({
|
|
259
|
+
sandboxId: SandboxId.make(ambiguousNumericProfile.id),
|
|
260
|
+
message: "Configured numeric profile matches an existing regionless sandbox; account identity is ambiguous"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const legacyResults = yield* Effect.forEach(regionless, (legacy) => lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
264
|
+
if (legacy.accessPassword !== null && (yield* hasActiveWorker(legacy.id))) {
|
|
265
|
+
return yield* new SandboxError({
|
|
266
|
+
sandboxId: SandboxId.make(legacy.id),
|
|
267
|
+
message: "Regionless legacy sandbox is still active; retry after its worker exits"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
102
270
|
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", legacy.id);
|
|
103
271
|
const containerIds = new Set([
|
|
104
272
|
...(legacy.containerId === null || legacy.containerId.length === 0 ? [] : [legacy.containerId]),
|
|
@@ -110,17 +278,20 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
110
278
|
message: "Regionless legacy sandbox is still starting; retry after its worker reports a container"
|
|
111
279
|
});
|
|
112
280
|
}
|
|
281
|
+
if (legacy.accessPassword !== null) {
|
|
282
|
+
yield* markLegacyRetirementStarted(SandboxId.make(legacy.id));
|
|
283
|
+
}
|
|
113
284
|
if (legacy.accessPassword === null) {
|
|
114
|
-
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf(isMissingContainerError, () => Effect.void)), { discard: true });
|
|
285
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
115
286
|
yield* updateStatus(SandboxId.make(legacy.id), "error", {
|
|
116
287
|
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
117
288
|
});
|
|
118
289
|
return Option.some(legacy);
|
|
119
290
|
}
|
|
120
|
-
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf(isMissingContainerError, () => Effect.void)), { discard: true });
|
|
121
|
-
yield*
|
|
291
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
292
|
+
yield* markLegacyRetired(SandboxId.make(legacy.id));
|
|
122
293
|
return Option.none();
|
|
123
|
-
}), { concurrency: 1 });
|
|
294
|
+
})), { concurrency: 1 });
|
|
124
295
|
const unauthenticated = legacyResults.find(Option.isSome);
|
|
125
296
|
if (unauthenticated !== undefined && Option.isSome(unauthenticated)) {
|
|
126
297
|
return yield* new SandboxError({
|
|
@@ -128,17 +299,24 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
128
299
|
message: "Regionless legacy sandbox requires explicit cleanup before recreation"
|
|
129
300
|
});
|
|
130
301
|
}
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
302
|
+
if (exactCandidate !== undefined && isOrdinaryStoppingSandbox(exactCandidate)) {
|
|
303
|
+
return yield* new SandboxError({
|
|
304
|
+
sandboxId: SandboxId.make(exactCandidate.id),
|
|
305
|
+
message: "Sandbox is still stopping; retry after it has stopped"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (effectiveExisting !== undefined) {
|
|
309
|
+
if (effectiveExisting.containerId === null && isPreContainerSandboxStatus(effectiveExisting.status)) {
|
|
310
|
+
const activeWorker = yield* hasActiveWorker(effectiveExisting.id);
|
|
311
|
+
if (activeWorker) {
|
|
312
|
+
return effectiveExisting;
|
|
313
|
+
}
|
|
314
|
+
yield* updateStatus(SandboxId.make(effectiveExisting.id), "error", {
|
|
137
315
|
error: "Orphaned (no container)"
|
|
138
316
|
});
|
|
139
317
|
}
|
|
140
318
|
else {
|
|
141
|
-
return
|
|
319
|
+
return effectiveExisting;
|
|
142
320
|
}
|
|
143
321
|
}
|
|
144
322
|
const sandboxCfg = yield* loadSandboxConfig;
|
|
@@ -150,28 +328,17 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
150
328
|
const accessPassword = yield* cryptoService.randomUUIDv4;
|
|
151
329
|
const workspacePath = `${basePath}/${id}`;
|
|
152
330
|
const now = new Date(nowMs).toISOString();
|
|
153
|
-
yield*
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
awsAccountId: params.awsAccountId,
|
|
157
|
-
repositoryName: params.repositoryName,
|
|
158
|
-
region: params.region,
|
|
159
|
-
sourceBranch: params.sourceBranch,
|
|
160
|
-
accessPassword,
|
|
161
|
-
workspacePath,
|
|
162
|
-
status: "creating",
|
|
163
|
-
createdAt: now,
|
|
164
|
-
lastActivityAt: now
|
|
165
|
-
});
|
|
166
|
-
// Fork daemon for async lifecycle
|
|
167
|
-
yield* Ref.update(activeWorkerIds, (ids) => new Set(ids).add(String(id)));
|
|
168
|
-
yield* ownerScope.fork(Effect.gen(function* () {
|
|
331
|
+
const workerTransferred = yield* Ref.make(false);
|
|
332
|
+
const releaseWorkerReservationForCreate = () => releaseWorkerReservation(String(id));
|
|
333
|
+
const worker = Effect.gen(function* () {
|
|
169
334
|
const fs = yield* FileSystem.FileSystem;
|
|
170
335
|
const host = yield* ChildEnv.HostEnvironment;
|
|
171
336
|
const log = (detail) => progress(id, detail);
|
|
337
|
+
if (yield* isStopRequested(String(id)))
|
|
338
|
+
return;
|
|
172
339
|
yield* log("Sandbox config validated");
|
|
173
340
|
// Clone via HTTPS + AWS credential helper
|
|
174
|
-
yield*
|
|
341
|
+
yield* updateWorkerStatus(id, "cloning");
|
|
175
342
|
yield* fs.makeDirectory(workspacePath, { recursive: true });
|
|
176
343
|
const cloneUrl = `https://git-codecommit.${params.region}.amazonaws.com/v1/repos/${params.repositoryName}`;
|
|
177
344
|
const branch = params.sourceBranch.replace(/^refs\/heads\//, "");
|
|
@@ -212,12 +379,14 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
212
379
|
if (cloneResult.exitCode !== 0) {
|
|
213
380
|
const stderrText = cloneResult.stderrText;
|
|
214
381
|
yield* log(`Clone failed: ${stderrText}`);
|
|
215
|
-
yield*
|
|
382
|
+
yield* updateWorkerStatus(id, "error", {
|
|
216
383
|
error: stderrText || `git clone failed (exit ${cloneResult.exitCode})`
|
|
217
384
|
});
|
|
218
385
|
return;
|
|
219
386
|
}
|
|
220
387
|
yield* log("Clone complete");
|
|
388
|
+
if (yield* isStopRequested(String(id)))
|
|
389
|
+
return;
|
|
221
390
|
const workspaceInfo = yield* fs.stat(workspacePath);
|
|
222
391
|
const containerIdentity = sandboxContainerIdentityForWorkspaceOwner(Option.getOrUndefined(workspaceInfo.uid), Option.getOrUndefined(workspaceInfo.gid));
|
|
223
392
|
if (containerIdentity.repairRootOwnedWorkspace) {
|
|
@@ -231,22 +400,40 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
231
400
|
}
|
|
232
401
|
}
|
|
233
402
|
// Pull image
|
|
234
|
-
yield*
|
|
403
|
+
yield* updateWorkerStatus(id, "starting");
|
|
235
404
|
yield* log(`Pulling image ${sandboxCfg.image}`);
|
|
236
405
|
yield* docker.pullImage(sandboxCfg.image).pipe(Effect.tap(() => log("Image ready")), Effect.catchIf(() => true, () => log("Image pull skipped (using cached)")));
|
|
237
|
-
// Create + start container
|
|
238
|
-
|
|
239
|
-
yield*
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
406
|
+
// Create + start container. Stop and provisioning share this gate so a
|
|
407
|
+
// stop request cannot pass while a worker is between create and start.
|
|
408
|
+
const cid = yield* lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
409
|
+
if (yield* isStopRequested(String(id)))
|
|
410
|
+
return undefined;
|
|
411
|
+
yield* log("Creating container");
|
|
412
|
+
yield* validateSandboxConfig(sandboxCfg, homePath);
|
|
413
|
+
const containerConfig = makeContainerConfig(workspacePath, port, id, params.pullRequestId, sandboxCfg, homePath, containerIdentity.user, accessPassword);
|
|
414
|
+
const containerId = yield* docker.createContainer(containerConfig);
|
|
415
|
+
const nextCid = containerId.trim();
|
|
416
|
+
if (yield* isStopRequested(String(id))) {
|
|
417
|
+
yield* updateStatus(id, "stopping", { containerId: nextCid, port });
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
yield* log(`Container ${nextCid.slice(0, 12)} created, starting`);
|
|
421
|
+
yield* docker.startContainer(nextCid);
|
|
422
|
+
yield* updateStatus(id, "starting", { containerId: nextCid, port });
|
|
423
|
+
yield* log(`Container started on port ${port}`);
|
|
424
|
+
if (yield* isStopRequested(String(id)))
|
|
425
|
+
return undefined;
|
|
426
|
+
return nextCid;
|
|
427
|
+
}));
|
|
428
|
+
if (cid === undefined)
|
|
429
|
+
return;
|
|
430
|
+
if (yield* isStopRequested(String(id)))
|
|
431
|
+
return;
|
|
247
432
|
// Wait for code-server to be ready (poll health)
|
|
248
433
|
yield* log("Waiting for code-server health check");
|
|
249
434
|
yield* docker.exec(cid, ["curl", "-sf", "http://localhost:8080/healthz"]).pipe(Effect.retry(Schedule.max([Schedule.recurs(30), Schedule.spaced(Duration.seconds(1))])), Effect.tap(() => log("code-server ready")), Effect.tapError((e) => log(`Health check failed: ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void));
|
|
435
|
+
if (yield* isStopRequested(String(id)))
|
|
436
|
+
return;
|
|
250
437
|
// Run plugin hooks
|
|
251
438
|
const row = yield* repo.findById(id);
|
|
252
439
|
const ctx = makeSandboxContext(row);
|
|
@@ -263,36 +450,98 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
263
450
|
yield* log(`Running ${sandboxCfg.setupCommands.length} setup command(s)`);
|
|
264
451
|
yield* Effect.forEach(sandboxCfg.setupCommands, (cmd, i) => log(`[${i + 1}/${sandboxCfg.setupCommands.length}] ${cmd}`).pipe(Effect.andThen(docker.exec(cid, ["sh", "-c", cmd])), Effect.tap((output) => log(`Command done: ${cmd.slice(0, 60)}${output.length > 0 ? `\n${output.trim()}` : ""}`)), Effect.tapError((e) => log(`Command failed: ${cmd.slice(0, 60)} — ${String(e)}`)), Effect.catchIf(() => true, () => Effect.void)), { discard: true });
|
|
265
452
|
}
|
|
266
|
-
yield*
|
|
453
|
+
yield* updateWorkerStatus(id, "running");
|
|
267
454
|
yield* log("Sandbox ready");
|
|
268
455
|
}).pipe(Effect.catch((error) => recordCreationFailure(id, error)),
|
|
269
456
|
// Observe and persist unexpected defects without recovering them.
|
|
270
457
|
// `tapDefect` leaves the original Cause / Exit unchanged.
|
|
271
|
-
Effect.tapDefect((defect) => recordCreationFailure(id, defect))
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
458
|
+
Effect.tapDefect((defect) => recordCreationFailure(id, defect)));
|
|
459
|
+
yield* Effect.acquireUseRelease(lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
460
|
+
const fallbackCandidates = yield* findFallbackCandidates(params);
|
|
461
|
+
const blockingCandidate = fallbackCandidates.find((candidate) => candidate.blocksCreation);
|
|
462
|
+
if (blockingCandidate !== undefined) {
|
|
463
|
+
return yield* new SandboxError({
|
|
464
|
+
sandboxId: SandboxId.make(blockingCandidate.row.id),
|
|
465
|
+
message: "AWS account identity is unavailable; retry after account discovery"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
const fallbackStopGuards = fallbackCandidates.map((candidate) => candidate.row.id);
|
|
469
|
+
yield* claimStopOwners(fallbackStopGuards);
|
|
470
|
+
yield* requestWorkerStops(fallbackStopGuards);
|
|
471
|
+
return fallbackStopGuards;
|
|
472
|
+
})), () => Effect.gen(function* () {
|
|
473
|
+
yield* markWorkerActive(String(id));
|
|
474
|
+
yield* repo.insert({
|
|
475
|
+
id,
|
|
476
|
+
pullRequestId: params.pullRequestId,
|
|
477
|
+
awsAccountId: params.awsAccountId,
|
|
478
|
+
repositoryName: params.repositoryName,
|
|
479
|
+
region: params.region,
|
|
480
|
+
sourceBranch: params.sourceBranch,
|
|
481
|
+
accessPassword,
|
|
482
|
+
workspacePath,
|
|
483
|
+
status: "creating",
|
|
484
|
+
createdAt: now,
|
|
485
|
+
lastActivityAt: now
|
|
486
|
+
}).pipe(Effect.andThen(Effect.uninterruptible(ownerScope.fork(worker, releaseWorkerReservationForCreate()).pipe(Effect.flatMap(({ fiber, started }) => Effect.race(started.pipe(Effect.as(true)), Fiber.await(fiber).pipe(Effect.as(false)))), Effect.flatMap((started) => started ? Ref.set(workerTransferred, true) : Effect.void)))), Effect.ensuring(Ref.get(workerTransferred).pipe(Effect.flatMap((transferred) => transferred ? Effect.void : releaseWorkerReservationForCreate()))));
|
|
487
|
+
}), (fallbackStopGuards) => Effect.forEach(fallbackStopGuards, (guardId) => releaseStopOwner(guardId), { discard: true }));
|
|
276
488
|
return yield* repo.findById(id);
|
|
277
|
-
}).pipe(Effect.mapError((cause) => Predicate.isTagged(cause, "SandboxError")
|
|
489
|
+
}).pipe(Effect.ensuring(createAdmission.release(1)), Effect.mapError((cause) => Predicate.isTagged(cause, "SandboxError")
|
|
278
490
|
? cause
|
|
279
491
|
: new SandboxError({ message: "Failed to create sandbox", cause }))),
|
|
280
492
|
get: (id) => repo.findById(id).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Sandbox not found", cause }))),
|
|
281
493
|
list: () => repo.findActive(),
|
|
282
494
|
listAll: () => repo.findAll(),
|
|
283
|
-
stop: (id) => Effect.
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
495
|
+
stop: (id) => Effect.uninterruptibleMask((restore) => {
|
|
496
|
+
const stopAdmissionAcquired = Effect.gen(function* () {
|
|
497
|
+
const acquired = yield* Ref.make(false);
|
|
498
|
+
return acquired;
|
|
499
|
+
});
|
|
500
|
+
const stopSandbox = lifecycleAdmission.withPermits(1)(Effect.gen(function* () {
|
|
501
|
+
const row = yield* repo.findById(id);
|
|
502
|
+
yield* updateStatus(id, "stopping");
|
|
503
|
+
if (row.containerId !== null) {
|
|
504
|
+
const containerId = row.containerId;
|
|
505
|
+
const ctx = makeSandboxContext(row);
|
|
506
|
+
yield* plugins.executeHook("onSandboxDestroy", ctx);
|
|
507
|
+
yield* containerAdmission.withPermits(1)(Effect.gen(function* () {
|
|
508
|
+
const stop = row.legacyRetiredAt === null
|
|
509
|
+
? docker.stopContainer(containerId).pipe(Effect.catchIf(() => true, () => Effect.void))
|
|
510
|
+
: docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void));
|
|
511
|
+
yield* stop;
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
yield* updateStatus(id, "stopped");
|
|
515
|
+
yield* Effect.logInfo(`Sandbox ${id} stopped`);
|
|
516
|
+
}));
|
|
517
|
+
return Effect.flatMap(stopAdmissionAcquired, (admissionAcquired) => Effect.ensuring(Effect.gen(function* () {
|
|
518
|
+
// Publish the stop intent before waiting for create admission.
|
|
519
|
+
// A provisioning worker may hold that permit while it inserts its row.
|
|
520
|
+
yield* claimStopOwners([String(id)]);
|
|
521
|
+
yield* requestWorkerStops([String(id)]);
|
|
522
|
+
yield* restore(createAdmission.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
523
|
+
yield* Ref.set(admissionAcquired, true);
|
|
524
|
+
yield* stopSandbox;
|
|
525
|
+
}))));
|
|
526
|
+
}), Ref.get(admissionAcquired).pipe(Effect.flatMap((acquired) => releaseStopOwner(String(id), !acquired)))));
|
|
293
527
|
}).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to stop sandbox", cause }))),
|
|
294
|
-
restart: (id) => Effect.gen(function* () {
|
|
528
|
+
restart: (id) => Effect.uninterruptibleMask((restore) => Effect.acquireUseRelease(Effect.gen(function* () {
|
|
529
|
+
yield* lifecycleAdmission.take(1);
|
|
530
|
+
return {
|
|
531
|
+
permitReleased: yield* Ref.make(false),
|
|
532
|
+
workerMarked: yield* Ref.make(false)
|
|
533
|
+
};
|
|
534
|
+
}), ({ permitReleased, workerMarked }) => Effect.gen(function* () {
|
|
295
535
|
const row = yield* repo.findById(id);
|
|
536
|
+
if (yield* isStopRequested(String(id)))
|
|
537
|
+
return;
|
|
538
|
+
const wasRetired = yield* Ref.get(retiredLegacyIds).pipe(Effect.map((ids) => ids.has(String(id))));
|
|
539
|
+
if (wasRetired || row.legacyRetiredAt !== null) {
|
|
540
|
+
return yield* new SandboxError({
|
|
541
|
+
sandboxId: id,
|
|
542
|
+
message: "Legacy sandbox was retired; create a replacement"
|
|
543
|
+
});
|
|
544
|
+
}
|
|
296
545
|
if (row.accessPassword === null) {
|
|
297
546
|
return yield* new SandboxError({
|
|
298
547
|
sandboxId: id,
|
|
@@ -302,18 +551,47 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
302
551
|
if (row.containerId === null) {
|
|
303
552
|
return yield* new SandboxError({ sandboxId: id, message: "No container to restart" });
|
|
304
553
|
}
|
|
305
|
-
|
|
306
|
-
yield*
|
|
307
|
-
yield*
|
|
308
|
-
yield*
|
|
309
|
-
yield*
|
|
310
|
-
yield*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
554
|
+
const containerId = row.containerId;
|
|
555
|
+
yield* markWorkerActive(String(id));
|
|
556
|
+
yield* Ref.set(workerMarked, true);
|
|
557
|
+
yield* lifecycleAdmission.release(1);
|
|
558
|
+
yield* Ref.set(permitReleased, true);
|
|
559
|
+
yield* restore(Effect.gen(function* () {
|
|
560
|
+
if (yield* isStopRequested(String(id)))
|
|
561
|
+
return;
|
|
562
|
+
yield* updateWorkerStatus(id, "starting");
|
|
563
|
+
if (yield* isStopRequested(String(id)))
|
|
564
|
+
return;
|
|
565
|
+
yield* progress(id, "Restarting container");
|
|
566
|
+
const started = yield* containerAdmission.withPermits(1)(Effect.gen(function* () {
|
|
567
|
+
if (yield* isStopRequested(String(id)))
|
|
568
|
+
return false;
|
|
569
|
+
yield* docker.startContainer(containerId);
|
|
570
|
+
yield* updateStatus(id, "starting", row.port !== null ? { port: row.port } : {});
|
|
571
|
+
return true;
|
|
572
|
+
}));
|
|
573
|
+
if (!started || (yield* isStopRequested(String(id))))
|
|
574
|
+
return;
|
|
575
|
+
yield* progress(id, "Waiting for code-server health check");
|
|
576
|
+
yield* docker.exec(containerId, ["curl", "-sf", "http://localhost:8080/healthz"]).pipe(Effect.retry(Schedule.max([Schedule.recurs(30), Schedule.spaced(Duration.seconds(1))])), Effect.tap(() => progress(id, "code-server ready")), Effect.catchIf(() => true, () => Effect.void));
|
|
577
|
+
if (yield* isStopRequested(String(id)))
|
|
578
|
+
return;
|
|
579
|
+
const ctx = makeSandboxContext(yield* repo.findById(id));
|
|
580
|
+
yield* plugins.executeHook("onSandboxReady", ctx);
|
|
581
|
+
if (yield* isStopRequested(String(id)))
|
|
582
|
+
return;
|
|
583
|
+
yield* updateWorkerStatus(id, "running");
|
|
584
|
+
if (yield* isStopRequested(String(id)))
|
|
585
|
+
return;
|
|
586
|
+
yield* progress(id, "Sandbox restarted");
|
|
587
|
+
yield* Effect.logInfo(`Sandbox ${id} restarted`);
|
|
588
|
+
}));
|
|
589
|
+
}), ({ permitReleased, workerMarked }) => Effect.gen(function* () {
|
|
590
|
+
if (!(yield* Ref.getAndSet(permitReleased, true)))
|
|
591
|
+
yield* lifecycleAdmission.release(1);
|
|
592
|
+
if (yield* Ref.getAndSet(workerMarked, false))
|
|
593
|
+
yield* releaseWorkerReservation(String(id));
|
|
594
|
+
}))).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to restart sandbox", cause }))),
|
|
317
595
|
cleanup: (id) => Effect.gen(function* () {
|
|
318
596
|
const row = yield* repo.findById(id);
|
|
319
597
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -325,10 +603,13 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
325
603
|
yield* Effect.logInfo(`Sandbox ${id} cleaned up`);
|
|
326
604
|
}).pipe(Effect.mapError((cause) => new SandboxError({ sandboxId: id, message: "Failed to cleanup sandbox", cause }))),
|
|
327
605
|
reconcile: () => Effect.gen(function* () {
|
|
606
|
+
yield* Effect.uninterruptible(lifecycleAdmission.take(1));
|
|
328
607
|
const active = yield* repo.findActive();
|
|
329
608
|
const all = yield* repo.findAll();
|
|
330
609
|
const activeIds = new Set(active.map((row) => row.id));
|
|
331
|
-
const rows = all.filter((row) =>
|
|
610
|
+
const rows = all.filter((row) => !isCompletedLegacyRetirement(row) &&
|
|
611
|
+
(row.accessPassword === null || activeIds.has(row.id) || isRegionlessSandbox(row) ||
|
|
612
|
+
(row.legacyRetiredAt !== null && row.status !== "stopped")));
|
|
332
613
|
yield* Effect.forEach(rows, (row) => Effect.gen(function* () {
|
|
333
614
|
if (isRegionlessSandbox(row)) {
|
|
334
615
|
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
@@ -339,24 +620,38 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
339
620
|
if (row.accessPassword !== null &&
|
|
340
621
|
containerIds.size === 0 &&
|
|
341
622
|
isPreContainerSandboxStatus(row.status)) {
|
|
342
|
-
const activeWorker = yield*
|
|
623
|
+
const activeWorker = yield* hasActiveWorker(row.id);
|
|
343
624
|
if (activeWorker)
|
|
344
625
|
return;
|
|
345
626
|
yield* updateStatus(SandboxId.make(row.id), "error", { error: "Orphaned (no container)" });
|
|
346
627
|
return;
|
|
347
628
|
}
|
|
348
629
|
if (row.accessPassword === null) {
|
|
349
|
-
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf(isMissingContainerError, () => Effect.void)), { discard: true });
|
|
630
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
350
631
|
yield* updateStatus(SandboxId.make(row.id), "error", {
|
|
351
632
|
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
352
633
|
});
|
|
353
634
|
return;
|
|
354
635
|
}
|
|
355
|
-
yield*
|
|
356
|
-
|
|
636
|
+
if (yield* hasActiveWorker(row.id)) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
yield* markLegacyRetirementStarted(SandboxId.make(row.id));
|
|
640
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
641
|
+
yield* markLegacyRetired(SandboxId.make(row.id));
|
|
357
642
|
yield* Effect.logInfo(`Reconciled regionless sandbox ${row.id}`);
|
|
358
643
|
return;
|
|
359
644
|
}
|
|
645
|
+
if (row.accessPassword !== null && row.legacyRetiredAt !== null) {
|
|
646
|
+
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
647
|
+
const containerIds = new Set([
|
|
648
|
+
...(row.containerId === null || row.containerId.length === 0 ? [] : [row.containerId]),
|
|
649
|
+
...discovered.map((container) => container.Id)
|
|
650
|
+
]);
|
|
651
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.inspectContainer(containerId).pipe(Effect.flatMap((info) => info.State.Running ? docker.stopContainer(containerId) : Effect.void), Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
652
|
+
yield* updateStatus(SandboxId.make(row.id), "stopped", { legacyRetiredAt: row.legacyRetiredAt });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
360
655
|
if (row.accessPassword === null) {
|
|
361
656
|
const discovered = yield* docker.listContainersByLabel("codecommit.sandbox.id", row.id);
|
|
362
657
|
const containerIds = new Set([
|
|
@@ -365,7 +660,7 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
365
660
|
]);
|
|
366
661
|
// Do not consider any legacy row reconciled until every persisted
|
|
367
662
|
// or labeled passwordless container has confirmed shutdown.
|
|
368
|
-
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf(isMissingContainerError, () => Effect.void)), { discard: true });
|
|
663
|
+
yield* Effect.forEach(containerIds, (containerId) => docker.stopContainer(containerId).pipe(Effect.catchIf((error) => isMissingContainerError(error) || isAlreadyStoppedContainerError(error), () => Effect.void)), { discard: true });
|
|
369
664
|
yield* updateStatus(SandboxId.make(row.id), "error", {
|
|
370
665
|
error: "Legacy unauthenticated sandbox stopped; delete and recreate it"
|
|
371
666
|
});
|
|
@@ -373,7 +668,7 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
373
668
|
}
|
|
374
669
|
if (row.containerId === null) {
|
|
375
670
|
if (isPreContainerSandboxStatus(row.status)) {
|
|
376
|
-
const activeWorker = yield*
|
|
671
|
+
const activeWorker = yield* hasActiveWorker(row.id);
|
|
377
672
|
if (activeWorker)
|
|
378
673
|
return;
|
|
379
674
|
}
|
|
@@ -386,8 +681,8 @@ const makeSandboxService = Effect.gen(function* () {
|
|
|
386
681
|
yield* Effect.logInfo(`Reconciled orphaned sandbox ${row.id}`);
|
|
387
682
|
}
|
|
388
683
|
}), { discard: true });
|
|
389
|
-
}).pipe(Effect.as(true), Effect.catch((cause) => Effect.logWarning("Sandbox reconcile failed", cause).pipe(Effect.as(false)))),
|
|
390
|
-
hasLegacyUnauthenticated: () => repo.findAll().pipe(Effect.map((rows) => rows.some((row) => row.accessPassword === null || isRegionlessSandbox(row)))),
|
|
684
|
+
}).pipe(Effect.as(true), Effect.catch((cause) => Effect.logWarning("Sandbox reconcile failed", cause).pipe(Effect.as(false))), Effect.ensuring(lifecycleAdmission.release(1))),
|
|
685
|
+
hasLegacyUnauthenticated: () => repo.findAll().pipe(Effect.map((rows) => rows.some((row) => row.accessPassword === null || (isRegionlessSandbox(row) && !isCompletedLegacyRetirement(row))))),
|
|
391
686
|
gcIdle: (idleTimeout = Duration.minutes(30), cleanupDelay = Duration.hours(24)) => Effect.gen(function* () {
|
|
392
687
|
const all = yield* repo.findAll();
|
|
393
688
|
const now = yield* Clock.currentTimeMillis;
|