opencode-ruby-upgrader 0.1.8 → 0.1.10

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.
@@ -0,0 +1,129 @@
1
+ // Service adapters for isolated target runtime.
2
+ //
3
+ // Each adapter defines container lifecycle (image, start args, readiness probe)
4
+ // and the environment variables the app container needs to reach the service
5
+ // over the per-run private Docker network. Only allowlisted adapter images are
6
+ // permitted; no user-supplied images or environment values.
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ const NAME = /^[a-z0-9][a-z0-9-]{0,127}$/;
12
+
13
+ function waitSecond() { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000); }
14
+
15
+ function readyLoop({ probe, attempts, label }) {
16
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
17
+ if (probe().status === 0) return;
18
+ waitSecond();
19
+ }
20
+ throw new Error(`Target ${label} did not become ready within ${attempts} seconds. Rerun prepare-target-runtime --ruby <x.y.z>.`);
21
+ }
22
+
23
+ export const services = Object.freeze({
24
+ redis: Object.freeze({
25
+ type: "redis",
26
+ label: "Redis",
27
+ image: "redis:7-alpine",
28
+ port: 6379,
29
+ // Start Redis in the foreground with default config; no authentication in
30
+ // the isolated test network.
31
+ startArgs: () => ["redis:7-alpine", "redis-server", "--appendonly", "no"],
32
+ // Probe readiness with redis-cli against localhost.
33
+ readyArgs: (container) => ["exec", container, "redis-cli", "ping"],
34
+ // Expose REDIS_URL so the app reaches Redis over the private network by
35
+ // container name. Use DB 0 for test isolation by convention.
36
+ env: ({ container }) => ({ REDIS_URL: `redis://${container}:6379/0` }),
37
+ ensureReady: ({ probe }) => readyLoop({ probe, attempts: 30, label: "Redis" })
38
+ }),
39
+ chrome: Object.freeze({
40
+ type: "chrome",
41
+ label: "Chrome",
42
+ image: "selenium/standalone-chrome:latest",
43
+ port: 4444,
44
+ startArgs: () => ["--shm-size=2g", "selenium/standalone-chrome:latest"],
45
+ readyArgs: (container) => ["exec", container, "curl", "-s", "--fail", "http://localhost:4444/wd/hub/status"],
46
+ env: ({ container }) => ({
47
+ SELENIUM_URL: "http://chrome:4444/wd/hub",
48
+ CHROME_REMOTE_URL: "http://chrome:4444",
49
+ CAPYBARA_SERVER_HOST: "0.0.0.0"
50
+ }),
51
+ ensureReady: ({ probe }) => readyLoop({ probe, attempts: 120, label: "Chrome" })
52
+ }),
53
+ selenium: Object.freeze({
54
+ type: "selenium",
55
+ label: "Selenium",
56
+ image: "selenium/standalone-chrome:latest",
57
+ port: 4444,
58
+ startArgs: () => ["--shm-size=2g", "selenium/standalone-chrome:latest"],
59
+ readyArgs: (container) => ["exec", container, "curl", "-s", "--fail", "http://localhost:4444/wd/hub/status"],
60
+ env: ({ container }) => ({
61
+ SELENIUM_URL: "http://selenium:4444/wd/hub",
62
+ CHROME_REMOTE_URL: "http://selenium:4444",
63
+ CAPYBARA_SERVER_HOST: "0.0.0.0"
64
+ }),
65
+ ensureReady: ({ probe }) => readyLoop({ probe, attempts: 120, label: "Selenium" })
66
+ })
67
+ });
68
+
69
+ export function resolveService(type) {
70
+ if (!type || !Object.hasOwn(services, type)) return undefined;
71
+ return services[type];
72
+ }
73
+
74
+ export function resolveServices(types = []) {
75
+ const result = [];
76
+ const seen = new Set();
77
+ for (const type of types) {
78
+ if (seen.has(type)) continue;
79
+ const svc = resolveService(type);
80
+ if (svc) { result.push({ type, adapter: svc }); seen.add(type); }
81
+ }
82
+ return result;
83
+ }
84
+
85
+ const REDIS_EVIDENCE = [
86
+ /\bgem\s*\(?\s*["']redis["']/i,
87
+ /\bgem\s*\(?\s*["']sidekiq["']/i,
88
+ /\bgem\s*\(?\s*["']resque["']/i,
89
+ /\bgem\s*\(?\s*["']redis-rails["']/i,
90
+ /\bredis\.yml\b/i,
91
+ /\bRedis\.new\b/i,
92
+ /\bSidekiq\.configure_server\b/i,
93
+ /\bREDIS_URL\b/i
94
+ ];
95
+ const REDIS_LOCK = /^[ \t]*redis \(/m;
96
+ const REDIS_LOCK_SIDEKIQ = /^[ \t]*sidekiq \(/m;
97
+ const REDIS_LOCK_RESQUE = /^[ \t]*resque \(/m;
98
+
99
+ export function detectServices(root = process.cwd()) {
100
+ const read = (file) => {
101
+ try { return fs.readFileSync(path.join(root, file), "utf8"); } catch { return ""; }
102
+ };
103
+ const uncommented = (contents) => contents.replace(/#.*$/gm, "");
104
+ const contents = [
105
+ "config/database.yml",
106
+ "Gemfile",
107
+ "Gemfile.lock",
108
+ "config/application.rb",
109
+ "config/environments/test.rb",
110
+ "config/environments/development.rb",
111
+ "config/initializers/sidekiq.rb",
112
+ "config/sidekiq.yml"
113
+ ].map(read).join("\n");
114
+ const text = uncommented(contents);
115
+ const lock = `${uncommented(read("Gemfile.lock"))}\n`;
116
+ const services = [];
117
+ const hasRedis = REDIS_EVIDENCE.some((re) => re.test(text)) ||
118
+ REDIS_LOCK.test(lock) ||
119
+ REDIS_LOCK_SIDEKIQ.test(lock) ||
120
+ REDIS_LOCK_RESQUE.test(lock);
121
+ if (hasRedis) services.push("redis");
122
+ const hasChrome = /driven_by\s*:\s*(?:selenium|cuprite|chrome)/i.test(text) ||
123
+ /selenium-webdriver|cuprite|capybara/i.test(text + lock);
124
+ if (hasChrome) services.push("chrome");
125
+ // De-duplicate while preserving order
126
+ return [...new Set(services)];
127
+ }
128
+
129
+ export { NAME };
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { RunStateError, readRun } from "./run-state.js";
6
6
  import { NAME, databases, detectDatabase, resolveDatabase } from "./databases.js";
7
+ import { services as serviceAdapters, detectServices, resolveServices } from "./services.js";
7
8
 
8
9
  const RUNTIME_FILE = "runtime.json";
9
10
  const LABEL = "io.opencode-ruby-upgrader.run-id";
@@ -46,27 +47,36 @@ function preparationResult(result) {
46
47
  return { sha256: crypto.createHash("sha256").update(output).digest("hex"), bytes: Buffer.byteLength(output) };
47
48
  }
48
49
  function bootstrap(spawn, runtime, db, rails) {
49
- const labels = ["Node.js setup", "Bundler installation", "dependency installation", ...(rails ? [`${db.label} test database initialization`] : []), "Ruby version attestation"];
50
+ const labels = ["Node.js setup", "Bundler installation", "dependency installation", ...(rails ? [`${db.label} test database initialization`] : []), "Ruby version attestation", "Bundler version attestation"];
50
51
  const commands = [
51
52
  ["exec", runtime.appContainer, "sh", "-c", NODE_INSTALL],
52
- ["exec", runtime.appContainer, "gem", "install", "bundler", "-v", "2.4.22", "--no-document"],
53
- ["exec", runtime.appContainer, "bundle", "_2.4.22_", "install"],
53
+ ["exec", runtime.appContainer, "gem", "install", "bundler", "-v", runtime.bundlerToInstall ?? "2.4.22", "--no-document"],
54
+ ["exec", runtime.appContainer, "bundle", `_${runtime.bundlerToInstall ?? "2.4.22"}_`, "install"],
54
55
  ...(rails ? [db.createArgs({ appContainer: runtime.appContainer, databaseContainer: runtime.databaseContainer })] : []),
55
- ["exec", runtime.appContainer, "ruby", "--version"]
56
+ ["exec", runtime.appContainer, "ruby", "--version"],
57
+ // Attest the Bundler version actually installed, so the report can warn when
58
+ // CI/host bundler differs. `gem install` output is advisory; `--version` is
59
+ // authoritative for what `bundle exec` will actually resolve.
60
+ ["exec", runtime.appContainer, "bundle", `_${runtime.bundlerToInstall ?? "2.4.22"}_`, "--version"]
56
61
  ];
57
62
  const results = commands.map((args, index) => {
58
63
  const result = docker(spawn, args);
59
64
  if (result.status !== 0) throw new Error(`Target runtime bootstrap failed during ${labels[index]}. Rerun prepare-target-runtime --ruby <x.y.z>.`);
60
65
  return result;
61
66
  });
62
- const rubyOutput = `${results.at(-1).stdout ?? ""}${results.at(-1).stderr ?? ""}`;
67
+ // Index the attestations explicitly rather than from the end of the array:
68
+ // `ruby --version` is no longer the final command, and an off-by-one here would
69
+ // validate the Bundler banner against the Ruby regex.
70
+ const rubyIndex = 3 + (rails ? 1 : 0);
71
+ const rubyOutput = `${results[rubyIndex].stdout ?? ""}${results[rubyIndex].stderr ?? ""}`;
63
72
  if (!new RegExp(`^ruby ${runtime.ruby.replaceAll(".", "\\.")}(?:p\\d+|\\s|$)`).test(rubyOutput.trim())) throw new Error("Target runtime did not execute the requested Ruby version.");
64
73
  return {
65
74
  node: preparationResult(results[0]),
66
75
  bundler: preparationResult(results[1]),
67
76
  bundleInstall: preparationResult(results[2]),
68
77
  ...(rails ? { databaseCreate: preparationResult(results[3]) } : {}),
69
- rubyVersion: preparationResult(results.at(-1))
78
+ rubyVersion: preparationResult(results[rubyIndex]),
79
+ bundlerVersion: `${results.at(-1).stdout ?? ""}${results.at(-1).stderr ?? ""}`.trim()
70
80
  };
71
81
  }
72
82
  function railsProject(root) {
@@ -108,7 +118,11 @@ function appMatches(container, root, runtime, db) {
108
118
  const env = environment(container);
109
119
  const mounts = container?.Mounts ?? [];
110
120
  const command = container?.Config?.Cmd ?? [];
111
- return container?.State?.Running && container?.Config?.Image === `ruby:${runtime.ruby}` && container?.Config?.WorkingDir === "/app" && mounts.length === 1 && mountedFromRoot(container, root) && command.length === 2 && command[0] === "sleep" && command[1] === "infinity" && onlyNetwork(container, runtime.network) && isolatedContainer(container) && env.RAILS_ENV === "test" && env.DATABASE_URL === db.databaseUrl(runtime.databaseContainer);
121
+ const servicesMatch = !runtime.services || runtime.services.every((svc) => {
122
+ const expected = serviceAdapters[svc.type]?.env?.({ container: svc.container }) || {};
123
+ return Object.entries(expected).every(([k, v]) => env[k] === v);
124
+ });
125
+ return container?.State?.Running && container?.Config?.Image === `ruby:${runtime.ruby}` && container?.Config?.WorkingDir === "/app" && mounts.length === 1 && mountedFromRoot(container, root) && command.length === 2 && command[0] === "sleep" && command[1] === "infinity" && onlyNetwork(container, runtime.network) && isolatedContainer(container) && env.RAILS_ENV === "test" && env.DATABASE_URL === db.databaseUrl(runtime.databaseContainer) && servicesMatch;
112
126
  }
113
127
  function databaseMatches(container, runtime, db) {
114
128
  const env = environment(container);
@@ -133,9 +147,13 @@ function selectedRun(root, reportPath) {
133
147
  if (!["in_progress", "paused"].includes(run.status)) throw new RunStateError("Target runtime preparation requires an active or paused upgrade run.", "run-not-resumable");
134
148
  return { reportPath, run };
135
149
  }
136
- function names(runId, db) {
150
+ function names(runId, db, serviceList = []) {
137
151
  const prefix = `ruby-upgrader-${runId}`;
138
- return { appContainer: `${prefix}-app`, databaseContainer: `${prefix}-${db.adapter}`, network: `${prefix}-network` };
152
+ const out = { appContainer: `${prefix}-app`, databaseContainer: `${prefix}-${db.adapter}`, network: `${prefix}-network` };
153
+ for (const svc of serviceList) {
154
+ out[`${svc.type}Container`] = `${prefix}-${svc.type}`;
155
+ }
156
+ return out;
139
157
  }
140
158
  function writeRuntime(root, runtime) {
141
159
  const file = runtimePath(root, true);
@@ -155,6 +173,7 @@ export function readTargetRuntime(root = process.cwd()) {
155
173
  runtime = { ...legacy, database: "postgres", databaseContainer: postgresContainer };
156
174
  }
157
175
  if (runtime?.version !== 2 || typeof runtime.runId !== "string" || !rubyVersion.test(runtime.ruby ?? "") || !/^sha256:[a-f0-9]{64}$/i.test(runtime.resolvedImageId ?? "") || (runtime.databaseImageId !== undefined && !/^sha256:[a-f0-9]{64}$/i.test(runtime.databaseImageId)) || !runtime.preparation || !Object.hasOwn(databases, runtime.database) || ![runtime.appContainer, runtime.databaseContainer, runtime.network].every((name) => NAME.test(name ?? ""))) throw new Error();
176
+ if (runtime.services && !Array.isArray(runtime.services)) throw new Error();
158
177
  return runtime;
159
178
  } catch { throw new Error("Target runtime metadata is invalid. Rerun prepare-target-runtime --ruby <x.y.z>."); }
160
179
  }
@@ -163,8 +182,13 @@ export function prepareTargetRuntime({ root = process.cwd(), reportPath, ruby, d
163
182
  if (!rubyVersion.test(ruby ?? "")) throw new Error("--ruby must be an exact numeric Ruby version such as 3.4.1.");
164
183
  const canonical = canonicalRoot(root);
165
184
  const db = resolveDatabase(database ?? detectDatabase(canonical));
185
+ const detectedServices = detectServices(canonical);
186
+ const serviceList = resolveServices(detectedServices);
166
187
  const selected = selectedRun(canonical, reportPath);
167
- const runtime = { version: 2, runId: selected.run.runId, reportPath: selected.reportPath, ruby, database: db.adapter, ...names(selected.run.runId, db) };
188
+ const run = readRun(canonical, selected.reportPath);
189
+ const bundlerToInstall = run.reportType === "bundler_bridge" ? run.targetBundler : (run.bundlerBridge ? run.bundlerBridge.bundlerTo : undefined) || "2.4.22";
190
+ const runtimeNames = names(selected.run.runId, db, serviceList);
191
+ const runtime = { version: 2, runId: selected.run.runId, reportPath: selected.reportPath, ruby, database: db.adapter, bundlerToInstall, services: [], ...runtimeNames };
168
192
  const owner = identity(runtime.runId, canonical);
169
193
  const existing = readTargetRuntime(canonical);
170
194
  if (existing && existing.runId !== runtime.runId) {
@@ -176,7 +200,9 @@ export function prepareTargetRuntime({ root = process.cwd(), reportPath, ruby, d
176
200
  let app = docker(spawn, ["inspect", runtime.appContainer]);
177
201
  let network = docker(spawn, ["network", "inspect", runtime.network]);
178
202
  const databaseInspections = new Map(databaseNames.map((name) => [name, docker(spawn, ["inspect", name])]));
179
- const resources = [app.status === 0 && parseInspection(app), network.status === 0 && parseInspection(network), ...[...databaseInspections.values()].filter((result) => result.status === 0).map(parseInspection)].filter(Boolean);
203
+ const serviceNames = serviceList.map((svc) => runtimeNames[`${svc.type}Container`]).filter(Boolean);
204
+ const serviceInspections = new Map(serviceNames.map((name) => [name, docker(spawn, ["inspect", name])]));
205
+ const resources = [app.status === 0 && parseInspection(app), network.status === 0 && parseInspection(network), ...[...databaseInspections.values()].filter((result) => result.status === 0).map(parseInspection), ...[...serviceInspections.values()].filter((result) => result.status === 0).map(parseInspection)].filter(Boolean);
180
206
  for (const resource of resources) {
181
207
  const resourceLabels = labels(resource);
182
208
  if (resourceLabels[LABEL] !== owner.runId || (resourceLabels[WORKTREE_LABEL] !== undefined && resourceLabels[WORKTREE_LABEL] !== owner.worktreeHash)) throw new Error("Refusing to use or replace a Docker resource not owned by this run and worktree.");
@@ -184,7 +210,7 @@ for (const resource of resources) {
184
210
  const legacy = resources.some((resource) => labels(resource)[WORKTREE_LABEL] === undefined);
185
211
  if (legacy && resources.length) {
186
212
  if (app.status !== 0 || !mountedFromRoot(parseInspection(app), canonical)) throw new Error("Refusing to migrate legacy Docker resources because ownership by this worktree cannot be established.");
187
- for (const [name, result] of [[runtime.appContainer, app], ...databaseInspections]) {
213
+ for (const [name, result] of [[runtime.appContainer, app], ...databaseInspections, ...serviceInspections]) {
188
214
  if (result.status === 0 && docker(spawn, ["rm", "--force", name]).status !== 0) throw new Error("Could not remove a legacy target-runtime container.");
189
215
  }
190
216
  if (network.status !== 0 && docker(spawn, ["network", "rm", runtime.network]).status !== 0) throw new Error("Could not remove the legacy target-runtime Docker network.");
@@ -216,6 +242,37 @@ for (const resource of resources) {
216
242
  try { docker(spawn, ["rm", "--force", runtime.databaseContainer]); } catch {}
217
243
  throw error;
218
244
  }
245
+ // Provision additional service containers (evidence-based)
246
+ const serviceStates = [];
247
+ for (const svc of serviceList) {
248
+ const svcContainer = runtimeNames[`${svc.type}Container`];
249
+ let svcResult = serviceInspections.get(svcContainer) ?? { status: 1 };
250
+ if (svcResult.status === 0) {
251
+ const c = parseInspection(svcResult);
252
+ const expectedEnv = (serviceAdapters[svc.type]?.env?.({ container: svcContainer }) || {});
253
+ const env = environment(c);
254
+ const hasBindMount = (c?.Mounts ?? []).some((mount) => mount.Type === "bind");
255
+ const owned = hasOwnership(c, owner);
256
+ const matches = c?.State?.Running && c?.Config?.Image === svc.adapter.image && onlyNetwork(c, runtime.network) && isolatedContainer(c) && !hasBindMount && owned && Object.entries(expectedEnv).every(([k, v]) => env[k] === v);
257
+ if (!matches) {
258
+ if (docker(spawn, ["rm", "--force", svcContainer]).status !== 0) throw new Error(`Could not replace the prior target-runtime ${svc.adapter.label} container.`);
259
+ svcResult = docker(spawn, ["inspect", svcContainer]);
260
+ }
261
+ }
262
+ if (svcResult.status !== 0) {
263
+ if (docker(spawn, ["run", "--detach", "--name", svcContainer, ...ownershipArgs, "--network", runtime.network, ...svc.adapter.startArgs()]).status !== 0) throw new Error(`Could not start the isolated target-runtime ${svc.adapter.label} container.`);
264
+ }
265
+ try {
266
+ svc.adapter.ensureReady({ probe: () => docker(spawn, svc.adapter.readyArgs(svcContainer)) });
267
+ } catch (error) {
268
+ try { docker(spawn, ["rm", "--force", svcContainer]); } catch {}
269
+ throw error;
270
+ }
271
+ const svcInspection = inspect(spawn, svcContainer, `Target ${svc.adapter.label} container is unavailable. Rerun prepare-target-runtime --ruby <x.y.z>.`);
272
+ if (!/^sha256:[a-f0-9]{64}$/i.test(svcInspection?.Image ?? "")) throw new Error(`Target ${svc.adapter.label} image ID is unavailable.`);
273
+ serviceStates.push({ type: svc.type, container: svcContainer, imageId: svcInspection.Image });
274
+ runtime.services = serviceStates;
275
+ }
219
276
  if (app.status === 0) {
220
277
  const container = parseInspection(app);
221
278
  if (!appMatches(container, canonical, runtime, db)) {
@@ -223,7 +280,13 @@ for (const resource of resources) {
223
280
  app = docker(spawn, ["inspect", runtime.appContainer]);
224
281
  }
225
282
  }
226
- if (app.status !== 0 && docker(spawn, ["run", "--detach", "--name", runtime.appContainer, ...ownershipArgs, "--network", runtime.network, "--mount", `type=bind,src=${canonical},dst=/app`, "--workdir", "/app", "--env", "RAILS_ENV=test", "--env", `DATABASE_URL=${db.databaseUrl(runtime.databaseContainer)}`, `ruby:${ruby}`, "sleep", "infinity"]).status !== 0) throw new Error("Could not start the target Ruby app container.");
283
+ const appEnv = ["--env", "RAILS_ENV=test", "--env", `DATABASE_URL=${db.databaseUrl(runtime.databaseContainer)}`];
284
+ for (const svc of serviceList) {
285
+ const svcContainer = runtimeNames[`${svc.type}Container`];
286
+ const envs = serviceAdapters[svc.type]?.env?.({ container: svcContainer }) || {};
287
+ for (const [k, v] of Object.entries(envs)) appEnv.push("--env", `${k}=${v}`);
288
+ }
289
+ if (app.status !== 0 && docker(spawn, ["run", "--detach", "--name", runtime.appContainer, ...ownershipArgs, "--network", runtime.network, "--mount", `type=bind,src=${canonical},dst=/app`, "--workdir", "/app", ...appEnv, `ruby:${ruby}`, "sleep", "infinity"]).status !== 0) throw new Error("Could not start the target Ruby app container.");
227
290
  const prepared = bootstrap(spawn, runtime, db, railsProject(canonical));
228
291
  const appInspection = inspect(spawn, runtime.appContainer, "Target Ruby app container is unavailable. Rerun prepare-target-runtime --ruby <x.y.z>.");
229
292
  const databaseInspection = inspect(spawn, runtime.databaseContainer, `Target ${db.label} container is unavailable. Rerun prepare-target-runtime --ruby <x.y.z>.`);
@@ -250,5 +313,10 @@ export function validateTargetRuntime({ root = process.cwd(), runtime = readTarg
250
313
  const expectedIds = [app?.Id, database?.Id].sort();
251
314
  const valid = appMatches(app, canonical, runtime, db) && databaseMatches(database, runtime, db) && hasOwnership(network, owner) && hasOwnership(app, owner) && hasOwnership(database, owner) && connectedIds.length === 2 && connectedIds.every((id, index) => id === expectedIds[index]) && app?.Image === runtime.resolvedImageId && (!runtime.databaseImageId || database?.Image === runtime.databaseImageId) && env.RAILS_ENV === "test" && env.DATABASE_URL === db.databaseUrl(runtime.databaseContainer);
252
315
  if (!valid) throw new Error("Target runtime no longer matches its prepared run. Rerun prepare-target-runtime --ruby <x.y.z>.");
253
- return { runId: runtime.runId, reportPath: runtime.reportPath, name: runtime.appContainer, id: app.Id, imageId: app.Image, imageRef: app.Config.Image, ruby: runtime.ruby, database: runtime.database, databaseContainer: runtime.databaseContainer, databaseContainerId: database.Id, databaseImageId: database.Image, databaseImageRef: database.Config.Image, network: runtime.network, networkId: network.Id };
316
+ // bundlerVersion is surfaced so the generated report can warn that the deploy
317
+ // host and CI may resolve a different Bundler than this container pinned. It
318
+ // comes from `bundle --version` in the prepared container, not from the
319
+ // bootstrap log, so it reflects what `bundle exec` actually resolved.
320
+ const bundlerVersion = /Bundler version (\d+(?:\.\d+)+)/.exec(runtime.preparation?.bundlerVersion ?? "")?.[1] ?? undefined;
321
+ return { runId: runtime.runId, reportPath: runtime.reportPath, name: runtime.appContainer, id: app.Id, imageId: app.Image, imageRef: app.Config.Image, ruby: runtime.ruby, ...(bundlerVersion ? { bundlerVersion } : {}), database: runtime.database, databaseContainer: runtime.databaseContainer, databaseContainerId: database.Id, databaseImageId: database.Image, databaseImageRef: database.Config.Image, network: runtime.network, networkId: network.Id };
254
322
  }