Skip to content

Rust SDK

一个客户端,每个产品(方块、沙箱、浏览器、云盘、数据库)一个命名空间,平台本身的调用直接在客户端上。阻塞式调用,返回 Result,链式选项,四个依赖,不需要异步运行时。

更新于

安装

调用是阻塞式的,返回 Result;选项是链式的 …Options;时长为 Duration,时间为 SystemTime;API 的 null 对应 Option。四个依赖(serde_json、带 ring 的 rustls、webpki-roots、libc),不需要异步运行时。crates.io 上的 crate 名是 glac:

[dependencies]
glac = "0.1"

客户端及其产品

use glac::{AppOptions, DriveOptions};

let client = glac::Client::new()?; // GLAC_API_KEY and GLAC_API_URL; another address or key: Client::builder()

let app = client.cube().deploy("ghcr.io/acme/api:1.4", &AppOptions::new().name("api").port(8080))?; // Cube: waits until it serves
println!("{:?}", app.url());

let sbx = client.sandboxes().create()?; // Sandbox
println!("{}", sbx.run("python3 -c 'print(6*7)'")?.stdout);

let drive = client.drives().create_with(&DriveOptions::new().size_bytes(1 << 30))?; // Drive

println!("{}", glac::run("echo hello")?.stdout); // the shorthand: a sandbox for one command, on the client the environment describes

每个产品都是客户端的一个命名空间,即借用客户端的一个轻量句柄:

  • client.cube():方块,作为服务运行的镜像,API 称之为应用。类型沿用 API 的名称:App、AppOptions、Version、Instance。client.workers() 是它的另一个名字,已标记为弃用。
  • client.sandboxes():沙箱,以及它们的快照 (.snapshots()) 和能打开它们的 SSH 密钥 (.ssh_keys())。
  • client.drives():云盘,项目的磁盘,不随沙箱消失,同一时间由一个沙箱以读写方式挂载(Drive、DriveOptions、DriveMount),以及它们的快照(DriveSnapshot、DriveSnapshotOptions、DriveForkOptions);client.volumes(),云盘底层的存储卷(Volume、VolumeOptions、VolumeMount);client.backups(),存储卷的备份(Backup、BackupOptions),属于控制平面。
  • client.domains():项目的自定义域名(Domain、DomainOptions),即应用或沙箱某个端口的名称;属于控制平面。
  • client.webhooks() 和 client.keys():项目的事件发往哪里(Webhook、WebhookOptions),以及项目的 API 密钥(ProjectKey、KeyOptions);属于控制平面。
  • client.destinations():项目中应用的日志、指标和追踪导出到哪里(Destination、DestinationOptions);属于控制平面。
  • client.secrets() 和 client.variables():项目的机密和共享变量。
  • client.deploys():项目的部署(/v1/deploys,属于控制平面):多个服务一起部署,按各自等待的对象排序,并一起切换(项目部署)。create([("api", DeployOptions::new().image("ghcr.io/acme/api:2")), ("web", DeployOptions::new())]) 给每个服务单独部署时接受的内容,并等待部署完成,跟随取代它的部署;失败时返回类型为 deploy_failed 的错误,附带部署的错误码(job_failed、job_timed_out、job_lost 等),服务作为 param() (services.<name>)。还有 create_with(…, &ProjectDeployOptions::new().no_wait())、get(id)、list()。
  • 不属于任何产品的调用在客户端本身上:账户、用量、套餐和账单、项目及其事件。

密钥从 GLAC_API_KEY 读取,地址从 GLAC_API_URL 读取。要使用其他地址、密钥或超时,请构建一个客户端:

let client = Client::builder().api_key(key).base_url(address).build()?;
let named = client.sandboxes().create_with(&CreateOptions::new().name(&name).env("STAGE", "dev").never_sleep())?;

简写。沙箱 SDK 的一行写法仍然保留,它们就是由环境变量描述的那个客户端(Client::shared(),第一次使用时创建)上的 client.sandboxes():glac::run、glac::run_with、Sandbox::create、Sandbox::get、Sandbox::get_or_create、Sandbox::list,以及它们的 _with 和 _page 形式。其他一切都需要客户端。

每个带选项的调用都有两种形式:create() 和 create_with(&options),run(cmd) 和 run_with(cmd, options)。SDK 没有封装的 API:client.request("GET", "/v1/webhooks", &[], None)。API 中有三个名称是 Rust 的关键字,因此改了名:错误的 type 是 kind(),文件的 type 是 FileEntry::kind,files/move 是 rename。

方块

下文中,cube 指 client.cube()。

调用 作用
cube.deploy(image, &AppOptions::new().name("api").port(8080)) 部署一个镜像,与 glac deploy 相同:为该名称的应用部署一个新版本,或创建一个新应用。它会等到版本承接流量,然后返回那时的应用(url()、current_version())。
cube.create_with(&AppOptions), cube.get(id_or_name), cube.get_or_create_with(name, &AppOptions), cube.list() 创建一个应用(给出镜像时,会部署并等待它的第一个版本);查找一个;查找或创建一个;列出。drives([DriveMount…]) 是应用唯一的实例以读写方式挂载的云盘(有多个实例或使用键的应用会被拒绝,409 drive_single_writer;版本只能以读写方式挂载云盘,快照的只读挂载只供沙箱使用);key_drives([KeyDrive…]) 给键控应用的每个键的实例各自的云盘,随该键的第一个实例创建,随键删除。
app.deploy_with(&DeployOptions), app.update(&AppChanges), app.rollback(Some("1"), true), app.versions(), app.delete() 新版本;修改;把流量切回某个版本;列出版本;删除。
AppOptions 和 DeployOptions 的 .runtime(json!({"ulimits": {"nofile": 65535}})) 版本的运行时设置,与 API 接受的形式相同:ulimits、sysctls、tmpfs、dns、shm_size_bytes;整体替换。
app.logs(), cube.logs_with(name, &LogsOptions), app.log_usage(…), app.info().logs_state, app.instance(key, None) 它的日志,一个日志行的迭代器,也可以按应用名称读取;日志的统计;保存其日志的每台机器,现在是否长期保留那里打印的内容(日志);键控实例。
instance.upgrade(Some("3")), app.upgrade_instance(key, version); up.wait(), up.wait_for(limit); instance.upgrades(), app.instance_upgrades(key), app.instance_upgrade(key, id) 把某个键的实例原地迁到应用的另一个版本:None 表示应用的当前版本,否则为版本的 id 或编号(必须是就绪的版本,也可以是更旧的)。实例保留它的 id、节点、键、地址、令牌和它自己的云盘;它的磁盘、机器和进程换成新版本的,切换期间它的请求暂存在边缘节点。返回进行中的 Some(Upgrade)(它的 info():status 为 preparing、switching、done 或 failed,from_version、to_version、timings、error、put_back、kind(upgrade 或 revert)、when、keep_days、rollback(回退它所需的内容,保留 keep_days 天)、reverts、batch;通过 API 要求等实例休眠的升级,在那之前为 waiting,被取消时为 cancelled(API 的 DELETE,或它的批次被停止):被取消的升级就此结束,等待它的调用会像升级失败那样返回错误,错误码为 cancelled);实例已经在那个版本上时返回 None。新版本没能起来时,wait() 返回类型为 upgrade_failed 的错误(错误码为 upgrade_failed 或 upgrade_not_here),实例留在原来的版本上。instance.refresh() 重新读取实例 (version_id)。
app.upgrade_instance_with(key, &UpgradeOptions::new().version("4").when_asleep().keep_days(14)), instance.upgrade_with(&…) 同上,带 when_asleep(升级等到实例休眠时再开始,期间为 waiting,这样它的请求一个都不会被暂存)和 keep_days(0 到 365,默认 7:升级完成后回退所需内容的保留天数,按快照计费)。如果版本的服务挂载的云盘或存储卷与实例的不同,就不能原地升级:升级以 failed 结束,错误码为 upgrade_not_here。
up.cancel(), app.cancel_instance_upgrade(key, id); up.revert(keep_days), app.revert_instance_upgrade(key, id, keep_days) 取消一个正在等待的升级(等实例休眠,或在批次中排队):状态变为 cancelled,实例留在原来的版本上(已经开始的升级返回 409 upgrade_started)。回退:把实例恢复到升级所保留的状态,即它的磁盘、升级之前的云盘(会先为云盘现在的内容创建快照)和原来的版本;返回一个新的 Upgrade,类型为 revert,它的进行和等待方式与升级相同。要回到的内容已不再保留时,返回 409 rollback_gone。
app.upgrade_instances(&UpgradeBatchOptions::new().version("4").from_versions(["3"]).keys(["k"]).when_now().keep_days(1).concurrency(8).max_failures(1)); app.upgrade_batches(), app.upgrade_batch(id), app.stop_upgrade_batch(id); batch.wait(), batch.wait_for(limit), batch.refresh(), batch.stop() 把应用的多个键批量升级到同一个版本(默认为应用的当前版本):每个键在其实例休眠时升级(或轮到时立即升级,when_now),默认同时 4 个(同一节点上 2 个),默认失败 5 次后以 failed 停止。返回一个 UpgradeBatch,它的 info() 是一个 UpgradeBatchInfo:status(running、done、failed、stopped)、stop_reason、counts、upgrades(每个成员的键、升级、状态和错误)、skipped、origin。无论批次如何结束,wait() 都返回 Ok;实例从不休眠的成员会让批次一直运行,所以 wait_for 会以 wait_timeout 结束。stop() 取消仍在等待的成员。
app.open_instance_port(key, "tcp", port, &["2001:db8::/32"]), app.instance_ports(key, "tcp"), app.close_instance_port(key, "tcp", port) 某个键的实例在它自己的公网 IPv6 地址上开放的端口,无论实例在哪里:按应用和键调用它的沙箱的 open_port、ports 和 close_port("udp" 表示 UDP;键控实例)。没有自己地址的实例会被拒绝 (409 no_ipv6)。
AppOptions::checkpoint(n), AppChanges::checkpoint(n), AppChanges::no_checkpoint() 应用的键控实例在运行期间的检查点(每 n 秒一次,1 到 600;0 表示用默认值,每 10 秒一次),以及关闭检查点;AppInfo::checkpoint 会说明它。实例的 SandboxInfo::checkpoint(一个 Option<SandboxCheckpoint>:every_seconds、last_at、behind_seconds、bytes_per_second、stored_bytes、error)说明它的检查点进行得如何。
app.metrics(from, to, step) 为应用提供服务的机器对它的统计,每分钟一步(Some("hour"):每小时一步):按状态码类别统计的请求数、耗时、实例及其用量(指标)。返回 API 的对象。
AppOptions 和 AppChanges 的 .trace_sample_rate(0.1)、.trust_incoming_trace(true);app.info().traces 平台为应用的一部分请求创建 span,实例自己的 span 是它的子 span(默认不创建),发往项目中接收 "traces" 的导出目标(追踪);应用当前的设置(一个 AppTraceSettings)。
app.create_key_secret(), app.key_secrets(), app.key_secret(id), app.delete_key_secret(id), glac::sign_key(secret, app_id, key, expires) 用来签名应用的键的机密(一个 KeySecret,只有刚创建的那个才有 secret),以及用它签名的键,即经由边缘节点的请求所携带的形式(签名键);glac::path_segment 把它写成路径段。

对已有的应用调用 deploy 时,先修改选项中属于应用自身的设置(区域、扩缩容、键、出站规则、元数据、日志、追踪、public、depends_on、网络:PATCH /v1/apps/{id},只在给出其中某些设置时才修改),然后用镜像和其余设置创建一个版本 (POST /v1/apps/{id}/versions),没有给出的沿用应用已有的:发出的请求与先调用 app.update(…)、再调用 app.deploy_with(…) 相同。没有该名称的应用 (404 app_not_found) 或不给名称时,它用全部设置创建一个应用 (POST /v1/apps);这时返回 409 name_taken 表示在此期间有人创建了它,于是部署到对方的应用。no_wait() 在版本创建后立即返回。没能起来的版本是一个 kind() 为 deploy_failed 的错误,附带版本自己的错误码和输出。应用属于控制平面:独立运行的节点对这些调用都返回 404 no_such_route。

沙箱

下文中,sandboxes 指 client.sandboxes()。

let mut sbx = client.sandboxes().create()?; // stays until deleted, also when the value is dropped
let res = sbx.run("python3 -c 'print(6*7)'")?; // res.stdout, res.stderr, res.exit_code
println!("{} {}", res.stdout, sbx.url(3000)?);

以字符串给出的命令用 sh -c 运行;以字符串数组给出时,它是一个程序及其参数。

let cmd = sbx.exec("node server.js")?; // do not wait
for piece in cmd.output() {
    print!("{}", String::from_utf8_lossy(&piece?.data));
}
调用 作用
sandboxes.create_with(&CreateOptions::new().image(…).name(…).env(k, v).drive(DriveMount::new("/data", id)).sleep_after(d).never_sleep().sleep_notice(d).sleep_hold_max(d).egress(…)) 创建一个沙箱;每个选项都可以省略。启动时运行的服务:.start(true)(镜像自己的启动命令)、.start_line(…) 或 .start_argv(…),配合 .ready_tcp(port)、.ready_http(port, path) 或 .ready_command(…);.hostname(…)、.stop_signal(…) 和 .stop_grace(d);.project_network() 让它从一开始就加入项目的私有网络 (info().project_address);VolumeMount::new("/data", vol).copy() 把镜像在该路径下的内容放进新的存储卷(创建沙箱)。
sbx.info().app_id, version_id, key, image_digest 通过控制平面创建的沙箱是某个应用的实例:不带镜像创建时,是项目默认的沙箱应用 (sbx-sandbox),否则是它的镜像对应的应用(应用的 instances() 列出该应用的沙箱,每个都在它的键下);它所属的该应用的版本、创建它所用的镜像(之后被移动过的标签会成为应用的新版本,此后创建的沙箱属于新版本);它在那里的键、名称或 id;以及按摘要标识的镜像。创建的响应和之后的每次读取都会给出这些信息,并且保持不变;从模板或快照创建的沙箱,以及分叉,这些信息为 None。
CreateOptions::init("systemd"); SandboxInfo::init 完整的系统:镜像自己的 systemd 作为沙箱的 PID 1(不指定镜像时,使用平台带 systemd 和 OpenSSH 的 Ubuntu),所以软件包、用 systemctl 启动的服务、cron 和 journal 都能像在一台普通机器上一样工作,它也有自己的 machine ID,在休眠、唤醒和迁移后保持不变。不能与启动命令或就绪检查同时使用 (400 invalid_start),也不能从快照创建。这样的沙箱 init 为 Some("systemd"),使用平台 init 的沙箱则为 None。
sandboxes.get, sandboxes.get_or_create, sandboxes.list_with(&ListOptions), sandboxes.list_page(&ListOptions) 查找;查找或创建;列出;获取一页。
CreateOptions 和 UpdateOptions 的 .suspend_after(d)、.wake_daily_at("03:30")(.default_suspend_after()、.no_daily_wake() 恢复默认);SandboxInfo::suspend_after、wake_daily_at 因空闲而停止后,它保持暂停多久才保存内存、真正休眠(一秒到一天;它的节点最多保持十分钟),以及休眠期间每天在 UTC 的哪个时刻唤醒它,以便做它自己等着唤醒才做的工作。
CreateOptions 和 UpdateOptions 的 .on_expiry("sleep")、.held_asleep(true);SandboxInfo::on_expiry、held_asleep expires_at 到期时做什么:delete 删除沙箱(默认)、sleep(让它休眠并保留,请求照常会唤醒它)或 hold(同上,但保持休眠);以及它休眠时,发往它某个端口的请求是否会唤醒它:保持休眠时,它的预览地址返回 502 sandbox_paused,API 调用会唤醒它。
sandboxes.run(cmd), sandboxes.run_with(cmd, RunOptions, &CreateOptions) 为一条命令创建的沙箱:创建、使用、删除。
sbx.run_with(cmd, RunOptions::new().cwd(…).timeout(d).on_stdout(f)) 运行并等待。CommandResult 包含 stdout、stderr、exit_code、duration、usage、wake。
sbx.exec, sbx.terminal() 启动一条命令(wait()、output()、write()、kill());一个终端。
sbx.files() read、read_text、write、list、stat、exists、mkdir、rename、remove,以及 set_attributes(path, mode, size_bytes, modified_at):文件的权限、长度和最后修改时间;.as_user("node")。
sbx.url(port), sbx.token_with(scopes, expires_in) 端口链接(在本地签名);这个沙箱的令牌。
sbx.info().ipv6, sbx.open_port("tcp", port, &[…]), sbx.ports("tcp"), sbx.close_port("tcp", port), .inbound(InboundPort::tcp(port, […])) 它自己的公网 IPv6 地址(如果有,否则为 None);向列出的来源(IPv6 前缀或地址,"::/0" 表示所有人)开放一个端口,并给出访问地址;开放的端口;关闭一个端口(并说明它之前是否开放);CreateOptions 和 UpdateOptions 中的列表。入站默认关闭;它休眠时和迁移时,开放的端口都保持开放。参见网络。
sbx.revoke_token(token) 从此只拒绝那一个令牌,其他令牌照常有效:令牌本身 (Token::token) 或它的 id(Token::id,tok_…)。一个沙箱最多保留 1,000 个尚未过期的这类吊销(超出时返回 409 too_many_revoked_tokens:这时请用 revoke_tokens())。返回一个 TokenRevocation,带有 token_id 和 expires_at。
sbx.revoke_tokens() 到目前为止为这个沙箱发出的所有令牌都会失效 (401 invalid_token),包括预览链接和 Cookie 中的令牌,以及 SSH 会话持有的令牌;之后创建的令牌有效。需要 API 密钥。返回一个 TokenRevocation:sandbox_id、generation、revoked_at。
sbx.update, sbx.sleep(), sbx.restart(), sbx.delete(), sbx.snapshot(), sbx.fork(n), sbx.restore(…) 修改;休眠;从磁盘启动;删除;创建快照;分叉;恢复。
CreateOptions::new().checkpoint(10), UpdateOptions::new().checkpoint(10), .no_checkpoint(); info().checkpoint 运行期间的检查点(间隔 1 到 600 秒;0 表示十秒),这样机器丢失时它能带着进程继续运行;关闭检查点;检查点的进行情况。没有机器支持检查点时返回 409 checkpoints_unavailable。
info().disk_writes 它的磁盘今天(UTC 日)已写入的量,与当天全速份额的对比(一个 Option<SandboxDiskWrites>):today_bytes,其中 checkpoint_copy_bytes 是检查点副本写入宿主机磁盘的量;full_speed_until_bytes,即份额(默认 100 GiB,节点另有设置时除外;None 表示节点不限速,但照样统计);limited 和 limited_until:超出份额后,它的磁盘以每秒 2 MiB 的速度接受写入,直到当天结束,任何写入都不会失败。在不把磁盘复制到对象存储的节点上为 None,那里什么都不统计。写入它的云盘的数据不计在这里:写入云盘不收费。
sandboxes.snapshots(): get, list, list_with(&ListOptions::new().sandbox(id)) 快照,可以再次查找;snap.rename(Some(name))(None:不要名称)、snap.expire_at(Some(t))(None:永不删除)、snap.delete()。
sandboxes.ssh_keys(): create(public_key), create_with(public_key, &SshKeyOptions), get, list, list_with 通过网关打开沙箱的 SSH 密钥 (ssh <sandbox>@<gateway>);私钥会在发送任何东西之前就被拒绝。

云盘

云盘是项目的一块磁盘,不随它的沙箱消失:同一时间由一个沙箱以读写方式挂载,可以在沙箱创建时挂载,也可以在沙箱运行时挂载;沙箱没了,云盘连同其中的内容一起保留,供下一个沙箱挂载:可以在它的区域,也可以在别的区域,只要那里的机器能读取它归属区域的对象存储(见下文)。云盘在写入的同时被复制到所在区域的对象存储中,所以其他节点上的沙箱也能挂载它:会先把它搬过去。只按写入的数据计费 (used_bytes)。云盘的快照是它在对象存储中的一个版本,不复制任何数据:分叉是从快照创建的新云盘,恢复让快照的内容重新成为云盘的内容,任意多个沙箱都可以同时以只读方式挂载一个快照。云盘属于控制平面 (/v1/drives):对着一个独立运行的节点,每个调用都会得到该节点的 404 no_such_route,节点上的磁盘是存储卷(见下文)。

use glac::DriveMount;

let drive = client.drives().create_with(&DriveOptions::new().size_bytes(1 << 30))?; // Drive
let sbx = client.sandboxes().create_with(&CreateOptions::new().drive(DriveMount::new("/data", drive.id())))?;
sbx.run("echo kept > /data/note")?;
client.drives().detach(sbx.id(), drive.id())?;
let other = client.sandboxes().create()?;
client.drives().attach(other.id(), &DriveMount::new("/data", drive.id()))?;
println!("{}", other.run("cat /data/note")?.stdout); // kept
调用 作用
drives.create_with(&DriveOptions::new().name(…).size_bytes(…).region(…).durability(…)), drives.get(id_or_name), drives.list(), drives.list_page(&ListOptions) 创建一个云盘,里面只有文件系统;每个选项都可以省略(size_bytes:4 KiB 块的整数倍,最小 16 MiB;默认 64 GiB,免费版上为 5 GiB;region:na、eu 或 ap;durability:standard 或 strict,后者要等对象存储收到 fsync 之前的每一次写入,客户机的 fsync 才返回)。查找一个;列出,最新的在前;获取一页。
drives.rename(id_or_name, Some(name)), drives.delete(id_or_name) 重命名(None:不要名称);删除云盘及其中的内容。它的快照会保留。
drives.attach(sandbox, &DriveMount::new(path, drive)), drives.detach(sandbox, drive), drives.force_detach(sandbox, drive) 立即以读写方式把它挂载到一个已有的沙箱中(休眠的沙箱会被唤醒);卸载它(有进程在使用时用 force_detach)。每个调用都返回云盘。带快照的挂载 (DriveMount::snapshot) 用 attach_snapshot,attach 会在发送任何东西之前就拒绝它;不带快照的 mode("snapshot") 会被 API 拒绝 (400 missing_field)。
drive.attach(sandbox, path), drive.detach(sandbox), drive.rename(name), drive.refresh(), drive.delete() 同样的调用,也可以在 Drive 上进行,它的 info() 是一个 DriveInfo:id(vol-…:云盘就是存储卷)、name、size_bytes、used_bytes、attached_to(DriveAttachment:沙箱、路径、模式)、region、home、status、lease(DriveLease:唯一写入方所在的节点、纪元和到期时间)、app_id 和 key(键控实例的云盘)、parent_drive_id 和 root_drive_id(来源云盘和这一脉分叉中的第一个云盘,来源云盘之后可能已被删除;不是分叉的云盘为 None)、managed_by(一个 ManagedBy,即这个云盘所属数据库的 id;不属于任何东西时为 None)、created_at。数据库的云盘只能由数据库的实例以读写方式挂载,重命名、恢复和删除都要通过数据库进行:在这里调用这些操作会被拒绝 (409 managed_by_database);项目可以为它创建快照和分叉。构成数据库的应用也会以同样的方式说明这一点 (app.info().managed_by),它的更新、部署、回滚和删除同样会被拒绝。
drives.snapshot(drive), drives.snapshot_with(drive, &DriveSnapshotOptions::new().name(…).fs_consistent()) 创建一个快照,即一个 DriveSnapshot:由云盘所在的节点生成的、云盘在对象存储中的一个版本,不复制任何数据。运行中的沙箱持有的云盘,会在写入被暂停片刻时创建快照:要么暂停它的机器(consistency 为 paused,云盘处于相当于断电时的状态),要么在带 fs_consistent() 时由沙箱的代理程序冻结它的文件系统(frozen,挂载时无需重放日志;代理程序做不到时返回 409 not_frozen)。最多十秒,之后返回 503 unavailable(可重试),客户机照常运行。没有运行中的沙箱持有的云盘按原样创建快照 (at_rest)。名称由 1 到 63 个小写字母、数字和连字符组成 (400 invalid_name)。
drives.snapshots(drive), drives.all_snapshots(drive_deleted), drives.get_snapshot(drive, snapshot), drives.delete_snapshot(drive, snapshot) 一个云盘的快照,最新的在前;项目中所有云盘的快照(Some(true):只列出已删除云盘的快照);按 id 查看一个 (404 snapshot_not_found);删除一个,计费到那一刻为止:从它分叉出的云盘会保留所需的数据,以只读方式挂载它的沙箱会保留挂载,直到卸载。
drives.fork(drive), drives.fork_with(drive, &DriveForkOptions::new().name(…).from_snapshot(…)) 一个新云盘,它的第一个版本是快照的内容,或云盘当前的内容:不复制任何数据,可以立即挂载,大小与来源云盘相同,带有 parent_drive_id 和 root_drive_id。和创建一样受套餐云盘数量的限制 (403 drive_limit_reached);名称已被另一个云盘或存储卷使用时返回 409 name_taken。
drives.restore(drive, snapshot) 在云盘最新的版本之后写入一个新版本,内容与快照相同。中间的版本只要有快照或分叉保留着就仍然存在,所以先恢复到较旧的快照、再恢复到较新的快照也可以。沙箱以读写方式持有云盘时会被拒绝(409 drive_attached:先卸载它)。返回云盘。
DriveOptions::new().history_seconds(n), drives.set_history(drive, Some(n)), drives.versions(drive), drives.version_at(drive, t), DriveForkOptions::new().at(t), drives.restore_to(drive, t); drive.set_history, drive.versions(), drive.restore_to(t) 云盘的版本窗口 (slice DB8):按时间最多能往回恢复或分叉多远,3600 到 86400 秒(不设置时没有窗口;关闭后,它保留的内容会被丢弃)。在每个有写入的 30 秒里,窗口都会得到一个版本,即云盘在某一时刻的状态;这些版本单独保留的数据和快照一样计费。按时间恢复或分叉时,取该时间点或之前最新的版本,也就是以该时间调用版本列表时给出的那个版本。versions 返回 DriveWindowVersion,最新的在前;set_history(drive, None) 关闭版本窗口;DriveInfo::history_seconds、history。
DriveOptions::new().cache_bytes(n), drives.set_cache(drive, Some(n)); drive.set_cache(Some(n)), drive.info().cache_bytes, drive.info().cached_bytes 云盘的缓存:节点在 NVMe 上最多保存云盘的多少数据。默认全部保存(即云盘的大小);也可以设得更小,最小到 0,超出的部分只保存在对象存储中,客户机读到时再从那里读取(最久没被读过的数据最先释放,对象存储还没有的数据从不释放)。缓存按小时、按已填充的量计费,与云盘的数据费用分开。调小缓存后,超出的部分一旦已在对象存储中就会被释放(如果运行中的沙箱挂载云盘时是全部缓存,则从那个沙箱下一次休眠或停止时开始);调大缓存后,从那时起客户机读到的数据都会留在缓存中。API 把云盘叫作存储卷 (/v1/volumes);各客户端通过 /v1/drives 访问它们,这个接口的响应和以前一样。set_cache(drive, None) 恢复为全部缓存。控制平面没有给出缓存信息时,DriveInfo::cache_bytes 就是云盘的大小;cached_bytes 是当前在节点 NVMe 上的量(没有节点持有云盘时为 None)。用量报告的分组和合计中有 cache_gb_hours。
drives.attach_snapshot(sandbox, &DriveMount::new(path, drive).snapshot(id)), drives.detach_snapshot(sandbox, snapshot), drives.force_detach_snapshot(sandbox, snapshot) 立即以只读方式把快照挂载到一个已有的沙箱中(不带快照的挂载会在发送任何东西之前就被拒绝);按快照的 id 卸载它,无论云盘和快照此后是否已被删除(有进程在使用时用 force_…)。每个调用都返回 DriveSnapshot。创建沙箱时:CreateOptions::new().drive(DriveMount::new("/data", drive.id()).snapshot(snap.id()))。
drive.snapshot(), drive.snapshot_with(…), drive.snapshots(), drive.fork(), drive.fork_with(…), drive.restore(snapshot); snap.refresh(), snap.delete(), snap.fork(Some(name)), snap.restore(), snap.attach(sandbox, path), snap.detach(sandbox), snap.force_detach(sandbox) 同样的调用,也可以在 Drive 和 DriveSnapshot 上进行,后者的 info() 是一个 DriveSnapshotInfo:id (dsnap-…)、drive_id(可能已被删除)、name、consistency、used_bytes(创建时云盘中的数据量)、exclusive_bytes(它单独保留的数据量,也是计费依据;在存储的回收程序算出之前为 0)和 exclusive_at、version(一个 DriveVersion:number、epoch)、created_at。
  • 一个云盘同一时间只属于一个沙箱:另一个沙箱持有的云盘会被拒绝 (409 drive_attached),删除已挂载的云盘也一样。键控实例的云盘随它的键一起删除 (409 key_drive)。
  • 另一个节点持有的云盘,会先被搬到沙箱所在的节点;对象存储中还没有它的最新状态时 (503 copy_behind),或无法把它搬过去时 (503 drive_not_brought),调用为 retryable:客户端会按 retries 的设置重试,之后返回错误,你稍后再请求一次即可。
  • 跨区域 (slice D9)。云盘的归属区域 (home) 是保存其副本的对象存储所在的区域:也就是创建它的区域。另一个区域的沙箱也能挂载它,前提是那个区域的机器能读取其归属区域的存储(不能时返回 409 not_on_one_machine):云盘被释放为一个版本,在那里被接管,客户机读取时从归属区域的存储读出,客户机写入的数据则进入它现在所在区域的存储 (region)。云盘在那里待满一小时后,归属区域会跟过去,用 drives.move_home(drive) (drive.move_home()) 则立即迁移:它的机器把归属区域存储中剩下的内容复制到自己区域的存储中,那里什么都不剩时,home 就变成新区域;DriveInfo::home_move 说明迁移进行到哪一步(asked_at、asked_by、copied_objects、copied_bytes、objects_left、error)。保留的版本都不再需要旧归属区域的副本后(一天后),这份副本会被删除。云盘的归属区域最多每六小时迁移一次 (409 home_moved_recently);归属区域就是当前所在区域的云盘返回 409 home_here。
  • 套餐的限制:免费版最多 3 个云盘,单个最大 10 GiB,合计 15 GiB;付费版最多 500 个,单个最大 1 TiB (403 drive_limit_reached)。
  • 快照不随云盘消失:云盘删除后,仍可以按云盘的 id 列出、读取、分叉它的快照,或以只读方式挂载。快照按它单独保留的数据 (exclusive_bytes) 计费,按小时计,价格与云盘的数据相同(每 GB·月 $0.02)。
  • 分叉和任何云盘一样,按它的 used_bytes,即完整的逻辑内容计费:从 10 GB 快照分叉出的云盘按 10 GB 的云盘计费。
  • 快照的只读挂载:在云盘所在区域的任意机器上,任意多个沙箱都可以同时挂载,无论云盘被谁持有,云盘删除后也可以。数据块在客户机读取时从对象存储取来:这些冷读不收费,并在用量中计在那个沙箱名下(UsageOptions::sandbox(id),drive_cold_read_bytes);读取那个沙箱的机器上已有的数据也不收费。客户机不能写入它,挂载了快照的沙箱也不能迁到别的机器上。应用的版本只能以读写方式挂载云盘:快照的只读挂载只供沙箱使用。
  • 节点不向对象存储复制任何数据时,云盘没有快照、分叉和恢复 (409 not_copied)。
  • UsageOptions::drive(id)、kind("drive") 和 group_by(&["drive"]) 报告云盘保存了多少、写入了多少、冷读了多少。drive.created、drive.deleted、drive.attached 和 drive.detached 事件携带 EventDrive 或 EventDriveAttached。

client.volumes() 对应 API 的 /v1/volumes,即云盘底层的磁盘,为过渡期而保留:只有它才有的,是独立运行的节点(节点上的磁盘就是存储卷),以及共享存储卷,即可由多个沙箱同时挂载的文件系统。控制平面不会为项目的密钥创建普通存储卷。

调用 作用
client.volumes().create_with(&VolumeOptions::new().name("team").shared()), volumes.get(id_or_name), volumes.list(), vol.delete() 创建存储卷、查找一个、列出、删除一个。沙箱用 CreateOptions::new().volume(VolumeMount::new("/team", vol.id())) 挂载存储卷;有沙箱持有的存储卷不能删除 (409 volume_in_use)。

存储卷的备份。备份是存储卷(云盘也是存储卷)在某一时刻的副本,保存在对象存储中,直到过期:可以由调用创建,可以按存储卷的规则每天创建,也会在服务的新版本替换其实例之前创建。与云盘的快照不同,它是一份独立的副本,仅凭它就能创建存储卷,而且不随存储卷消失;存储卷的备份只保存自上一个备份以来变化的内容,多个备份都需要的数据块只计费一次,计在最早的备份上。在存储收到全部数据之前为 making,之后为 ready,从那时起按存储中保存的数据量 (stored_bytes)、以休眠沙箱所保存内容的价格计费(备份)。备份属于控制平面。

调用 作用
client.volumes().backup(id_or_name), backup_with(id_or_name, &BackupOptions::new().expires_in_days(7)), vol.backup(), client.volumes().backups(id_or_name) 立即创建一个备份,即一个 Backup,保留这么多天(1 到 365),或按存储卷的规则(未设置时为 30 天);列出一个存储卷的备份,最新的在前(已删除的存储卷按 id 指定)。不向对象存储复制任何数据的节点没有备份 (409 not_copied)。
client.backups().list(), list_of(volume_id), get(id), delete(id), restore(id, Some("data-back")) 项目的备份,最新的在前,包括已删除存储卷的备份;一个存储卷的备份;查看一个;删除一个(之后不再为它计费;从它创建的存储卷仍然保留);从一个就绪的备份创建新存储卷,等同于 client.volumes().create_with(&VolumeOptions::new().from_backup(id))(创建期间返回 409 backup_not_ready,失败后返回 409 backup_unusable)。
b.refresh(), b.restore(name), b.delete() 同样的调用,也可以在 Backup 上进行,它的 info() 是一个 BackupInfo:id (bak-…)、volume_id、volume_name(当时存储卷的名称)、status(making、ready、failed)、reason(manual、daily、before_version)、consistency(frozen、crash)、frozen_ms(存储卷的写入方为复制等待了多久;没有冻结时为 None)、size_bytes(存储卷当时的数据量)、volume_size_bytes(它最多能容纳的量,也就是从备份创建的存储卷的容量)、stored_bytes(计费依据)、own_bytes(删除它能省下的量)、shared_bytes、shared、region、failure、created_at、ready_at(从此开始计费)、expires_at。
  • 从备份创建的存储卷,大小和类型与备份所来自的存储卷相同,位于备份所在的区域。它的 status 在完整拿到备份之前为 restoring,之后为 ready,在此之前不能挂载;from_backup 指明来源备份。如果被分配到的机器无法恢复它,控制平面会在该区域的另一台机器上再创建一次:这时 restore_attempt 为 Some(2),first_failure 说明第一次为什么失败。如果这一次也失败,它就是 failed (failure),只能删除。
  • UsageOptions::new().backup(id)、kind("backup") 和 group_by(&["backup"]) 报告备份保存了多少,计在状态计量项上 (state_gb_hours);client.billing() 的 meters.state.backups_gb_hours 说明该计量项中有多少来自备份。

域名

项目的域名是一个指向应用(和它的默认地址一样)或沙箱某个端口的名称。域名属于控制平面。

调用 作用
client.domains().create(domain, &DomainOptions::new().app(…)) 或 .sandbox(…).port(…),配合 .proxied(…);get、list()、list_of("app_id", app)、delete;domain.update(&DomainOptions…)、domain.refresh()、domain.delete() 为应用或沙箱端口注册的名称,在 DNS 上能查到 info().verification 中的某条记录之前为 pending_verification(自定义域名);项目中已有该域名时返回 409 domain_exists。

浏览器

尚未开放。浏览器还没有向账户开放:下面是开放之前 SDK 目前的样子。

会话是一个托管的 Chromium,由 Chrome DevTools Protocol (CDP) 客户端在它的 cdp_url 上从外部驱动:也就是 Playwright 的 connectOverCDP 和 Puppeteer 的 browserURL 接受的地址。有客户端连接时它保持运行,最后一个客户端离开几秒后进入休眠,页面会保留。地址里带着凭据,所以 {:?} 不包含地址。

调用 作用
client.browsers().create(), create_with(&BrowserOptions::new().name("agent-42")) 创建一个处于运行状态的新会话;BrowserOptions 有 kind(默认为 chromium-headless;chromium 有屏幕;lightpanda 是没有布局引擎的浏览器)、name、region、sleep_after_seconds、connection_idle_seconds、delete_after_idle_seconds、metadata、profile(name, save) 和 profile_version(name, version, save)(在返回会话之前加载项目的一个配置文件;设置了 save 时,会话删除时再保存回去)、proxy(url)(你自己的 HTTP 代理,需要接受 CONNECT,付费版可用:会话到 80 和 443 端口的连接经由浏览器旁边的一个中继从它出站,凭据由平台保管;免费版上返回 403 proxy_not_on_plan)。如果用已有会话的名称创建,会返回那个会话:它会被唤醒,页面都还在。沙箱在运行前就失败的会话不算会话:创建会被拒绝,返回 503 session_failed。
client.browsers().get(id_or_name), delete(id_or_name), delete_without_saving_profile(id_or_name) 查找一个;结束它(用需要保存的配置文件创建的会话会先保存它,除非结束时指定不保存)。BrowserSession 包含 id、kind、status、cdp_url、live_url、profile(name、save、version)、proxy(它自己的代理的 url 和 username,从不包含密码)、它的时间设置、awake_seconds,以及 proxy_bytes,即双向经过其代理的流量(proxy_sent_bytes、proxy_received_bytes)。
session.status, session.error awake、asleep、unavailable(它的节点无法访问)或 failed(创建得到响应之后它的沙箱失败了:比如浏览器停止且没有恢复);error 说明原因,一个 BrowserError(code、message,按节点的原话),没有失败时为 None。browser.failed 事件也会说明这一点:EventBrowserFailed::of(&event) 读取它的数据。

会话和任何实例一样,按它使用的方块资源计费:计算时的 CPU、运行时的内存和出站流量(价格与套餐)。session.awake_seconds 是它已运行的时长。

列出、修改、让会话休眠和换新地址,只有 Python 和 TypeScript SDK 以及 glac browsers 才有。会话属于控制平面。

数据库

数据库是托管的 Postgres:运行在云盘上的独立 PostgreSQL,没有任何请求时休眠,有连接时被唤醒,会话都会保留。所有者的密码只出现在 create 和 reset_password 的响应中,任何地方都不保存:只有它们返回的 Database 的 connection_string() 才带有密码。

调用 作用
client.databases().create("shop"), create_with("shop", &DatabaseOptions::new().version("18")) 创建一个数据库并等待它第一次启动(status 为 ready,或为 creating:这时反复 get 它,直到就绪);DatabaseOptions 有 version、region、database_name、size(vcpus, memory_bytes)、storage(size_bytes, durability)、sleep_after_seconds、suspend_after_seconds(保存内存、真正休眠之前暂停的秒数,暂停期间连接在一两毫秒内就能得到响应:1 到 86400,默认 300)、wake_daily_at("03:30")(休眠期间每天在那时唤醒)、metadata。用已有数据库的名称创建时,返回那个数据库,不带密码。databases.set_suspend_after(db, Some(n)) 和 set_wake_daily_at(db, Some("04:00")) 修改这两项,所有分支一起修改;None 把其中任意一项恢复默认(分别为 300 和不唤醒)。免费版数据库的休眠设置是固定的,与 Neon 免费版缩容到零的做法相同:最后一个查询一秒后暂停(sleep_after_seconds 为 1),五分钟后休眠(suspend_after_seconds 为 300);无论创建还是修改,其他值都会以 400 invalid_field 被拒绝。
client.databases().get(id_or_name), list(), list_named(name), delete(id_or_name, final_snapshot) 查找一个;列出;删除一个数据库及其分支(除非 final_snapshot 为 false,否则它的云盘会保留一个快照七天)。
client.databases().branches(db), branch(db, "main"), sleep(db, "main"), wake(db, "main"), reset_password(db, None) 它的分支(先是 main,然后是独立分支)、其中一个分支、立即让某个分支休眠或唤醒它,以及为它的所有者设置新密码,只出现在那次响应中。
client.databases().create_branch(db, "preview", &DatabaseBranchOptions::new().parent("main").expires_at(t)), delete_branch(db, branch) 独立分支:它的父分支(默认 main)此刻的状态,不复制任何数据,有自己的 PostgreSQL、端点和云盘,在项目网络上的地址是 <database>-<name>;给出过期时间时,到时会自行删除;它的角色和密码与父分支相同 (branch.connection_string(Some(owner_password)))。删除会停止它的 PostgreSQL 并删除它的云盘;不能删除 main (409 root_branch),也不能删除有其他分支从它创建的分支 (409 has_branches)。
DatabaseOptions::new().history_seconds(n), databases.set_history(db, n), databases.restore(db, t), databases.restore_exactly(db, t), DatabaseBranchOptions::new().at(t), at_exactly(t) 数据库的历史(slice DB8 和 DB5):根分支按时间最多能恢复到多久以前,或从多久以前创建分支:默认一天;付费版可设为 0(不保留)到 30 天,免费版为一天。恢复会把根分支原地带回过去:恢复到整分钟或某个版本自身的时刻时,使用云盘版本窗口中该时刻或之前最新的版本(每个有写入的 30 秒一个版本,最多保留一天);更早的时刻、精确到秒的时刻,或用 exactly 指定的那个精确时刻(精确到毫秒),则通过 WAL 归档恢复(把 WAL 重放到那一刻:需要几秒到几分钟,调用会一直等待)。端点、连接字符串和所有者的密码都保持不变,现有连接会断开,恢复之前的状态会作为一个独立分支 main_old_<the restore's moment> 保留;如果新版本在五分钟内不能提供服务,恢复会被撤销 (503 database_restore_failed)。按时间创建分支时,则以那个时刻创建分支 (from: {at, exact})。历史中没有的时间返回 400 invalid_history,错误信息会说明版本窗口和归档分别能追溯到什么时候。精确到秒的时间(或带 exact)只能从归档恢复,否则拒绝,从不退而使用窗口中之前的版本:归档追溯不到时返回 400 invalid_history(数据库不保留归档,或时间早于最早的基础备份),归档的存储拒绝提供分段时返回 503 database_archive_unavailable(可重试);每次拒绝都会指明窗口中该时间或之前的版本,你可以改为请求那个版本自身的时刻。存储给不出所需数据块时,返回 503 drive_unreadable(可重试),不会创建任何东西。如果对象存储中断太久,等待归档的 WAL 超过了云盘为日志预留的空间,这部分 WAL 会被放弃,以保证数据库继续运行:这就是归档中的一个缺口,记录在 history.gaps 中(DatabaseHistoryGap:from、to、from_lsn、to_lsn):被放弃的 WAL 从何时开始,归档到何时为止无法恢复任何时间(缺口之后第一个基础备份完成的时间;在有基础备份之前为 null),以及缺口在日志中的起止位置。缺口内的时间只能从云盘的版本窗口恢复(整分钟,或某个版本自身的时刻);精确到秒或精确时刻的恢复返回 400 invalid_history。缺口也会以事件的形式报告:首次发现缺口时发出 database.history_gap(数据库运行期间,每个缺口一次),缺口之后归档重新可用于恢复时发出 database.history_resumed;EventDatabaseGap::of(&event) 读取它们的数据(其他类型的事件返回 None):数据库,缺口的 from、to(前者为 None)、from_lsn 和 to_lsn,segments(放弃了多少个 16 MB 的分段),以及前者才有的 error(对象存储最后一次拒绝时返回的内容)。相关字段:Database::history_seconds、history。
DatabaseOptions::new().tiering(true), databases.set_tiering(db, on) 开启或关闭分层存储(slice DB18:付费版可用,且所在区域要有对象网关;有分支的分区已经分层时,关闭会被拒绝,返回 409 tiered_data_exists;每个分支的 PostgreSQL 会为此重启一次)。Database::tiering 说明是否已开启,以及目前按多少计费 (tiered_bytes)。
client.databases().roles(db), reset_role_password(db, role) 根分支中可登录的角色,直接从 PostgreSQL 读取(DatabaseRole:name、owner):它的所有者和你创建的角色 (CREATE ROLE … LOGIN),平台自己的角色不在其中;以及为其中任何一个角色设置新密码,只出现在那次响应中:一个 DatabaseRoleSecret,带有该角色的 password 和根分支上该角色的连接字符串 (connection_string());所有者的连接字符串由 API 响应的数据库生成。独立分支保留它创建时的密码。
db.connection_string(), branch.connection_string(Some(password)) tokio-postgres、sqlx 和 psql 接受的连接字符串:API 给出公网连接字符串时用公网的,否则用项目网络上的,并带有所有者的密码(百分号编码)。db.default_branch 包含 connection_uri、pooled_uri、private_uri、endpoint 和 state。

数据库在项目网络上按名称访问,来自它的沙箱(用 project_network 创建)和服务:这就是它的 private_uri。有边缘节点提供 PostgreSQL 服务时,也可以从任何地方通过它的公网地址访问(connection_uri,端口 5432,只支持 TLS;连接池的地址:pooled_uri);没有边缘节点提供、或数据库没有可用来命名的区域时,它们为 None;给出时,连接字符串会使用它们。数据库或分支的 {:?} 既不显示密码,也不显示带密码的连接字符串。数据库属于控制平面。

托管应用

目录中的应用(比如 Rivet)以它自己的方块资源安装到项目中:它的应用、存储卷、机密和存储桶,都带有该安装的标签,只能通过这个安装修改,并按这些资源本身计费(托管应用)。安装的响应包含全部输出,包括机密输出(管理员令牌、带有令牌的端点),之后的读取只对有 admin 权限范围的密钥显示这些输出:安装对象会保留之前的响应给出、而之后的响应隐藏的每个输出。

let rivet = client.catalogue().get("rivet", None)?;       // its parameters, and a month of it in Cube's meters
println!("{:?}", rivet.month);
let engine = client.installations().create("rivet", &InstallationOptions::new().parameters(json!({"disk_gib": 20})))?;
println!("{}", engine.info().outputs["endpoint"]);        // RIVET_ENDPOINT, its token in it
调用 作用
client.catalogue().list(), client.catalogue().get(app, version) 目录中的应用,每个都是一个 CatalogueApp(app、version、title、summary、verified、licence、versions、parameters:一个 JSON Schema,outputs、month,完整的条目在 raw 中);查看一个应用的最新版本或指定的版本,它的定义在 raw 中。
client.installations().create(app, &InstallationOptions::new().name(..).version(..).region(..).parameters(..).requires(name, installation).no_wait()) 安装它,并等它就绪(no_wait:立即返回)。安装失败时返回类型和错误码都为 installation_failed 的错误,附带错误信息。
client.installations().list(app), client.installations().get(id_or_name) 项目的安装,最新的在前(None:全部);查看一个。
installation.update(&InstallationUpdate::new().version(..).parameters(..)), delete(), refresh(), wait(), wait_for(limit) 换一个版本,或以合并补丁的方式修改参数(null 把参数恢复为默认值);连同它的所有组成部分一起删除(status():deleted 或 deleting);wait_for 以 wait_timeout 结束时,安装仍会继续。
installation.id(), name(), status(), info() 按 API 给出的样子:info() 包含 outputs、secret_outputs、parameters、apps、volumes、buckets、requires、failure。

托管应用属于控制平面。

机密与变量

调用 作用
client.secrets(): put_with("DB_URL", &url, Some(&["db.example.com"])), put, update(name, hosts), list, delete API 按名称保存、永远不会返回的值,以及每个值可以发往的主机:用于应用的环境变量、拉取镜像的机密、沙箱的出站规则。
client.variables(): put(name, value), list, delete 项目的共享变量,服务的 env 引用它们 ("${{ shared.REGION }}")。属于控制平面。

项目、账户与账单

调用 作用
client.account(), client.update_account(&AccountOptions) 账户:本月发往互联网的流量,以及这类流量在哪里停止;通知发往哪里(email,用 AccountOptions::new().email(…) 设置;email_verified,以及发往它的邮件被退回期间的 email_undeliverable,一个 EmailUndeliverable:参见通知发往哪里);通知的语言(language,一个 AccountLanguage:code 为 “en”、“de” 或 “fr”,以及它的 source;AccountOptions::new().language("de"),或用 no_language() 重新跟随账单地址)、账单地址和税号;以及在控制平面后面的 keeps(一个 Option<AccountKeeps>):密钥所属项目在云盘和存储卷上的保有量,按创建时所受限制的数字给出,每项都附带已有的量(volumes、volume_bytes、volumes_bytes,其 limit 为 None 表示不限,以及 unsized_bytes:参见提高限制)。
client.usage_report(&UsageOptions), client.usage(&ListOptions) 用量,来自按小时的账目,可以分组;每个沙箱的用量。
client.billing(), client.update_billing(&BillingOptions), client.balance(), client.update_balance(&BalanceOptions), client.top_up(usd), client.top_up_with(usd, &TopUpOptions), client.charges(), client.ledger() 套餐和账单、余额、自动充值和充值、扣款记录和账目。属于控制平面。
client.project(), client.events_page(&EventsOptions) 密钥所属的项目,以及它的事件的一页。属于控制平面。

事件的数据。控制平面的每种事件,在这个 crate 中都有一个对应其数据的类型,以描述中该事件的 schema 命名,它的 of(&event) 读取对应类型的事件,其他类型的事件返回 None (glac::EventDatabase::of(&e))。数据库的事件: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 等)。某类事件并非都有的字段,或为 null 的字段,类型为 Option。

Webhook、API 密钥、支出上限、项目。client.webhooks().create_with(url, &WebhookOptions::new().events(&[…]).enabled(true)) 把项目的事件发到你的地址(为每次投递签名的 secret 只出现在这次响应中);还有 get、list、update、delete。client.keys().create_with(&KeyOptions::new().name(…).scopes(&[…])) 创建一个 API 密钥(密钥本身 key 只出现在这次响应中);还有 get、list、delete。client.limits() 和 client.update_limits(Some(50.0)) 是项目的支出上限和本月已支出的金额(None 去掉上限),sbx.limits()、sbx.update_limits(…) 则是某个沙箱自己的。client.request_limit(limit, wanted, why) 申请提高账户的某项可应要求提高的限制(按名称指定的上限、"volumes" 或 "volume_bytes";后两者中的大小以字节为单位;同一项限制已有申请在等待时返回 409 limit_request_pending),client.limit_requests(limit, cursor) 和 client.limit_request(id) 读取申请及运营方的答复(LimitRequest:status 为 pending、granted 或 refused,granted、answer)。EventInstanceRestarted::of(&event)、EventInstanceUpgrade::of(instance.upgraded、.reverted、.upgrade_failed)和 EventLimitRequest::of 读取这些事件的数据;instance.start_command()(沙箱的 info().start_command)说明它的启动命令如何运行、在机器中被重新启动了多少次,以及上一次是如何结束的。client.update_project(Some(30)) 设置沙箱休眠多少天后被删除(None:永不删除)。属于控制平面。Webhook 和 API 密钥需要有 admin 权限范围的密钥,设置项目的上限和 update_project 也一样;设置沙箱的上限需要 write。

日志导出目标、审计日志、计费文件。client.destinations() 是项目中应用的日志(以及指标和请求的追踪:.signals(&["logs", "traces"]))导出的地方(OpenTelemetry 端点):create(name, &DestinationOptions::new().endpoint(u).header(name, value)) 创建时会立即用一条记录测试 (status.connected),还有 get、list、update、delete、test(立即发送一条记录);请求头的值是凭据,只在创建或修改导出目标时发送,不会出现在任何响应中(返回的内容只有请求头的名称)。client.audit_page(&ListOptions::new().limit(n)) 是项目审计日志的一页,最新的在前(谁、什么方法、什么路径、什么状态):每一次不是读取的调用(被拒绝的也算),以及运营方对应用日志的每一次读取。client.statement(Some("2026-09")) 是某个月的对账单(None 表示本月),其中每个数据库单独一行(databases:它这个月的用量,其中历史和分层数据单列,以及按标价折合多少),client.invoices() 是 Stripe 上的发票(附 PDF 链接),client.notices() 是发给账户的邮件,client.withdrawals(&ListOptions::new()) 是现在可以撤回的购买和已做的撤回(欧洲经济区或英国的消费者可以在 14 天内撤回一笔购买),client.withdraw(charge_id, name, Some(email)) 是买方本人的撤回声明,立即受理。属于控制平面。创建、修改、测试和删除导出目标、读取审计日志以及撤回,需要有 admin 权限范围的密钥;其他调用只需要有读取权限的密钥。

立即购买 (start_now)。欧洲经济区或英国的消费者(没有已验证或正在验证的税号的账户)可以在 14 天内撤回一笔购买,所购内容只有在其本人明确要求时才会立即提供。start_now() 就带有这一要求:client.top_up_with(20.0, &TopUpOptions::new().start_now())、BillingOptions::new().plan("paid").start_now()(从免费版购买付费版)和 BalanceOptions::new().auto_reload(true).start_now()(自动充值保持开启期间的每一次自动充值),措辞与控制台在勾选框旁显示的相同(GET /v1/billing/withdrawals 中也有,见 right.start_now):

我要求在可以撤回这笔购买的 14 天内立即提供所购内容。如果我撤回,我需要为此前已提供的部分付费。我理解,一次充值的点数全部用完后,即视为已全部提供,我将不能再撤回。

服务立即开始;在 14 天内,买方仍可撤回,并为此前已提供的部分付费;一旦全部提供(一次充值的点数全部用完),撤回权即丧失。只有在购买者本人提出这一要求时才加上它:SDK 不会自行发送它。没有它时,这类账户的购买会被拒绝:返回一个 Error,状态为 400,错误码为 start_now_required,param() 为 start_now,错误信息会指明账户所在的国家并引用这一要求的原文,什么也不会买。开启了自动充值却没有这一要求的账户不会自动充值,并且每月会收到一次通知(auto_reload.no_request 事件,用 EventAutoReloadNoRequest::of 读取;没有绑定银行卡的账户则是 auto_reload.no_card 和 EventAutoReloadNoCard)。

错误

非零退出码是一个结果:读取 res.exit_code 即可。API 的错误是一个 glac::Error,带有 status()、kind()、code()、param()、retryable()、request_id() 和 message(),以及指向错误页面上对应错误码的 docs_url()。连不上或超时也是这种错误,status() 为 None;is_timeout() 可以判断是否超时。与费用有关的错误(402 account_in_debt、403 trust_limit_exceeded、409 no_card、400 start_now_required 等)带有控制平面自己的错误信息,说明怎样解除。

值得了解的行为

  • 休眠的沙箱。不会有调用因为沙箱在休眠或正在启动而失败:它会等待,然后继续。res.wake 说明 run 为唤醒等待了多久;客户端上的 on_response 能看到每个调用的这一信息。
  • 重试。没有响应,或出现 retryable 错误时,会重试两次 (retries),间隔从 0.2 秒开始翻倍。每个 POST 都带有 Idempotency-Key;idempotency_key(key) 让重启后的程序能拿回它之前创建的东西。
  • 超时。timeout(默认 15 分钟)是等待 API 的时长;RunOptions::timeout 是命令自己的时限。
  • 线程。Client 可以在线程之间共享,各产品的句柄共用它的连接;Sandbox 上的只读调用接受 &self。在 tokio 中,在有异步版本之前,请用 tokio::task::spawn_blocking(move || sbx.run("make")).await??。
  • HTTPS 使用内置的 Mozilla 根证书;SSL_CERT_FILE 可以加入你自己的证书。到目前为止,它只在纯 HTTP 的开发机器上运行过。
导航

输入以搜索……

↑↓ 切换↵ 选择Esc 关闭