安装
没有任何依赖:它使用 fetch、WebSocket 和 node:crypto,Node 23.6 或更高版本可以直接运行它的 TypeScript 源码。npm 上的包名是 glac,并为不能运行 TypeScript 的运行时提供 JavaScript 和类型声明:
npm install glac开头几行
import { Glac, Sandbox } from "glac";
const glac = new Glac(); // GLAC_API_KEY and GLAC_API_URL; or new Glac({ apiKey, baseUrl })
const app = await glac.cube.deploy("ghcr.io/acme/api:1.4", { name: "api", port: 8080 }); // Cube: waits until it serves
console.log(app.url);
const sbx = await glac.sandboxes.create({ image: "python:3.12" }); // Sandbox
console.log((await sbx.run("python3 -c 'print(6*7)'")).stdout); // res.stdout, res.stderr, res.exitCode
const drive = await glac.drives.create({ name: "data", sizeBytes: 10 * 2 ** 30 }); // Drive: disks that outlive sandboxes
console.log((await Sandbox.run("echo hello")).stdout); // the shorthand: a sandbox for one command, on the default client客户端
new Glac({ apiKey, baseUrl, retries }) 是某个地址上的一个密钥;没有给出的从 GLAC_API_KEY 和 GLAC_API_URL 读取。它为每个产品提供一个命名空间:
| 命名空间 | 说明 | 调用 |
|---|---|---|
glac.cube |
方块:作为服务运行在自己地址上的镜像(API 称之为 app,/v1/apps)。glac.workers 是它的另一个名字,已标记为弃用 |
deploy(image, options), create(options), get(idOrName), getOrCreate(name, options), list() |
glac.sandboxes |
沙箱 (/v1/sandboxes) |
run(command, options), create(options), get(idOrName), getOrCreate(name, options), list({ status, metadata }) |
glac.sandboxes.snapshots |
沙箱的快照 | get(idOrName), list({ sandboxId }) |
glac.sandboxes.sshKeys |
进入沙箱和应用实例的 SSH 密钥 | create(publicKey, { name, sandboxId, expiresAt }), get(idOrName), list({ sandboxId }) |
glac.drives |
云盘:项目的磁盘,不随沙箱消失,同一时间由一个沙箱以读写方式挂载;以及云盘的快照,可以同时被任意多个沙箱以只读方式挂载(/v1/drives,属于控制平面) |
create({ name, sizeBytes, region, durability }), get(idOrName), list(), update(drive, { name }), delete(drive), attach(sandbox, drive, path, { mode }), detach(sandbox, drive, { force }), snapshot(drive, { name, fsConsistent }), snapshots(drive), getSnapshot(drive, snapshot), deleteSnapshot(drive, snapshot), fork(drive, { name, fromSnapshot }), restore(drive, snapshot), attachSnapshot(sandbox, snapshot, path, { drive }), detachSnapshot(sandbox, snapshot, { force }) |
glac.volumes |
存储卷:云盘底层的磁盘 (/v1/volumes),为过渡期而保留:用于独立运行的节点和共享存储卷 |
create({ name, sizeBytes, shared, fromBackup }), get(idOrName), list(), backup(volume, { expiresInDays }), backups(volume) |
glac.backups |
存储卷的备份:存储卷在某一时刻的完整副本,保存在对象存储中,不随存储卷消失(/v1/backups,属于控制平面) |
list({ volume }), get(backup), delete(backup), restore(backup, { name }) |
glac.secrets |
项目的机密 | put(name, value, { allowedHosts }), update(name, allowedHosts), list(), delete(name) |
glac.variables |
项目的共享变量 | put(name, value), list(), delete(name) |
glac.deploys |
项目的部署(/v1/deploys,属于控制平面):多个服务一起部署,按各自等待的对象排序,并一起切换(项目部署) |
create(services, { wait, timeoutSeconds })、get(id)、list():create 按 API 的命名接受每个服务的版本字段({}:按原样),等待部署完成(并跟随取代它的部署),失败时抛出 DeployError,附带 deploy、service 和部署的 code |
glac.domains |
项目的自定义域名(/v1/domains,属于控制平面):应用或沙箱某个端口的名称 |
create(domain, { app } or { sandbox, port }), get(idOrName), list({ app, sandbox }), delete(idOrName) |
glac.webhooks, glac.keys |
项目的事件发往哪里 (/v1/webhooks),以及项目的 API 密钥 (/v1/keys),属于控制平面 |
create、get、list、update(用于 Webhook)、delete |
glac.destinations |
项目中应用的日志、指标和追踪导出到哪里 (/v1/destinations),属于控制平面 |
create, get, list, update, delete, test |
平台本身的调用是客户端的方法:glac.account()、updateAccount()、usage()、billing()、updateBilling()、balance()、updateBalance()、topUp(usd)、charges()、ledger()、project() 和 events()。SDK 没有封装的 API:glac.json("GET", "/v1/webhooks"),列表用 glac.pages(path)。调用返回的对象(Sandbox、App、Drive、Volume)会保留它的客户端,它自己的方法使用这个客户端。
简写:Sandbox.run、Sandbox.create、Sandbox.get、Sandbox.getOrCreate 和 Sandbox.list 就是默认客户端上 glac.sandboxes 的调用,默认客户端在第一次使用时根据 GLAC_API_KEY 和 GLAC_API_URL 创建;{ client } 可以指定另一个客户端。其他产品都没有简写。
值为 undefined 的选项不会发送。有些选项中的 null 有特定含义:sleepAfterSeconds: null 表示永不休眠,更新中的 sleepNoticeMs: null 表示休眠前不再通知它的程序,env: { KEY: null } 删除一个键,egress: null 去掉所有网络规则。
方块
方块(API 称之为 app)是作为服务运行在自己地址上的镜像,实例数量随请求的需要而定,没有请求时一个都没有。SDK 中的 App 就是一个方块。
import { DeployError, Glac } from "glac";
const glac = new Glac();
const app = await glac.cube.deploy("ghcr.io/acme/api:1.4", { name: "api", port: 8080 });
try {
await app.deploy({ image: "ghcr.io/acme/api:1.5" }); // a new version, waited for
} catch (e) {
if (!(e instanceof DeployError)) throw e; // it did not come up: the traffic stays where it was
}
for await (const line of app.logs({ follow: true })) if (line.kind === "output") console.log(line.text);| 调用 | 作用 |
|---|---|
glac.cube.deploy(image, { name, ...every option of create, wait, timeoutSeconds }) |
与 glac deploy 相同:部署到 name 指定的应用时,先修改给出的应用自身设置 (PATCH),再用其余部分创建一个版本;名称没有对应的应用、或不给名称时,创建一个新应用及其第一个版本。除非 wait: false,否则会等待版本,失败时抛出 DeployError,然后返回应用。 |
glac.cube.create(...), get(idOrName), getOrCreate(name, options), list() |
创建一个应用(给出 image 时,会等待它的第一个版本);查找一个;查找或创建一个,不部署任何东西;列出。drives 是应用唯一的实例以读写方式挂载的云盘(有多个实例或使用键的应用会被拒绝,409 drive_single_writer);keyDrives 给键控应用的每个键的实例各自的云盘,随该键的第一个实例创建,随键删除。 |
app.deploy(...), app.rollback(version), app.update(changes), app.versions(), app.delete() |
用给出的内容创建一个版本;把流量切回另一个版本;修改(修改构成版本的内容时产生新版本);列出版本;删除。 |
app.managedBy |
唯一能修改这个应用的东西(一个 ManagedBy)。它所属的数据库,{ object: "database", id: "db-…" }:在这里对它更新、部署、回滚或删除都会被拒绝,返回 409 managed_by_database(它的日志和其他应用一样可以读取和删除)。或者是沙箱产品,{ object: "sandboxes", id: null }:项目默认的沙箱应用 sbx-sandbox,或某个沙箱所用镜像对应的应用,它的实例就是项目的沙箱;除了指定默认应用的镜像,其他修改都会被拒绝,返回 409 managed_by_sandboxes,但可以删除它。不属于任何东西的应用为 null。 |
glac.cube.create、deploy、app.deploy 和 app.update 的 runtime |
版本的运行时设置,一个 ServiceRuntime:ulimits ({ nofile: 65535 })、sysctls、tmpfs、dns、shm_size_bytes;整体替换(更新中为 null:不设置)。 |
app.logs({ follow, since, ... }), app.logUsage(), app.logsState, app.instance(key) |
日志(for await,连接丢失后会自动恢复)、日志的统计、保存其日志的每台机器现在是否长期保留那里打印的内容(日志),以及键控实例。 |
instance.upgrade({ version, when, keepDays }), app.upgradeInstance(key, { version, when, keepDays }); await upgrade.wait({ timeoutSeconds }); instance.upgrades(), app.instanceUpgrades(key), app.instanceUpgrade(key, id) |
把某个键的实例原地迁到应用的另一个版本:默认为应用的当前版本,也可以用 version 指定(id、编号或 Version;必须是就绪的版本,也可以是更旧的)。实例保留它的 id、节点、键、地址、令牌,以及它自己的云盘和其中的内容;它的磁盘、机器和进程换成新版本的,打开的连接会结束,切换期间它的请求暂存在边缘节点。解析为一个 Upgrade(status 为 preparing、switching、done 或 failed;fromVersion、toVersion、timings、error、putBack;kind(upgrade 或 revert)、when、keepDays、rollback(回退它所需的内容,保留 keepDays 天)、reverts、batch;通过 API 要求等实例休眠的升级,在那之前为 waiting,被取消时为 cancelled(API 的 DELETE,或它的批次被停止):被取消的升级就此结束,等待它的调用会像升级失败那样抛出异常,错误码为 cancelled);实例已经在那个版本上时为 null。新版本没能起来时,wait() 抛出 UpgradeError(错误码为 upgrade_failed 或 upgrade_not_here):这时实例留在原来的版本上。instance.refresh() 重新读取实例,其中包括它的 versionId。when: "asleep" 让升级等到实例休眠时再开始 (waiting),这样它的请求一个都不会被暂存;keepDays(0 到 365,默认 7)是升级完成后回退所需内容的保留天数,按快照计费。如果版本的服务挂载的云盘或存储卷与实例的不同,就不能原地升级:升级以 failed 结束,错误码为 upgrade_not_here。 |
upgrade.cancel(), app.cancelInstanceUpgrade(key, upgrade); upgrade.revert({ keepDays }), app.revertInstanceUpgrade(key, upgrade, { keepDays }) |
取消一个正在等待的升级(等实例休眠,或在批次中排队):状态变为 cancelled,实例留在原来的版本上(已经开始的升级返回 409 upgrade_started)。回退:把实例恢复到升级所保留的状态,即它的磁盘、升级之前的云盘(会先为云盘现在的内容创建快照)和原来的版本;返回一个新的 Upgrade,kind 为 revert,reverts 指明被回退的升级,它的进行和等待方式与升级相同。要回到的内容已不再保留时,返回 409 rollback_gone。 |
app.upgradeInstances({ version, fromVersions, keys, when, keepDays, concurrency, maxFailures }); app.upgradeBatches(), app.upgradeBatch(id), app.stopUpgradeBatch(id); batch.wait({ timeoutSeconds }), batch.refresh(), batch.stop() |
把应用的多个键批量升级到同一个版本(默认为应用的当前版本),可以限定为 fromVersions 中的版本、keys 中的键:每个键在其实例休眠时升级(when: "asleep",默认)或轮到时立即升级 ("now"),同时 concurrency 个(默认 4;同一节点上 2 个),失败数达到 maxFailures(默认 5)时以 failed 停止。解析为一个 UpgradeBatch:status(running、done、failed、stopped)、stopReason、counts、upgrades(每个成员的键、升级、状态和错误)、skipped、origin(api,或在应用的 keys 设置了 upgrade_on_deploy 时为 deploy)。无论批次如何结束,wait() 都会解析,不抛出任何异常;实例从不休眠的成员会让批次一直处于 running,所以 timeoutSeconds 会抛出 wait_timeout 错误。stop() 取消仍在等待的成员。 |
app.openInstancePort(key, port, { from, protocol }), app.instancePorts(key), app.closeInstancePort(key, port) |
某个键的实例在它自己的公网 IPv6 地址上开放的端口,无论实例在哪里:按应用和键调用它的沙箱的 openPort、ports 和 closePort(键控实例)。没有自己地址的实例会被拒绝 (409 no_ipv6)。 |
glac.cube.create、deploy 和 app.update 的 checkpoint;app.checkpoint;sbx.checkpoint |
应用的键控实例在运行期间的检查点(一个 AppCheckpoint,{ everySeconds }:1 到 600,省略或为 null 时为 10;app.update({ checkpoint: null }) 去掉检查点),以及实例的检查点进行得如何(一个 SandboxCheckpoint:everySeconds、lastAt、behindSeconds、bytesPerSecond、storedBytes、error;没有检查点时为 null)。 |
app.metrics({ start, end, step }) |
为应用提供服务的机器对它的统计,每分钟一步(step: "hour":每小时一步):按状态码类别统计的请求数、耗时、实例及其用量(指标)。返回 API 的对象。 |
glac.cube.create、deploy、getOrCreate 和 app.update 的 traces;app.traces |
平台为应用的一部分请求创建 span,实例自己的 span 是它的子 span(一个 AppTraceSettings:{ sampleRate: 0.1, trustIncoming: false };默认不创建),发往项目中接收 "traces" 的导出目标(追踪);应用当前的设置。 |
app.createKeySecret(), app.keySecrets(), app.keySecret(id), app.deleteKeySecret(id), signKey(secret, appId, key, { expiresAt, path }) |
用来签名应用的键的机密(一个 KeySecret,只有刚创建的那个才有 secret),以及用它签名的键,即经由边缘节点的请求所携带的形式(签名键);path: true 把它写成路径段。 |
沙箱
| 调用 | 作用 |
|---|---|
glac.sandboxes.create({ image, snapshot, name, env, metadata, volumes, drives, publicPorts, sleepAfterSeconds, sleepNoticeMs, sleepHoldMaxMs, expiresAt, egress, imagePullSecret, start, startCommand, ready, projectNetwork, hostname, stop, checkpoint }) |
创建一个沙箱;每个选项都可以省略。drives: [{ path, drive }] 挂载云盘,[{ path, drive, snapshot }] 以只读方式挂载云盘的快照(见下文)。启动时运行的服务:start: true(镜像自己的启动命令)或 startCommand,配合 ready: { tcp: true, port: 5432 }(或 { http: "/health", port: 8080 }、{ command: "…" });hostname 设置机器的名称,stop: { signal, graceMs } 设置服务的停止方式;projectNetwork: true 让它从一开始就加入项目的私有网络,地址为 sbx.projectAddress;存储卷的 copy: true 把镜像在该路径下的内容放进新的存储卷(创建沙箱)。 |
sbx.appId, sbx.versionId, sbx.key, sbx.imageDigest |
通过控制平面创建的沙箱是某个应用的实例:不带镜像创建时,是项目默认的沙箱应用 (sbx-sandbox),否则是它的镜像对应的应用((await glac.apps.get(sbx.appId)).instances() 列出该应用的沙箱,每个都在它的键下);它所属的该应用的版本、创建它所用的镜像(之后被移动过的标签会成为应用的新版本,此后创建的沙箱属于新版本);它在那里的键、名称或 id;以及按摘要标识的镜像。创建的响应和之后的每次读取都会给出这些信息,并且保持不变;从模板或快照创建的沙箱,以及分叉,这些信息为 null。 |
sbx.startCommand, instance.startCommand |
它的启动命令如何运行,按客户机最近一次的报告(一个 StartCommandState):status(running;restarting,在 restarting_at 重新启动;exited)、restarts(键控实例和有状态服务的实例,在命令自行结束时会在机器中重新启动,每次启动都是一个 instance.restarted 事件)、last_exit(status、signal、at);节点没有报告时为 null。 |
create 和 sbx.update 的 suspendAfterMs、wakeDailyAt;sbx.suspendAfterMs、sbx.wakeDailyAt |
因空闲而停止后,它保持暂停多久才保存内存、真正休眠(一秒到一天;它的节点最多保持十分钟),以及休眠期间每天在 UTC 的哪个时刻 ("HH:MM") 唤醒它,以便做它自己等着唤醒才做的工作;在更新中设为 null 会把其中任意一项恢复默认。 |
create 和 sbx.update 的 onExpiry、heldAsleep;sbx.onExpiry、sbx.heldAsleep |
expiresAt 到期时做什么:"delete" 删除沙箱(默认)、"sleep"(让它休眠并保留,请求照常会唤醒它)或 "hold"(同上,但保持休眠);以及它休眠时,发往它某个端口的请求是否会唤醒它:保持休眠时,它的预览地址返回 502 sandbox_paused,API 调用会唤醒它。在更新中设为 null 会把其中任意一项恢复默认。 |
create 和 run 的 init: "systemd";sbx.init |
完整的系统:镜像自己的 systemd 作为沙箱的 PID 1(不带 image 时,使用平台带 systemd 和 OpenSSH 的 Ubuntu),所以软件包、用 systemctl 启动的服务、cron 和 journal 都能像在一台普通机器上一样工作,它也有自己的 machine ID,在休眠、唤醒和迁移后保持不变。不能与 start、startCommand 或 ready 同时使用 (400 invalid_start),也不能与 snapshot 同时使用。这样的沙箱 sbx.init 为 "systemd",使用平台 init 的沙箱则为 null。 |
glac.sandboxes.run(command, options) |
为一条命令创建的沙箱:创建、使用、删除。 |
glac.sandboxes.get(idOrName), getOrCreate(name, options), list({ status, metadata }) |
查找一个;查找或创建一个;列出。 |
sbx.run(command, { cwd, env, user, timeoutSeconds, stdin, onStdout, onStderr, signal }) |
运行并等待。返回一个 Result。 |
sbx.exec(command, { stdinOpen, tty }) |
启动命令并返回一个 Command:wait()、output(from)、write()、kill()。 |
sbx.terminal({ cols, rows, join, onData }) |
基于 WebSocket 的终端:write、resize、close。 |
sbx.files |
read、readText、write、list、stat、exists、mkdir、move、remove,以及 setAttributes(path, { mode, sizeBytes, modifiedAt }):文件的权限、长度和最后修改时间。 |
sbx.files.upload(localPath, path), download(path, localPath), readStream(path, { offset, length }), write(path, blobOrStream) |
任意长度的文件都可以在一次调用中传输,无论这里还是 API 都不会持有整个文件:upload 边读本地文件边发送,download 随字节到来写入本地文件(所有字节都到齐后才放到位),readStream 给出这些字节的 ReadableStream,write 也接受 Blob、ReadableStream 或字节的异步可迭代对象。在达到响应所说的长度之前就结束的流,会在结束时报错:请重新读取。 |
sbx.url(port), sbx.rotateUrls(), sbx.token({ scopes, expiresInSeconds }) |
端口链接(在本地签名);新的链接机密;这个沙箱的令牌。 |
sbx.ipv6、sbx.openPort(port, { from, protocol })、sbx.ports("tcp")、sbx.closePort(port, { protocol }),create 和 update 的 inbound: […] |
它自己的公网 IPv6 地址(如果有,否则为 null);向列出的来源(IPv6 前缀或地址,"::/0" 表示所有人)开放一个端口,并给出访问地址;开放的端口;关闭一个端口(返回它之前是否开放);完整的列表。入站默认关闭;它休眠时和迁移时,开放的端口都保持开放。参见网络。 |
sbx.revokeToken(token) |
从此只拒绝那一个令牌,其他令牌照常有效:token() 给出的东西(一个 SandboxToken,它的 id,即 tok_…,指明是哪个)、令牌本身,或它的 id。一个沙箱最多保留 1,000 个尚未过期的这类吊销(超出时返回 409 too_many_revoked_tokens:这时请用 revokeTokens())。返回一个 TokenRevocation,带有 tokenId 和 expiresAt。 |
sbx.revokeTokens() |
到目前为止为这个沙箱发出的所有令牌都会失效 (401 invalid_token),包括预览链接和 Cookie 中的令牌,以及 SSH 会话持有的令牌;之后创建的令牌有效。需要 API 密钥。返回一个 TokenRevocation:sandboxId、generation、revokedAt。 |
sbx.update(changes), sbx.sleep(), sbx.restart(), sbx.delete() |
修改;立即休眠;从磁盘启动;删除。 |
glac.sandboxes.create 和 sbx.update 的 checkpoint;sbx.checkpoint |
运行期间的检查点({ everySeconds: 10 };在更新中设为 null 会去掉检查点),这样机器丢失时它能带着进程继续运行;检查点的进行情况。没有机器支持检查点时返回 409 checkpoints_unavailable。 |
sbx.diskWrites |
它的磁盘今天(UTC 日)已写入的量,与当天全速份额的对比(一个 SandboxDiskWrites):todayBytes,其中 checkpointCopyBytes 是检查点副本写入宿主机磁盘的量;fullSpeedUntilBytes,即份额(默认 100 GiB,节点另有设置时除外;null 表示节点不限速,但照样统计);limited 和 limitedUntil(一个 Date):超出份额后,它的磁盘以每秒 2 MiB 的速度接受写入,直到当天结束,任何写入都不会失败。在不把磁盘复制到对象存储的节点上为 null,那里什么都不统计。写入它的云盘的数据不计在这里:写入云盘不收费。 |
sbx.snapshot(), sbx.fork(), sbx.restore(snapshot), glac.sandboxes.snapshots, snap.update({ name, expiresAt }) |
创建快照、分叉、原地回到某个快照;列出快照;给快照换个名称(null:不要名称),或设置它自行删除的时间(null:永不删除)。 |
glac.sandboxes.sshKeys.create(publicKey, { sandboxId, expiresAt }) |
单个沙箱或项目中所有沙箱的 SSH 密钥;私钥会在发送任何东西之前就被拒绝。 |
sbx.runCode(code), sbx.agent("claude"), new Desktop(sbx) |
代码执行、编程智能体,以及桌面沙箱的屏幕。 |
实时输出、文件、快照和分叉:
const built = await sbx.run("make", { onStdout: (text) => process.stdout.write(text) });
const cmd = await sbx.exec("node server.js"); // do not wait
for await (const piece of cmd.output()) process.stdout.write(piece.text);
await sbx.files.write("/tmp/notes.txt", "hello\n");
const before = await sbx.snapshot({ name: "before-upgrade" }); // memory, processes and disk
const [copy] = await sbx.fork(); // goes on from this moment
await sbx.restore(before); // back to the snapshot
await copy.delete();云盘
云盘是项目的一块磁盘,不随它的沙箱消失:同一时间由一个沙箱以读写方式挂载,可以在沙箱创建时挂载,也可以在沙箱运行时挂载;沙箱没了,云盘连同其中的内容一起保留,供下一个沙箱挂载:可以在它的区域,也可以在别的区域,只要那里的机器能读取它归属区域的对象存储(见下文)。云盘在写入的同时被复制到所在区域的对象存储中,所以其他节点上的沙箱也能挂载它:会先把它搬过去。只按写入的数据计费 (usedBytes)。云盘属于控制平面 (/v1/drives):对着一个独立运行的节点,每个调用都会抛出该节点的 404 no_such_route,节点上的磁盘是存储卷(见下文)。
const drive = await glac.drives.create({ name: "data", sizeBytes: 10 * 2 ** 30 }); // or none: 64 GiB, or 5 GiB on Free
const sbx = await glac.sandboxes.create({ drives: [{ path: "/data", drive: drive.id }] });
await sbx.files.write("/data/notes.txt", "hello\n");
await glac.drives.detach(sbx, drive);
const other = await glac.sandboxes.create();
await glac.drives.attach(other, drive, "/data"); // the same Drive, in another sandboxDrive 包含 id(vol-…:云盘就是存储卷)、name、sizeBytes、usedBytes、attachedTo(沙箱、路径、模式)、region、home、status、lease(唯一写入方所在的节点、纪元和到期时间)、appId 和 key(键控实例的云盘)、parentDriveId 和 rootDriveId(分叉的云盘才有:来源云盘和这一脉的第一个云盘,来源云盘之后可能已被删除;不是分叉的云盘为 null)、managedBy(一个 ManagedBy,数据库的云盘才有;不属于任何东西时为 null),并自带 attach、detach、update({ name })、refresh() 和 delete()。
- 一个云盘同一时间只属于一个沙箱:另一个沙箱持有的云盘会被拒绝 (409
drive_attached),删除已挂载的云盘也一样。键控实例的云盘随它的键一起删除 (409key_drive)。 - 数据库的云盘 (
drive.managedBy) 只能由数据库的实例以读写方式挂载,重命名、恢复和删除都要通过数据库进行:在这里调用这些操作,或以读写方式挂载它,都会被拒绝 (409managed_by_database)。项目可以为它创建快照和分叉,任何人都可以以只读方式挂载它的快照。 - 另一个节点持有的云盘,会先被搬到沙箱所在的节点;对象存储中还没有它的最新状态时 (503
copy_behind),或无法把它搬过去时 (503drive_not_brought),调用为retryable:客户端会按retries的设置重试,之后抛出异常,你稍后再请求一次即可。 - 跨区域 (slice D9)。云盘的归属区域 (
home) 是保存其副本的对象存储所在的区域:也就是创建它的区域。另一个区域的沙箱也能挂载它,前提是那个区域的机器能读取其归属区域的存储(不能时返回 409not_on_one_machine):云盘被释放为一个版本,在那里被接管,客户机读取时从归属区域的存储读出,客户机写入的数据则进入它现在所在区域的存储 (region)。云盘在那里待满一小时后,归属区域会跟过去,用glac.drives.moveHome(drive)(drive.moveHome()) 则立即迁移:它的机器把归属区域存储中剩下的内容复制到自己区域的存储中,那里什么都不剩时,home就变成新区域;drive.homeMove说明迁移进行到哪一步(asked_at、asked_by、copied_objects、copied_bytes、objects_left、error)。保留的版本都不再需要旧归属区域的副本后(一天后),这份副本会被删除。云盘的归属区域最多每六小时迁移一次 (409home_moved_recently);归属区域就是当前所在区域的云盘返回 409home_here。 - 套餐的限制:免费版最多 3 个云盘,单个最大 10 GiB,合计 15 GiB;付费版最多 500 个,单个最大 1 TiB (403
drive_limit_reached)。 glac.usage({ drive })、kind: "drive"和groupBy: "drive"报告云盘保存了多少、写入了多少、冷读了多少,glac.usage({ snapshot: "dsnap-…" })报告一个快照单独保留了多少。drive.created、drive.deleted、drive.attached和drive.detached事件携带EventDrive或EventDriveAttached,drive.snapshot.created和drive.snapshot.deleted携带EventDriveSnapshot。
快照、分叉与恢复。云盘的快照是它在对象存储中的一个版本:不复制任何数据。运行中的沙箱正在写入的云盘,会在写入被暂停片刻时创建快照:带 fsConsistent: true 时,由沙箱的代理程序冻结它的文件系统(consistency 为 "frozen";代理程序做不到时返回 409 not_frozen:这时请不带它创建),否则暂停它的机器("paused",云盘处于相当于断电时的状态,文件系统的日志就是为此准备的);没有运行中的沙箱持有的云盘按原样创建快照 ("at_rest")。暂停最多十秒,之后调用会被拒绝,返回 503 unavailable (retryable)。
const snap = await drive.snapshot({ name: "before" }); // a version in the object store: nothing is copied
const reader = await glac.sandboxes.create({ drives: [{ path: "/before", drive: drive.id, snapshot: snap }] }); // read-only
await glac.drives.attachSnapshot(sbx, snap, "/before"); // into a running sandbox too, as many as want it
const copy = await snap.fork({ name: "data-2" }); // a new Drive, mountable at once
await glac.drives.detach(other, drive);
await drive.restore(snap); // detached first: back to the snapshot| 调用 | 作用 |
|---|---|
glac.drives.snapshot(drive, { name, fsConsistent }), snapshots(drive), allSnapshots({ driveDeleted }), getSnapshot(drive, snapshot), deleteSnapshot(drive, snapshot) |
创建快照(名称不必唯一);云盘的快照,最新的在前(云盘删除后按它的 id 指定),或项目中所有云盘的快照(driveDeleted: true:只列出已删除云盘的快照);按 id 查看一个(云盘没有这个 id 的快照时返回 404 snapshot_not_found);删除一个:从它分叉出的云盘会保留所需的数据,挂载着它的沙箱会保留挂载,直到卸载。 |
glac.drives.fork(drive, { name, fromSnapshot }) |
一个新云盘,它的第一个版本是快照的内容,或云盘当前的内容:不复制任何数据,可以立即挂载。大小与来源云盘相同,和创建一样受套餐云盘数量的限制 (403 drive_limit_reached);它的 parentDriveId 和 rootDriveId 说明它从何而来。 |
glac.drives.restore(drive, snapshot) |
把云盘恢复到它的某个快照,并返回云盘。 |
glac.drives.create({ historySeconds }), update(drive, { historySeconds }), versions(drive, { at }), fork(drive, { at }), restore(drive, undefined, { at }); drive.versions({ at }), drive.historySeconds, drive.history |
云盘的版本窗口 (slice DB8):按时间最多能往回恢复或分叉多远,3600 到 86400 秒(不设置时没有窗口;关闭后,它保留的内容会被丢弃)。在每个有写入的 30 秒里,窗口都会得到一个版本,即云盘在某一时刻的状态;这些版本单独保留的数据和快照一样计费。按时间恢复或分叉时,取该时间点或之前最新的版本,也就是以该时间调用版本列表时给出的那个版本。versions 返回 DriveVersion,最新的在前;restore 接受快照或 { at },二者只能给一个(两个都给或都不给时,这里会抛出 TypeError);historySeconds: null 关闭版本窗口。 |
glac.drives.create({ cacheBytes }), update(drive, { cacheBytes }); drive.update({ cacheBytes }), drive.cacheBytes, drive.cachedBytes |
云盘的缓存:节点在 NVMe 上最多保存云盘的多少数据。默认全部保存(即云盘的大小);也可以设得更小,最小到 0,超出的部分只保存在对象存储中,客户机读到时再从那里读取(最久没被读过的数据最先释放,对象存储还没有的数据从不释放)。缓存按小时、按已填充的量计费,与云盘的数据费用分开。调小缓存后,超出的部分一旦已在对象存储中就会被释放(如果运行中的沙箱挂载云盘时是全部缓存,则从那个沙箱下一次休眠或停止时开始);调大缓存后,从那时起客户机读到的数据都会留在缓存中。API 把云盘叫作存储卷 (/v1/volumes);各客户端通过 /v1/drives 访问它们,这个接口的响应和以前一样。更新时传 cacheBytes: null 恢复为全部缓存。控制平面没有给出缓存信息时,drive.cacheBytes 就是云盘的大小;drive.cachedBytes 是当前在节点 NVMe 上的量(没有节点持有云盘时为 null)。用量报告的分组和合计中有 cache_gb_hours,即缓存保存的量。 |
glac.drives.attachSnapshot(sandbox, snapshot, path, { drive }), detachSnapshot(sandbox, snapshot, { force }) |
以只读方式把快照挂载到正在运行的沙箱中,并按快照的 id 卸载这个挂载;两者都返回 DriveSnapshot。DriveSnapshot 带有它所属的云盘;只给出快照的 id 时需要 drive,否则会在发送任何东西之前就被拒绝 (400 missing_field)。 |
drive.snapshot(), drive.snapshots(), drive.fork(), drive.restore(snapshot); snap.refresh(), snap.delete(), snap.fork({ name }), snap.restore(), snap.attach(sandbox, path), snap.detach(sandbox) |
同样的调用,也可以在 Drive 和 DriveSnapshot 上进行,后者包含 id (dsnap-…)、name、driveId、consistency、usedBytes(当时云盘中的数据量)、exclusiveBytes(它单独保留的数据量)、exclusiveAt、version 和 createdAt。 |
- 快照不随云盘消失:云盘删除后,仍可以按云盘的 id 列出、读取、分叉它的快照,或以只读方式挂载。快照按它单独保留的数据 (
exclusiveBytes) 计费,按小时计,价格与云盘的数据相同(每 GB·月 $0.02),在云盘也删除之后一直计费,直到快照本身被删除。 - 分叉和任何云盘一样,按它的
usedBytes,即完整的逻辑内容计费:从 10 GB 快照分叉出的云盘按 10 GB 的云盘计费。 - 恢复会在云盘最新的版本之后写入一个新版本,内容与快照相同;中间的版本只要有快照或分叉保留着就仍然存在,所以先恢复到较旧的快照、再恢复到较新的快照也可以。云盘现在的内容如果没有快照保留就会消失:请先创建一个快照。沙箱以读写方式持有的云盘不能恢复 (409
drive_attached):先卸载它。 - 只读挂载:在云盘所在区域的任意机器上,任意多个沙箱都可以挂载,无论云盘被谁持有,云盘删除后也可以。数据块在客户机读取时从对象存储取来,不收费;它们在用量中计在挂载它的沙箱名下(
glac.usage({ sandbox }),drive_cold_read_bytes);读取它的机器上已有的数据也不收费。客户机不能写入它,挂载了快照的沙箱也不能迁到别的机器上。沙箱会把它列在volumes中,带有view_of。 glac.drives.create的durability为standard或strict,后者要等对象存储收到 fsync 之前的每一次写入,客户机的 fsync 才返回;云盘的durability会说明它,一经设定不再改变(写入有多持久)。- 不向对象存储复制任何数据的节点没有快照、分叉和恢复 (409
not_copied)。应用的版本只能以读写方式挂载云盘:快照的只读挂载只供沙箱使用。
glac.volumes 对应 API 的 /v1/volumes,即云盘底层的磁盘,为过渡期而保留:只有它才有的,是独立运行的节点(节点上的磁盘就是存储卷),以及共享存储卷,即可由多个沙箱同时挂载的文件系统。沙箱或应用以 volumes: [{ path: "/data", volume: vol.id }] 的形式挂载存储卷。
const team = await glac.volumes.create({ name: "team", shared: true }); // no size is needed
const box = await glac.sandboxes.create({ volumes: [{ path: "/team", volume: team.id }] });存储卷的备份。备份是存储卷(云盘也是存储卷)在某一时刻的副本,保存在对象存储中,直到过期:可以由调用创建,可以按存储卷的规则每天创建,也会在服务的新版本替换其实例之前创建。与云盘的快照不同,它是一份独立的副本,仅凭它就能创建存储卷,而且不随存储卷消失;存储卷的备份只保存自上一个备份以来变化的内容,多个备份都需要的数据块只计费一次,计在最早的备份上。在存储收到全部数据之前为 making,之后为 ready,从那时起按存储中保存的数据量 (storedBytes)、以休眠沙箱所保存内容的价格计费(备份)。备份属于控制平面。
const b = await glac.volumes.backup("data", { expiresInDays: 7 }); // "making", then "ready"
while ((await b.refresh()).status === "making") await new Promise((r) => setTimeout(r, 1000));
const back = await glac.backups.restore(b, { name: "data-back" }); // a new volume: "restoring", then "ready"glac.volumes.backup(volume, { expiresInDays })(或vol.backup())立即创建一个备份,保留expiresInDays天(1 到 365),或按存储卷的规则(未设置时为 30 天);glac.volumes.backups(volume)列出一个存储卷的备份,最新的在前(已删除的存储卷按 id 指定)。不向对象存储复制任何数据的节点没有备份 (409not_copied)。glac.backups.list({ volume })列出项目的备份,最新的在前,包括已删除存储卷的备份;get(backup)、delete(backup)(之后不再为它计费;从它创建的存储卷仍然保留),以及restore(backup, { name }),即从一个就绪的备份创建新存储卷,等同于glac.volumes.create({ fromBackup, name })(创建期间返回 409backup_not_ready,失败后返回 409backup_unusable)。Backup包含id(bak-…)、volumeId、status(making、ready、failed)、reason(manual、daily、before_version)、storedBytes(计费依据)、ownBytes(删除它能省下的量)、sharedBytes、volumeName(当时存储卷的名称)、consistency(frozen、crash)、frozenMs(存储卷的写入方为复制等待了多久;没有冻结时为 null)、sizeBytes(存储卷当时的数据量)、volumeSizeBytes(它最多能容纳的量,也就是从备份创建的存储卷的容量)、shared、region、failure、createdAt、readyAt(从此开始计费)、expiresAt,以及refresh()、restore({ name })、delete()。- 从备份创建的存储卷,大小和类型与备份所来自的存储卷相同(与
fromBackup一起时不给出sizeBytes和shared),位于备份所在的区域。它的status在完整拿到备份之前为restoring,之后为ready,在此之前不能挂载;fromBackup指明来源备份。如果被分配到的机器无法恢复它,控制平面会在该区域的另一台机器上再创建一次:这时info.restore_attempt为 2,info.first_failure说明第一次为什么失败。如果这一次也失败,它就是failed(info.failure),只能删除。 glac.usage({ backup })、kind: "backup"和groupBy: "backup"报告备份保存了多少,计在状态计量项上 (state_gb_hours);glac.billing()的meters.state.backups_gb_hours说明该计量项中有多少来自备份。
域名
项目的域名是一个指向应用(和它的默认地址一样)或沙箱某个端口的名称。域名属于控制平面。
| 调用 | 作用 |
|---|---|
glac.domains.create(domain, { app }) 或 { sandbox, port }、get(idOrName)、list({ app, sandbox })、domain.update(...)、domain.refresh()、domain.delete() |
为应用或沙箱端口注册的名称,在 DNS 上能查到 verification.records 中的某条记录之前为 pending_verification(自定义域名);项目中已有该域名时返回 409 domain_exists。 |
const name = await glac.domains.create("api.example.com", { app: "api" });浏览器
尚未开放。浏览器还没有向账户开放:下面是开放之前 SDK 目前的样子。
会话是一个托管的 Chromium,由 Playwright、Puppeteer 或任何其他 Chrome DevTools Protocol (CDP) 客户端从外部驱动。有客户端连接时它保持运行,最后一个客户端离开 sleep_after_seconds 秒后进入休眠,页面会保留。CDP 客户端只需要它的 cdpUrl:凭据就在地址的主机名中,所以会话的 JSON 中不包含它。
await using session = await glac.browsers.create({ name: "agent-42" }); // closed and deleted at the end of the block
const browser = await session.connect(); // Playwright, where it is installed
const page = browser.contexts()[0].pages()[0];
await page.goto("https://example.com");-
glac.browsers.create({ kind, name, region, sleep_after_seconds, connection_idle_seconds, delete_after_idle_seconds, metadata, profile, proxy })创建一个处于运行状态的会话(kind:默认为chromium-headless;chromium有屏幕;lightpanda是没有布局引擎的浏览器);如果用已有会话的名称创建,会返回那个会话:它会被唤醒,页面都还在。profile: { name, save, version }在返回会话之前加载一个配置文件(见下文)。还有get(idOrName)、list({ status, kind, name })(status:awake、asleep、unavailable或failed)、update(session, changes)、delete(session)、sleep(session)、rotateUrl(session);BrowserSession自己也有同样的方法。沙箱在运行前就失败的会话不算会话:创建会被拒绝,返回 503session_failed。 -
session.status为awake、asleep、unavailable(它的节点无法访问)或failed(创建得到响应之后它的沙箱失败了:比如浏览器停止且没有恢复),这时session.error说明原因({ code, message },按节点的原话;没有失败时为null)。browser.failed事件也会说明这一点(它的数据类型为EventBrowserFailed)。 -
session.cdpUrl就是chromium.connectOverCDP和 Puppeteer 的browserURL接受的地址。rotateUrl()换新地址:旧地址从此被拒绝,经由它们建立的连接会断开。 -
session.connect({ chromium, timeoutMs })连接 Playwright:装有playwright或playwright-core时使用它们(这个包不依赖二者中的任何一个),否则使用给出的chromium。close()关闭那个浏览器;会话仍然保留,没有客户端连接时进入休眠。 -
session.liveUrl是在浏览器中实时观看其页面的地址(无头会话是随变化实时画出的标签页,有头会话是它的屏幕),观看者的鼠标和键盘操作会传到页面上;await session.live({ interactive: false })给出一个单独的地址,任何操作都传不到页面,用来给人看。观看者会让会话保持运行。两个地址都会被rotateUrl更换,读取它们需要密钥有url权限范围。 -
配置文件(它的 Cookie、Local Storage、IndexedDB、Session Storage 和偏好设置,由平台为项目保存):
create({ profile: { name: "acme-login", save: true } })加载一个配置文件,并在会话删除时再次保存(delete({ saveProfile: false })则不保存);session.saveProfile(name?)立即保存(浏览器会重启,已连接的客户端需要重新连接;这期间对同一配置文件的另一次保存会以 409profile_busy被拒绝);session.profile是它持有的配置文件;还有glac.browsers.profiles.list()、get(name)、delete(name)。 -
你自己的代理,付费版可用(免费版上返回 403
proxy_not_on_plan):create({ proxy: "http://user:password@proxy.example.com:3128" })(或{ url })让会话到 80 和 443 端口的连接经由浏览器旁边的一个中继,从这个接受CONNECT的 HTTP 代理出站,浏览器本身对代理一无所知;QUIC 在那里会被拒绝,所以浏览器改用基于 TCP 的 HTTP。session.update({ proxy })可以在运行中的会话上更换代理,无需重启浏览器(经由原代理的连接会断开),{ proxy: null }则去掉代理。凭据由平台保管,不会出现在任何响应中:session.proxy为{ url, username },session.proxyBytes是经由代理双向传输的总量(proxySentBytes和proxyReceivedBytes分别是每个方向的量)。如果代理不接受隧道,页面会通过 HTTP 得到 502proxy_failed。参见自己的代理。
会话和任何实例一样,按它使用的方块资源计费:计算时的 CPU、运行时的内存和出站流量(价格与套餐)。session.awakeSeconds 是它已运行的时长。经过它的代理的流量和其他流量一样算作它的流量。会话属于控制平面。
数据库
数据库是托管的 Postgres:运行在云盘上的独立 PostgreSQL,没有任何请求时休眠,有连接时被唤醒,会话都会保留。它大约两秒就能创建好,带有根分支 main 和所有者 glacdb_owner(glac_superuser 的成员,不是超级用户)。所有者的密码只出现在 create 和 resetPassword 的响应中,任何地方都不保存:只有它们返回的数据库的 connectionString() 才带有密码。
import pg from "pg";
const db = await glac.databases.create("shop"); // ready, with the owner's password
const client = new pg.Client({ connectionString: db.connectionString() }); // from a sandbox or a service of the project
await client.connect();
await client.query("create table orders (id bigserial primary key, total numeric)");glac.databases.create(name, { version, region, database_name, size, storage, sleep_after_seconds, suspend_after_seconds, wake_daily_at, metadata })创建一个数据库并等待它第一次启动:ready,或creating(反复db.refresh(),直到就绪)。version:16、17 或 18(默认);size: { vcpus, memory_bytes },即上限(默认 1 vCPU 和 1 GiB);storage: { size_bytes, durability },它的云盘,strict(默认:对象存储收到提交后提交才返回)或standard;suspend_after_seconds:保存内存、真正休眠之前保持暂停多久,暂停期间连接在一两毫秒内就能得到响应,1 到 86400(默认 300);wake_daily_at:休眠期间每天在 UTC 的哪个时刻 ("HH:MM") 唤醒它(它的pg_cron任务无论如何都会在各自的时间唤醒它)。用已有数据库的名称创建时,返回那个数据库,不带密码。update(db, { suspend_after_seconds, wake_daily_at })(以及db.update)修改这两项,所有分支一起修改;null把其中任意一项恢复默认(分别为 300 和不唤醒)。免费版数据库的休眠设置是固定的,与 Neon 免费版缩容到零的做法相同:最后一个查询一秒后暂停(sleep_after_seconds为 1),五分钟后休眠(suspend_after_seconds为 300);无论创建还是修改,其他值都会以 400invalid_field被拒绝。glac.databases.createBranch(db, name, { parent, expires_at })(以及db.createBranch(name, { parent, expires_at }))创建一个独立分支:它的父分支(默认main,可用parent指定其他分支)此刻的状态,不复制任何数据,有自己的 PostgreSQL、端点和云盘,在项目网络上的地址是<database>-<name>;给出expires_at时,到时会自行删除。它的角色和密码与父分支相同:创建它的Database带有所有者的密码时,分支的connectionString()也带有。deleteBranch(db, branch)(db.deleteBranch(branch)、branch.delete())停止它的 PostgreSQL 并删除它的云盘;不能删除main(409root_branch),也不能删除有其他分支从它创建的分支 (409has_branches)。glac.databases.create(name, { history_seconds })、update(db, { history_seconds })、restore(db, { at, exact })和createBranch(db, name, { from: { at, exact } })(以及db.update、db.restore、db.historySeconds、db.history):数据库的历史(slice DB8 和 DB5):根分支按时间最多能恢复到多久以前,或从多久以前创建分支:默认一天;付费版可设为 0(不保留)到 30 天,免费版为一天。恢复会把根分支原地带回过去:恢复到整分钟或某个版本自身的时刻时,使用云盘版本窗口中该时刻或之前最新的版本(每个有写入的 30 秒一个版本,最多保留一天);更早的时刻、精确到秒的时刻,或带exact: true时的那个精确时刻(精确到毫秒),则通过 WAL 归档恢复(把 WAL 重放到那一刻:需要几秒到几分钟,调用会一直等待)。端点、连接字符串和所有者的密码都保持不变,现有连接会断开,恢复之前的状态会作为一个独立分支main_old_<the restore's moment>保留;如果新版本在五分钟内不能提供服务,恢复会被撤销 (503database_restore_failed)。按时间创建分支时,则以那个时刻创建分支。历史中没有的时间返回 400invalid_history,错误信息会说明版本窗口和归档分别能追溯到什么时候。精确到秒的时间(或带exact)只能从归档恢复,否则拒绝,从不退而使用窗口中之前的版本:归档追溯不到时返回 400invalid_history(数据库不保留归档,或时间早于最早的基础备份),归档的存储拒绝提供分段时返回 503database_archive_unavailable(可重试);每次拒绝都会指明窗口中该时间或之前的版本,你可以改为请求那个版本自身的时刻。存储给不出所需数据块时,返回 503drive_unreadable(可重试),不会创建任何东西。如果对象存储中断太久,等待归档的 WAL 超过了云盘为日志预留的空间,这部分 WAL 会被放弃,以保证数据库继续运行:这就是归档中的一个缺口,记录在db.history.gaps中(DatabaseHistoryGap:from、to、from_lsn、to_lsn):被放弃的 WAL 从何时开始,归档到何时为止无法恢复任何时间(缺口之后第一个基础备份完成的时间;在有基础备份之前为 null),以及缺口在日志中的起止位置。缺口内的时间只能从云盘的版本窗口恢复(整分钟,或某个版本自身的时刻);精确到秒或精确时刻的恢复返回 400invalid_history。缺口也会以事件的形式报告:首次发现缺口时发出database.history_gap(数据库运行期间,每个缺口一次),缺口之后归档重新可用于恢复时发出database.history_resumed;它们的data是一个EventDatabaseGap:数据库,缺口的from、to(前者为 null)、from_lsn和to_lsn,segments(放弃了多少个 16 MB 的分段),以及前者才有的error(对象存储最后一次拒绝时返回的内容)。create(name, { tiering: { enabled: true } })和update(db, { tiering: { enabled } })用来开启或关闭分层存储(slice DB18:付费版可用;每个分支的 PostgreSQL 会为此重启一次),数据库的info.tiering说明是否已开启,以及目前按多少计费 (tiered_bytes)。get(idOrName)、list({ name })、delete(db, { finalSnapshot })(除非finalSnapshot: false,否则它的云盘会保留一个快照七天,即db.finalSnapshot)、branches(db)(先是main,然后是独立分支,最早的在前)、branch(db, "main")、sleep(db, "main")、wake(db, "main")(它的内存会被保存,醒来时会话都还在;连接也会唤醒它)、resetPassword(db, { role })(新密码只出现在这次响应中;从下一次登录起旧密码会被拒绝)。Database自己也有同样的方法。glac.databases.roles(db)(以及db.roles())列出根分支中可登录的角色,直接从 PostgreSQL 读取 ({ name, owner }):它的所有者和你创建的角色 (CREATE ROLE … LOGIN),平台自己的角色不在其中。resetRolePassword(db, role)(以及db.resetRolePassword(role))给其中任何一个角色设置新密码,只出现在这次响应中:一个DatabaseRoleSecret,带有该角色的password和根分支上该角色的连接字符串 (connectionString());所有者的连接字符串由 API 响应的数据库生成。独立分支保留它创建时的密码。db.connectionString({ password, pooled, private })是 node-postgres (connectionString)、psql 和 libpq 都能直接使用的字符串:API 给出公网连接字符串时用公网的,否则用项目网络上的,并带有所有者的密码(来自与之一起返回的响应),或者password(百分号编码)。公网连接字符串中的sslmode=require,按 node-postgres 目前的理解写作sslmode=verify-full。db.defaultBranch包含 API 给出的连接字符串(connectionUri、pooledUri、privateUri)、它的endpoint和state(awake、paused、asleep)。
数据库在项目网络上按名称访问,来自它的沙箱(用 project_network 创建)和服务:这就是它的 private_uri。有边缘节点提供 PostgreSQL 服务时,也可以从任何地方通过它的公网地址访问(connection_uri,端口 5432,只支持 TLS;连接池的地址:pooled_uri);没有边缘节点提供、或数据库没有可用来命名的区域时,它们为 null;给出时,连接字符串会使用它们。数据库的 JSON 中不包含密码。数据库属于控制平面。
托管应用
目录中的应用(比如 Rivet)以它自己的方块资源安装到项目中:它的应用、存储卷、机密和存储桶,都带有该安装的标签,只能通过这个安装修改,并按这些资源本身计费(托管应用)。安装的响应包含全部输出,包括机密输出(管理员令牌、带有令牌的端点),之后的读取只对有 admin 权限范围的密钥显示这些输出:安装对象会保留之前的响应给出、而之后的响应隐藏的每个输出。
const rivet = await glac.catalogue.get("rivet"); // its parameters, and a month of it in Cube's meters
console.log(rivet.calculator?.month);
const engine = await glac.installations.create("rivet", { parameters: { disk_gib: 20 } }); // waits until it is ready
console.log(engine.outputs.endpoint); // RIVET_ENDPOINT, its token in it| 调用 | 作用 |
|---|---|
glac.catalogue.list(), glac.catalogue.get(app, { version }) |
目录中的应用,每个都是一个 CatalogueApp(app、version、title、summary、verified、licence、versions、parameters:一个 JSON Schema,outputs、带有 month 的 calculator);查看一个应用的最新版本或指定的版本,以及它的 definition。 |
glac.installations.create(app, { name, version, region, parameters, requires, wait, timeoutSeconds }) |
安装它,并等它就绪(wait: false:立即返回)。安装失败时抛出 InstallationError,附带错误信息和 installation;timeoutSeconds 之后仍在创建时,抛出错误码为 wait_timeout 的 GlacError。 |
glac.installations.list({ app }), glac.installations.get(idOrName) |
项目的安装,最新的在前;查看一个。 |
installation.update({ version, parameters }, { wait }), installation.delete(), refresh(), wait({ timeoutSeconds }) |
换一个版本,或以合并补丁的方式修改参数(null 把参数恢复为默认值);连同它的所有组成部分一起删除(status:deleted 或 deleting)。 |
installation.status, failure, outputs, secretOutputs, parameters, apps, volumes, buckets, requires, requiredBy |
按 API 给出的样子。 |
托管应用属于控制平面。
机密与变量
glac.secrets.put(name, value, { allowedHosts }) 保存一个任何响应都不会返回的值,它只会发往给定的主机;应用以环境变量的形式得到它 (secrets: { DATABASE_URL: "db_url" })。glac.variables.put(name, value) 是一个共享变量,应用的 env 以 ${{ shared.NAME }} 引用它。
项目、账户与账单
glac.project() 和 glac.events();glac.account() 和 glac.usage(),以及 updateAccount(),它可以设置账户的通知发往哪里 ({ email }),以及通知使用的语言等({ language: "de" }:“en”、“de” 或 “fr”,或 null 表示重新跟随账单地址);账户的 email_verified 说明它的地址是否已验证,email_undeliverable(一个 EmailUndeliverable:why、since、detail)说明发往它的邮件被退回了,这会让通知暂缓发送(通知发往哪里);它的 keeps(一个 AccountKeeps,在控制平面后面)是密钥所属项目在云盘和存储卷上的保有量,按创建时所受限制的数字给出,每项都附带已有的量(volumes、volume_bytes、volumes_bytes,其 limit 为 null 表示不限,以及 unsized_bytes:参见提高限制);glac.billing()、glac.balance()、glac.topUp(usd)、glac.charges() 和 glac.ledger(),以及用来修改套餐和自动充值的 updateBilling() 和 updateBalance()。账单和项目属于控制平面。
事件的数据。控制平面的每种事件,都有一个对应其数据 (event.data) 的接口,以描述中该事件的 schema 命名 (event.data as EventDatabase)。数据库的事件:EventDatabase(database.created,附带它的 region 和 PostgreSQL version;database.deleted,附带 final_snapshot_id,即保留七天的云盘快照)、EventDatabaseBranch(database.branch_created,附带它的 parent;database.branch_deleted,附带它的 reason)、EventDatabaseRestored(database.restored:时刻 at,以及 old_branch_id,即保存恢复之前状态的分支)、EventDatabaseArchive(database.archive_failing:since、连续失败的分段数 failures、archived_until,即在此期间恢复能追溯多远,以及对象存储的 error;database.archive_recovered:recovered_at)和 EventDatabaseGap。其他事件也一样:沙箱的(EventSandboxSlept、EventSandboxWoke、EventSandboxMoved、EventSandboxDeleted 等)、应用及其键的(EventApp、EventAppFailed、EventKeyCreated 等)、项目的部署和密钥、备份、域名、导出目标和云盘快照的(EventProjectDeployed、EventBackup、EventDomain、EventDestination、EventDriveSnapshot 等),以及账户的套餐、充值、退款、撤回、余额、点数、存储、出站流量上限、支出上限、冻结和欠款的(EventSubscriptionPeriod、EventTopupCollected、EventWithdrawalReceived、EventBalance、EventInDebt 等)。某类事件并非都有的字段是可选字段。
Webhook、API 密钥、支出上限、项目。glac.webhooks.create(url, { events, enabled }) 把项目的事件发到你的地址(为每次投递签名的 secret 只出现在这次响应中);还有 get、list、update、delete。glac.keys.create({ name, scopes, expiresAt }) 创建一个 API 密钥(密钥本身 key 只出现在这次响应中);还有 get、list、delete。glac.limits() 和 glac.updateLimits(usd) 是项目的支出上限和本月已支出的金额(null 去掉上限),sbx.limits() 和 sbx.updateLimits(usd) 则是某个沙箱自己的。glac.requestLimit(limit, wanted, why) 申请提高账户的某项可应要求提高的限制(按名称指定的上限、"volumes" 或 "volume_bytes";后两者中的大小以字节为单位;同一项限制已有申请在等待时返回 409 limit_request_pending),glac.limitRequests({ limit, cursor }) 和 glac.limitRequest(id) 读取申请及运营方的答复(LimitRequest:status 为 pending、granted 或 refused,granted、answer)。instance.restarted、instance.upgraded、instance.reverted、instance.upgrade_failed 以及 limit_request.granted 和 .refused 事件的数据,分别是 EventInstanceRestarted、EventInstanceUpgraded、EventInstanceReverted、EventInstanceUpgradeFailed 和 EventLimitRequest。glac.updateProject({ deleteAfterIdleDays: 30 }) 设置沙箱休眠多少天后被删除(null:永不删除)。属于控制平面。Webhook 和 API 密钥需要有 admin 权限范围的密钥,设置项目的上限和 updateProject 也一样;设置沙箱的上限需要 write。
日志导出目标、审计日志、计费文件。glac.destinations 是项目中应用的日志(以及指标和请求的追踪:signals: ["logs", "traces"])导出的地方(OpenTelemetry 端点):create(name, { endpoint, headers, ... }) 创建时会立即用一条记录测试 (status.connected),还有 get、list、update、delete、test(立即发送一条记录);请求头的值是凭据,只在创建或修改导出目标时发送,不会出现在任何响应中(返回的内容只有请求头的名称)。glac.audit({ limit, cursor }) 是项目审计日志的一页,最新的在前(谁、什么方法、什么路径、什么状态):每一次不是读取的调用(被拒绝的也算),以及运营方对应用日志的每一次读取。glac.statement("2026-09") 是某个月的对账单(不给出时为本月;也可以给出该月中的某个 Date),其中每个数据库单独一行(databases:它这个月的用量,其中历史和分层数据单列,以及按标价折合多少),glac.invoices() 是 Stripe 上的发票(附 PDF 链接),glac.notices() 是发给账户的邮件,glac.withdrawals() 是现在可以撤回的购买和已做的撤回(欧洲经济区或英国的消费者可以在 14 天内撤回一笔购买),glac.withdraw(chargeId, { name, email }) 是买方本人的撤回声明,立即受理。属于控制平面。创建、修改、测试和删除导出目标、读取审计日志以及撤回,需要有 admin 权限范围的密钥;其他调用只需要有读取权限的密钥。
立即购买 (startNow)。欧洲经济区或英国的消费者(没有已验证或正在验证的税号的账户)可以在 14 天内撤回一笔购买,所购内容只有在其本人明确要求时才会立即提供。topUp(20, { startNow: true })、updateBilling({ plan: "paid", startNow: true })(从免费版购买付费版)和 updateBalance({ autoReload: true, startNow: true })(自动充值保持开启期间的每一次自动充值)都带有这一要求,措辞与控制台在勾选框旁显示的相同(GET /v1/billing/withdrawals 中也有,见 right.start_now):
我要求在可以撤回这笔购买的 14 天内立即提供所购内容。如果我撤回,我需要为此前已提供的部分付费。我理解,一次充值的点数全部用完后,即视为已全部提供,我将不能再撤回。
服务立即开始;在 14 天内,买方仍可撤回,并为此前已提供的部分付费;一旦全部提供(一次充值的点数全部用完),撤回权即丧失。只有在购买者本人提出这一要求时才加上它:SDK 不会自行发送它。没有它时,这类账户的购买会被拒绝:抛出一个 GlacError,即 400 start_now_required,param 为 start_now,错误信息会指明账户所在的国家并引用这一要求的原文,什么也不会买。开启了自动充值却没有这一要求的账户不会自动充值,并且每月会收到一次通知(auto_reload.no_request 事件,它的数据是 EventAutoReloadNoRequest)。
错误
非零退出码是一个结果:读取 res.exitCode 即可。API 的错误会抛出 GlacError,带有 status、type、code、param、retryable、requestId 和 message。DeployError(没能起来的版本,或失败的项目部署)和 LogsError(无法再读取的日志)也都是 GlacError。
值得了解的行为
- 休眠的沙箱。不会有调用因为沙箱在休眠而失败:它会被唤醒,调用继续进行。没有专门的唤醒调用。
- 重试。没有响应,或出现
retryable错误时,会重试两次 (retries),间隔从 0.2 秒开始翻倍。每个POST都带有Idempotency-Key。带字节请求体的调用不会重试。 - Python SDK 有而它还没有的:
cmd.wait()的时限和客户端超时、原始输出字节,以及精确的十进制用量合计。它目前只在 Node 上运行过。