Skip to content

Go SDK

Go SDK,一个客户端,每个产品(方块、沙箱、浏览器、云盘、数据库)一个字段。只依赖标准库,支持 Go 1.21 及以上;context 作为第一个参数,错误作为返回值,使用函数式选项。

更新于

安装

只依赖标准库;需要 Go 1.21 或更高版本。每个调用都以 context.Context 作为第一个参数,错误作为返回值,选项是函数式选项,流是 io.Reader 和 io.Writer。模块是 github.com/GlacAI/glac-go;包名是 glac:

go get github.com/GlacAI/glac-go

客户端

一个客户端,每个产品一个字段:

client, err := glac.NewClient() // GLAC_API_KEY and GLAC_API_URL; or glac.NewClient(glac.APIKey(key), glac.BaseURL(address))
if err != nil {
	log.Fatal(err)
}

app, err := client.Cube.Deploy(ctx, "ghcr.io/acme/api:1.4", glac.Name("api"), glac.Port(8080)) // Cube: waits until it serves
if err != nil {
	log.Fatal(err)
}
fmt.Println(app.URL())

sbx, err := client.Sandboxes.Create(ctx, glac.Image("python:3.12")) // Sandbox
if err != nil {
	log.Fatal(err)
}
res, err := sbx.Run(ctx, "python3 -c 'print(6*7)'")
if err != nil {
	log.Fatal(err)
}
fmt.Print(res.Stdout)

drive, err := client.Drives.Create(ctx, glac.Name("data"), glac.SizeBytes(10<<30)) // Drive
if err != nil {
	log.Fatal(err)
}
fmt.Println(drive.ID)

唯一不需要你自己创建客户端的调用,是在默认客户端上用沙箱运行一条命令(默认客户端在第一次需要时根据环境变量创建):

res, err := glac.Sandboxes.Run(ctx, "echo hello") // one call: make, run, delete
if err != nil {
	log.Fatal(err)
}
fmt.Print(res.Stdout)

每个产品都是客户端的一个字段(go-github 的做法:一个用该客户端发起调用的服务),平台本身的调用是客户端的方法:

位置 内容 调用
client.Cube 方块:作为服务运行的镜像,每个都有自己的地址(API 称之为 app:/v1/apps,类型为 App)。client.Workers 是同一个服务的另一个名字,已标记为弃用 Deploy, Create, Get, GetOrCreate, List
client.Sandboxes 沙箱 (/v1/sandboxes) Run, Create, Get, GetOrCreate, List, ListPage
client.Sandboxes.Snapshots 沙箱的快照 Get, List
client.Sandboxes.SSHKeys 能打开沙箱的 SSH 密钥 Create, Get, List
client.Drives 云盘:项目的磁盘,不随沙箱消失,同一时间由一个沙箱以读写方式挂载;以及云盘的快照,可以同时被任意多个沙箱以只读方式挂载(/v1/drives,属于控制平面;类型为 Drive 和 DriveSnapshot) Create, Get, List, ListPage, Rename, Delete, Attach, Detach, ForceDetach; Snapshot, Snapshots, GetSnapshot, DeleteSnapshot, Fork, Restore, AttachSnapshot, DetachSnapshot, ForceDetachSnapshot
client.Volumes 云盘底层的存储卷(/v1/volumes;类型为 Volume) Create, Get, List, Backup, Backups
client.Backups 存储卷的备份:存储卷在某一时刻的完整副本,保存在对象存储中,不随存储卷消失(/v1/backups,属于控制平面;类型为 Backup) List, ListOf, Get, Delete, Restore
client.Domains 项目的自定义域名(/v1/domains;属于控制平面;类型为 Domain) Create, Get, List, Delete
client.Webhooks, client.Keys 项目的事件发往哪里 (/v1/webhooks),以及项目的 API 密钥 (/v1/keys);属于控制平面 Create、Get、List、Update(用于 Webhook)、Delete
client.Destinations 项目中应用的日志、指标和追踪导出到哪里 (/v1/destinations);属于控制平面 Create, Get, List, Update, Delete, Test
client.Secrets, client.Variables 项目的机密和共享变量 Put、Update(机密的主机)、List、Delete
client.Deploys 项目的部署:一起部署的多个服务(/v1/deploys,属于控制平面;类型为 Deploy) Create, Get, List
client 本身 平台:账户、用量、账单和余额、项目及其事件 GetAccount, UpdateAccount, GetUsage, GetUsageReport, GetBilling, UpdateBilling, GetBalance, UpdateBalance, TopUp, ListCharges, ListLedger, GetProject, ListEvents, Request
glac.Sandboxes 默认客户端的沙箱 (Sandboxes),以及它的 Snapshots 和 SSHKeys 与 client.Sandboxes 的调用相同,使用默认客户端;无法根据环境变量创建默认客户端时,每个调用都会返回原因

密钥从 GLAC_API_KEY 读取,地址从 GLAC_API_URL 读取(默认 https://api.glac.ai)。glac.NewClient(glac.APIKey(…), glac.BaseURL(…), glac.RequestTimeout(d), glac.Retries(n), glac.HTTPClient(…), glac.OnResponse(f)) 可以指定其他地址、密钥、超时或重试次数。SDK 没有封装的 API:client.Request(ctx, "GET", "/v1/webhooks", nil, nil, &out)。

方块

方块(API 称之为 app)是作为服务运行在自己地址上的镜像(这里的类型为 App),实例数量随请求的需要而定,没有请求时一个都没有。每次部署都会产生一个版本,版本就绪后承接应用的流量。

client.Cube.Deploy(ctx, image, …) 的作用与 glac deploy 相同:带 Name 时,该名称的应用会得到这个镜像的一个新版本,没有这个应用时就创建它;不带时,会创建一个新应用,由 API 命名。它会等到版本承接流量,然后返回那时的应用(app.URL()、app.CurrentVersion())。

app, err = client.Cube.Deploy(ctx, "ghcr.io/acme/api:1.5", glac.Name("api"), glac.MinInstances(1)) // the app is there: MinInstances is changed first, then a version is made
var failed *glac.DeployError
if errors.As(err, &failed) {
	log.Fatalf("version %d did not come up: %s", failed.Version.Number, failed.Code()) // the traffic stayed where it was
} else if err != nil {
	log.Fatal(err)
}
fmt.Println(app.CurrentVersion())
  • 部署到已有的应用时,立即生效的设置(Regions、扩缩容、键、WithEgress、Metadata、日志设置、追踪设置、Public、DependsOn、Aliases、EndpointMode)会先修改,与 app.Update 的修改方式相同,而且只在给出其中某些设置时才修改;然后用构成版本的内容(Image、Port、Env、Secrets、StartLine、VCPUs、Volumes、Drives、KeyDrives 等)创建一个版本,与 app.Deploy 的做法相同:没有给出的沿用应用的,而 Env 和 Secrets 就是版本拥有的全部。不存在的应用 (404 app_not_found) 会用全部这些设置创建,与 Create 的做法相同;在此期间被别人创建的应用 (409 name_taken) 按已有的应用部署。
  • 其他调用。client.Cube.Create、Get、GetOrCreate、List;以及应用上的 app.Deploy(一个版本:*AppVersion)、app.Update、app.Version(ctx, "3")、app.Versions、app.Rollback(ctx, "1")、app.Logs(ctx, glac.Follow())、app.LogUsage、app.DeleteLogs、app.Metrics(ctx, from, to, "minute")(为它提供服务的机器对它的统计,每分钟或每小时一步:指标),Create、Deploy 和 app.Update 的 glac.TraceSampleRate(0.1) 和 glac.TrustIncomingTrace(true),以及 app.Info().Traces(平台为应用的一部分请求创建 span,实例自己的 span 是它的子 span,默认不创建,发往项目中接收 traces 的导出目标:追踪),app.Info().LogsState(保存其日志的每台机器,现在是否长期保留那里打印的内容:日志),app.Instance(ctx, key)(为某个键保留的实例),app.CreateKeySecret、app.KeySecrets、app.KeySecret(ctx, id)、app.DeleteKeySecret(用来签名应用的键的机密)和 glac.SignKey(secret, app.ID, key, expires)(签名后的键,即经由边缘节点的请求所携带的形式;glac.PathSegment 把它写成路径段:签名键),app.Delete。
  • 等待。会构建版本的调用会等待版本,除非给出 glac.NoWait()。没能起来的版本是一个 *glac.DeployError,它的 Code() 是版本自己的错误码(boot_failed、image_not_found 等);应用的流量保持不变,应用会随错误一起返回。
  • 运行时设置。glac.Runtime(glac.ServiceRuntime{…}) 为版本提供运行时设置:按 Docker 的名称设置的进程限制、Docker 允许容器设置的内核参数、内存中的文件系统、解析器以及 /dev/shm 的大小,整体替换。
  • 检查点。glac.Checkpoint(n)(每 n 秒一次,1 到 600;0 表示用默认值,每 10 秒一次)让应用的键控实例在运行期间创建检查点,glac.NoCheckpoint() 去掉检查点;两者都是应用自己的设置,Cube.Create、Cube.Deploy 和 app.Update 都接受,AppInfo.Checkpoint(一个 *AppCheckpoint,没有时为 nil)会说明它。实例的 SandboxInfo.Checkpoint(一个 *SandboxCheckpoint:EverySeconds、LastAt、BehindSeconds、BytesPerSecond、StoredBytes、Error;没有检查点时为 nil)说明它的检查点进行得如何。
  • 单个键的实例的版本。inst.Upgrade(ctx, "3")(或 app.UpgradeInstance(ctx, key, version))把某个键的实例原地迁到应用的另一个版本:"" 表示应用的当前版本,否则为版本的 id 或编号(必须是就绪的版本,也可以是更旧的)。实例保留它的 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);实例已经在那个版本上时,返回 nil 且没有错误。up.Wait(ctx) 等待它结束,新版本没能起来时返回类型为 upgrade_failed 的 *Error(它的 Code 为 upgrade_failed 或 upgrade_not_here),实例留在原来的版本上。inst.Upgrades(ctx)、app.InstanceUpgrades(ctx, key) 和 app.InstanceUpgrade(ctx, key, id) 读取升级记录;inst.Refresh(ctx) 重新读取实例 (VersionID)。版本之后的选项:glac.WhenAsleep() 让升级等到实例休眠时再开始 (waiting),这样它的请求一个都不会被暂存;glac.KeepDays(n)(0 到 365,默认 7)是升级完成后回退所需内容的保留天数,按快照计费。如果版本的服务挂载的云盘或存储卷与实例的不同,就不能原地升级:升级以 failed 结束,错误码为 upgrade_not_here。
  • 取消与回退。up.Cancel(ctx)(或 app.CancelInstanceUpgrade(ctx, key, id))取消一个正在等待的升级(等实例休眠,或在批次中排队):状态变为 cancelled,实例留在原来的版本上(已经开始的升级返回 409 upgrade_started)。up.Revert(ctx, glac.KeepDays(n))(或 app.RevertInstanceUpgrade(ctx, key, id))把实例恢复到升级所保留的状态,即它的磁盘、升级之前的云盘(会先为云盘现在的内容创建快照)和原来的版本:返回一个新的 *Upgrade,Kind 为 revert,它的进行和等待方式与升级相同。要回到的内容已不再保留时,返回 409 rollback_gone。
  • 批量升级多个键。app.UpgradeInstances(ctx, glac.BatchVersion("4"), glac.FromVersions("3"), glac.OnlyKeys(…), glac.WhenNow(), glac.KeepDays(n), glac.BatchConcurrency(8), glac.MaxFailures(1)) 把应用的多个键升级到同一个版本(默认为应用的当前版本),每个键在其实例休眠时升级(带 WhenNow 时轮到就升级),默认同时 4 个(同一节点上 2 个),默认失败 5 次后以 failed 停止。它返回 *UpgradeBatch(Status 为 running、done、failed、stopped;StopReason、Counts、Upgrades、Skipped、Origin);无论批次如何结束,batch.Wait(ctx) 都返回 nil(实例从不休眠的成员会让批次一直运行:context 的截止时间会以 wait_timeout 结束等待),batch.Stop(ctx) 取消仍在等待的成员。app.UpgradeBatches(ctx)、app.UpgradeBatch(ctx, id) 和 app.StopUpgradeBatch(ctx, id) 读取和停止批次。
  • 单个键的实例的端口。app.OpenInstancePort(ctx, key, "tcp", 5432, "2001:db8::/32") 在某个键的实例自己的公网 IPv6 地址上,向给定的来源(IPv6 前缀或地址,::/0 表示所有人)开放一个端口,无论实例在哪里,并返回 OpenedPort,附带访问地址;app.InstancePorts(ctx, key, "tcp") 列出端口,app.CloseInstancePort(ctx, key, "tcp", 5432) 关闭一个,并说明它之前是否开放。"udp" 表示 UDP。它们就是实例的沙箱的端口(键控实例);没有自己地址的实例会被拒绝 (409 no_ipv6)。
  • 一起部署多个服务。client.Deploys.Create(ctx, map[string][]glac.DeployOption{"api": {glac.Image("ghcr.io/acme/api:2")}, "web": nil}) 一次部署项目中的多个服务(项目部署):每个服务都接受 App.Deploy 接受的内容(nil:按原样),按各自等待的对象排序,并一起切换;它会等待部署完成(glac.NoWait() 则不等),并跟随取代它的部署。失败时是一个带有 Deploy 和 Service 的 *glac.DeployError,它的 Code() 是部署的错误码(job_failed、job_timed_out、job_lost 等);解包后是类型为 deploy_failed 的 *Error,其 Param 为 services.<name>。client.Deploys.Get 和 List 读取部署。
  • 属于控制平面。对着一个独立运行的节点,client.Cube 的每个调用都会得到该节点的 404 no_such_route。

沙箱

sbx, err := client.Sandboxes.Create(ctx) // stays until deleted
if err != nil {
	log.Fatal(err)
}
defer sbx.Delete(ctx)
res, err := sbx.Run(ctx, "python3 -c 'print(6*7)'") // res.Stdout, res.Stderr, res.ExitCode
if err != nil {
	log.Fatal(err)
}
link, err := sbx.URL(3000)
if err != nil {
	log.Fatal(err)
}
fmt.Println(res.Stdout, link)

以一个字符串给出的命令用 sh -c 运行;用 glac.Args(...) 时,它是一个程序及其参数,按原样运行。Run 等待期间,glac.Stdout(os.Stdout) 会把输出实时写出;sbx.Exec 立即返回,cmd.Output(ctx) 随输出到来读取它(它也是一个 io.Reader)。client.Sandboxes.Run 为一条命令创建沙箱;sbx.Run 在沙箱中运行一条命令。

调用 作用
client.Sandboxes.Run(ctx, cmd, …) 为一条命令创建的沙箱,运行后删除;Create 和沙箱的 Run 的选项。
client.Sandboxes.Create(ctx, Image(…), Name(…), Env(…), Metadata(…), Volumes(…), Drives(…), PublicPorts(…), SleepAfter(d), NeverSleep(), SleepNotice(d), SleepHoldMax(d), ExpiresAt(t), WithEgress(…), FromSnapshot(…)) 创建一个沙箱;每个选项都可以省略。启动时运行的服务:AutoStart(true)(镜像自己的启动命令)、StartLine(…) 或 StartArgv(…),配合 ReadyTCPOn(port)、ReadyHTTPOn(port, path) 或 ReadyCommand(…);Hostname(…)、StopSignal(…) 和 StopGrace(d);ProjectNetwork() 让它从一开始就加入项目的私有网络 (Info().ProjectAddress);VolumeMount{…, Copy: true} 把镜像在该路径下的内容放进新的存储卷(创建沙箱)。
sbx.Info().AppID, VersionID, Key, ImageDigest 通过控制平面创建的沙箱是某个应用的实例:不带镜像创建时,是项目默认的沙箱应用 (sbx-sandbox),否则是它的镜像对应的应用(应用的 Instances 列出该应用的沙箱,每个都在它的键下);它所属的该应用的版本、创建它所用的镜像(之后被移动过的标签会成为应用的新版本,此后创建的沙箱属于新版本);它在那里的键、名称或 id;以及按摘要标识的镜像。创建的响应和之后的每次读取都会给出这些信息,并且保持不变;从模板或快照创建的沙箱,以及分叉,这些信息为空。
Create 和 Run 的 Init("systemd");SandboxInfo.Init 完整的系统:镜像自己的 systemd 作为沙箱的 PID 1(不带 Image 时,使用平台带 systemd 和 OpenSSH 的 Ubuntu),所以软件包、用 systemctl 启动的服务、cron 和 journal 都能像在一台普通机器上一样工作,它也有自己的 machine ID,在休眠、唤醒和迁移后保持不变。不能与启动命令或就绪检查同时使用 (400 invalid_start),也不能与 FromSnapshot 同时使用。这样的沙箱 SandboxInfo.Init 为 "systemd",使用平台 init 的沙箱则为空。
client.Sandboxes.Get, GetOrCreate, List(ctx, Status(…), MetadataIs(k, v)), ListPage(ctx, PageSize(n), After(cursor)) 查找;查找或创建;列出;获取一页。
Create 和 sbx.Update 的 SuspendAfter(d)、WakeDailyAt("03:30")(DefaultSuspendAfter()、NoDailyWake() 恢复默认);SandboxInfo.SuspendAfterMS、WakeDailyAt 因空闲而停止后,它保持暂停多久才保存内存、真正休眠(一秒到一天;它的节点最多保持十分钟),以及休眠期间每天在 UTC 的哪个时刻唤醒它,以便做它自己等着唤醒才做的工作。设置在应用上时,作用于它的键控实例和有状态实例。
Create 和 sbx.Update 的 OnExpiry("sleep")、HeldAsleep(true);SandboxInfo.OnExpiry、HeldAsleep ExpiresAt 到期时做什么:delete 删除沙箱(默认)、sleep(让它休眠并保留,请求照常会唤醒它)或 hold(同上,但保持休眠);以及它休眠时,发往它某个端口的请求是否会唤醒它:保持休眠时,它的预览地址返回 502 sandbox_paused,API 调用会唤醒它。
sbx.Run(ctx, cmd, Args(…), Dir(…), Env(…), User(…), Timeout(d), Stdin(r), Stdout(w), Stderr(w)) 运行并等待。*Result 包含 Stdout、Stderr、ExitCode、Duration、Usage、Wake。
sbx.Exec(ctx, cmd, StdinOpen(), TTY(cols, rows)), sbx.Terminal(ctx, …) 启动一条命令;一个终端 (io.ReadWriteCloser)。
sbx.Files Read、Open、Write、WriteFrom、List、Stat、Exists、Mkdir、Move、Remove,以及 SetAttributes(ctx, path, glac.FileAttributes{Mode, SizeBytes, ModifiedAt}):文件的权限、长度和最后修改时间。
sbx.URL(port), sbx.Token(ctx, Scopes(…), ExpiresIn(d)) 端口链接(在本地签名);这个沙箱的令牌。
sbx.Info().IPv6, sbx.OpenPort(ctx, "tcp", port, from…), sbx.Ports(ctx, "tcp"), sbx.ClosePort(ctx, "tcp", port), Inbound(InboundPort{…}) 它自己的公网 IPv6 地址(如果有,否则为空);向列出的来源(IPv6 前缀或地址,"::/0" 表示所有人)开放一个端口,并给出访问地址;开放的端口;关闭一个端口(并说明它之前是否开放);完整的列表,在 Create 或 Update 时给出。入站默认关闭;它休眠时和迁移时,开放的端口都保持开放。参见网络。
sbx.RevokeToken(ctx, token) 从此只拒绝那一个令牌,其他令牌照常有效:令牌本身 (Token.Token) 或它的 id(Token.ID,tok_…)。一个沙箱最多保留 1,000 个尚未过期的这类吊销(超出时返回 409 too_many_revoked_tokens:这时请用 RevokeTokens)。返回一个 *TokenRevocation,带有 TokenID 和 ExpiresAt。
sbx.RevokeTokens(ctx) 到目前为止为这个沙箱发出的所有令牌都会失效 (401 invalid_token),包括预览链接和 Cookie 中的令牌,以及 SSH 会话持有的令牌;之后创建的令牌有效。需要 API 密钥。返回一个 *TokenRevocation:SandboxID、Generation、RevokedAt。
sbx.Update, sbx.Sleep, sbx.Restart, sbx.Delete, sbx.Snapshot, sbx.Fork(ctx, n), sbx.Restore 修改;休眠;从磁盘启动;删除;创建快照;分叉;恢复。
Create 或 Update 的 glac.Checkpoint(n),Update 的 glac.NoCheckpoint();Info().Checkpoint 运行期间的检查点(间隔 1 到 600 秒;0 表示十秒),这样机器丢失时它能带着进程继续运行;关闭检查点;检查点的进行情况。没有机器支持检查点时返回 409 checkpoints_unavailable。
Info().DiskWrites 它的磁盘今天(UTC 日)已写入的量,与当天全速份额的对比(一个 *SandboxDiskWrites):TodayBytes,其中 CheckpointCopyBytes 是检查点副本写入宿主机磁盘的量;FullSpeedUntilBytes,即份额(默认 100 GiB,节点另有设置时除外;nil 表示节点不限速,但照样统计);Limited 和 LimitedUntil:超出份额后,它的磁盘以每秒 2 MiB 的速度接受写入,直到当天结束,任何写入都不会失败。在不把磁盘复制到对象存储的节点上为 nil,那里什么都不统计。写入它的云盘的数据不计在这里:写入云盘不收费。
client.Sandboxes.Snapshots.Get, client.Sandboxes.Snapshots.List(ctx, OfSandbox(id)), snap.Rename(ctx, name), snap.ExpireAt(ctx, t) 快照:沙箱在某一时刻的状态,包括内存和进程;给快照换个名称("":不要名称),或设置它自行删除的时间(零值时间:永不删除)。
client.Sandboxes.SSHKeys.Create(ctx, publicKey, OfSandbox(…), ExpiresAt(t)), Get, List SSH 密钥:持有私钥的人可以 ssh <sandbox>@<gateway>。私钥会在发送任何东西之前就被拒绝。

云盘

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

  • client.Drives.Create(ctx, glac.Name("data"), glac.SizeBytes(10<<30), glac.InRegion("eu")) 创建一个云盘,里面只有文件系统;每个选项都可以省略(SizeBytes:4 KiB 块的整数倍,最小 16 MiB;默认 64 GiB,免费版上为 5 GiB;Durability("strict"):对象存储收到 fsync 之前的每一次写入后,客户机的 fsync 才返回)。还有 Get、List、ListPage、Rename(ctx, drive, name)(名称为空表示不要名称)、Delete(ctx, drive)。
  • client.Drives.Attach(ctx, sandbox, glac.DriveMount{Path: "/data", Drive: drive.ID}) 立即以读写方式把它挂载到一个已有的沙箱中(休眠的沙箱会被唤醒);Detach(ctx, sandbox, drive) 卸载它,有进程在使用时用 ForceDetach。每个调用都返回 *Drive,它的 Info() 是一个 DriveInfo(ID,即 vol-…:云盘就是存储卷;Name、SizeBytes、UsedBytes、AttachedTo、Region、Home、Status、Lease:唯一写入方所在的节点、纪元和到期时间;键控实例的云盘有 AppID 和 Key;分叉有 ParentDriveID 和 RootDriveID:来源云盘和这一脉的第一个云盘,来源云盘之后可能已被删除,不是分叉的云盘为空;数据库的云盘有 ManagedBy,不属于任何东西的云盘为 nil),并且自带 Attach、Detach、ForceDetach、Rename、Refresh、Delete、Snapshot、Snapshots、Fork 和 Restore。
  • 数据库的云盘(Info().ManagedBy,即数据库的 ID)只能由数据库的实例以读写方式挂载,重命名、恢复和删除都要通过数据库进行:在这里调用这些操作,或以读写方式挂载它,都会被拒绝 (409 managed_by_database)。项目可以为它创建快照和分叉,任何人都可以以只读方式挂载它的快照。构成数据库的应用也会以同样的方式说明这一点 (app.Info().ManagedBy),它的 Update、Deploy、Rollback 和 Delete 同样会被拒绝。
  • 新沙箱在 client.Sandboxes.Create(不是一次性的 Run)中带上 glac.Drives(glac.DriveMount{…}) 来挂载云盘;应用的唯一实例在部署时带上同样的选项(有多个实例或使用键的应用会被拒绝,409 drive_single_writer;版本以读写方式挂载云盘,快照的只读挂载只供沙箱使用:在那里带 Snapshot 的挂载会被拒绝,400 unknown_field),键控应用的每个键的实例则用 glac.KeyDrives(glac.KeyDrive{Name: "home", Path: "/home/app"}) 获得自己的云盘。
  • 一个云盘同一时间只属于一个沙箱:另一个沙箱持有的云盘会被拒绝 (409 drive_attached),删除已挂载的云盘也一样。键控实例的云盘随它的键一起删除 (409 key_drive)。
  • 另一个节点持有的云盘,会先被搬到沙箱所在的节点;对象存储中还没有它的最新状态时 (503 copy_behind),或无法把它搬过去时 (503 drive_not_brought),调用为 Retryable:客户端会按 Retries 的设置重试,之后返回错误,你稍后再请求一次即可。
  • 跨区域 (slice D9)。云盘的归属区域 (home) 是保存其副本的对象存储所在的区域:也就是创建它的区域。另一个区域的沙箱也能挂载它,前提是那个区域的机器能读取其归属区域的存储(不能时返回 409 not_on_one_machine):云盘被释放为一个版本,在那里被接管,客户机读取时从归属区域的存储读出,客户机写入的数据则进入它现在所在区域的存储 (region)。云盘在那里待满一小时后,归属区域会跟过去,用 client.Drives.MoveHome(ctx, drive) (drive.MoveHome(ctx)) 则立即迁移:它的机器把归属区域存储中剩下的内容复制到自己区域的存储中,那里什么都不剩时,home 就变成新区域;DriveInfo.HomeMove 说明迁移进行到哪一步(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)。
  • glac.OfDrive(id)、glac.OfKind("drive") 和 glac.GroupBy("drive") 报告云盘保存了多少、写入了多少、冷读了多少。drive.created、drive.deleted、drive.attached 和 drive.detached 事件解码为 EventDrive 或 EventDriveAttached。
  • 快照。client.Drives.Snapshot(ctx, drive, glac.Name(…), glac.FSConsistent())(或 drive.Snapshot(ctx, …))为云盘创建快照,即一个 *DriveSnapshot:云盘在对象存储中的一个版本,会被保留;不复制任何数据。运行中的沙箱以读写方式持有的云盘,会在客户机的写入被暂停片刻时创建快照,最多十秒(超过时返回 503 unavailable,Retryable):glac.FSConsistent() 让沙箱的代理程序冻结它的文件系统(Consistency 为 frozen;代理程序做不到时返回 409 not_frozen:这时请不带它创建),不带它时则暂停机器(paused,相当于断电);没有运行中的沙箱持有的云盘为 at_rest。节点不向对象存储复制任何数据的云盘没有快照 (409 not_copied)。Snapshots(ctx, drive) 列出一个云盘的快照,最新的在前,client.Drives.AllSnapshots(ctx, "") 列出项目中所有云盘的快照("true":只列出已删除云盘的快照);GetSnapshot(ctx, drive, snapshot) 查找一个 (404 snapshot_not_found),DeleteSnapshot(ctx, drive, snapshot) 删除一个。*DriveSnapshot 自带 Refresh、Delete、Fork、Restore、Attach、Detach 和 ForceDetach;它的 Info() 是一个 DriveSnapshotInfo(ID,即 dsnap-…;DriveID、Name、Consistency、UsedBytes:当时云盘中的数据量;ExclusiveBytes:它单独保留的数据量;ExclusiveAt、Version、CreatedAt)。
  • 快照不随云盘消失:云盘删除后,仍可以按云盘的 id 列出、读取、分叉它的快照,或以只读方式挂载。快照按它单独保留的数据 (ExclusiveBytes) 计费,按小时计,价格与云盘的数据相同(每 GB·月 $0.02)。
  • 分叉。client.Drives.Fork(ctx, drive, glac.Name(…), glac.FromSnapshot(snapshot))(或 drive.Fork,或 snap.Fork(ctx, glac.Name(…)))创建一个新云盘,它的第一个版本是快照的内容,或云盘当前的内容;不复制任何数据,可以立即挂载。它返回新云盘,其 ParentDriveID 和 RootDriveID 说明它从何而来,和创建一样受套餐云盘数量的限制 (403 drive_limit_reached)。分叉和任何云盘一样,按它的 UsedBytes,即完整的逻辑内容计费:从 10 GB 快照分叉出的云盘按 10 GB 的云盘计费。
  • 恢复。client.Drives.Restore(ctx, drive, snapshot)(或 drive.Restore(ctx, snapshot),或 snap.Restore(ctx))在云盘最新的版本之后写入一个新版本,内容与快照相同;中间的版本只要有快照或分叉保留着就仍然存在,所以先恢复到较旧的快照、再恢复到较新的快照也可以。沙箱以读写方式持有的云盘会被拒绝 (409 drive_attached):先卸载它。
  • 版本窗口。创建时的 glac.KeepHistory(d)、client.Drives.SetHistory(ctx, drive, d)(0 表示关闭)、Versions(ctx, drive) 和 VersionAt(ctx, drive, t) (DriveWindowVersion)、glac.ForkAt(t)、client.Drives.RestoreTo(ctx, drive, t)(以及 drive.SetHistory、drive.Versions、drive.RestoreTo;DriveInfo.HistorySeconds、History):云盘的版本窗口 (slice DB8):按时间最多能往回恢复或分叉多远,3600 到 86400 秒(不设置时没有窗口;关闭后,它保留的内容会被丢弃)。在每个有写入的 30 秒里,窗口都会得到一个版本,即云盘在某一时刻的状态;这些版本单独保留的数据和快照一样计费。按时间恢复或分叉时,取该时间点或之前最新的版本,也就是以该时间调用版本列表时给出的那个版本。
  • 节点 NVMe 上的缓存。创建时的 glac.CacheBytes(n)、client.Drives.SetCache(ctx, drive, n) 和 drive.SetCache(ctx, n)(glac.CacheAll 表示全部缓存)、drive.CacheBytes() 和 DriveInfo.CachedBytes:节点在 NVMe 上最多保存云盘的多少数据。默认全部保存(即云盘的大小);也可以设得更小,最小到 0,超出的部分只保存在对象存储中,客户机读到时再从那里读取(最久没被读过的数据最先释放,对象存储还没有的数据从不释放)。缓存按小时、按已填充的量计费,与云盘的数据费用分开。调小缓存后,超出的部分一旦已在对象存储中就会被释放(如果运行中的沙箱挂载云盘时是全部缓存,则从那个沙箱下一次休眠或停止时开始);调大缓存后,从那时起客户机读到的数据都会留在缓存中。API 把云盘叫作存储卷 (/v1/volumes);各客户端通过 /v1/drives 访问它们,这个接口的响应和以前一样。控制平面没有给出缓存信息时,CacheBytes() 就是云盘的大小;CachedBytes 是当前在节点 NVMe 上的量(没有节点持有云盘时为 nil)。UsageGroup 和 UsageReportTotals 中有 CacheGBHours。
  • 只读挂载。glac.DriveMount{Path: "/snap", Drive: drive.ID, Snapshot: snap.ID}(它会发送 mode: "snapshot")以只读方式挂载快照:创建沙箱时交给 glac.Drives(…),沙箱运行时交给 client.Drives.AttachSnapshot(ctx, sandbox, mount) (snap.Attach(ctx, sandbox, path));DetachSnapshot(ctx, sandbox, snapshot) 和 ForceDetachSnapshot 按快照的 id 卸载它。每个调用都返回快照。在云盘所在区域的任意机器上,任意多个沙箱都可以同时挂载同一个快照,无论云盘被谁持有,云盘删除后也可以。数据块在客户机读取时从对象存储取来:这些冷读不收费,并在用量中计在那个沙箱名下(client.GetUsageReport(ctx, glac.OfSandbox(id)) 的 DriveColdReadBytes);读取那个沙箱的机器上已有的数据也不收费。客户机不能写入它,挂载了快照的沙箱也不会迁到别的机器上。Attach 不接受带 Snapshot 的挂载,AttachSnapshot 不接受不带它的挂载,DetachSnapshot 只接受 dsnap-… 形式的 id:这些都会在发送任何东西之前就被拒绝。

client.Volumes 对应 API 的 /v1/volumes,即云盘底层的磁盘,为过渡期而保留:只有它才有的,是独立运行的节点(节点上的磁盘就是存储卷),以及共享存储卷,即可由多个沙箱同时挂载的文件系统 (glac.Shared())。控制平面不会为项目的密钥创建普通存储卷。client.Volumes.Create(ctx, glac.Name("team"), glac.Shared())、Get、List 和 vol.Delete(ctx)(仍有沙箱持有的存储卷会保留,并返回错误);用 glac.Volumes(glac.VolumeMount{Path: "/team", Volume: vol.ID}) 挂载存储卷,交给 client.Sandboxes.Create 或 sbx.Update,或者交给应用,由它的实例挂载。

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

  • client.Volumes.Backup(ctx, "data", glac.ExpiresInDays(7))(或 vol.Backup(ctx))立即创建一个备份,即一个 *Backup,保留这么多天(1 到 365),或按存储卷的规则(未设置时为 30 天);client.Volumes.Backups(ctx, "data") 列出一个存储卷的备份,最新的在前(已删除的存储卷按 id 指定)。不向对象存储复制任何数据的节点没有备份 (409 not_copied)。
  • client.Backups.List(ctx) 列出项目的备份,最新的在前,包括已删除存储卷的备份,ListOf(ctx, volumeID) 列出一个存储卷的备份;Get(ctx, id)、Delete(ctx, id)(之后不再为它计费;从它创建的存储卷仍然保留),以及 Restore(ctx, id, glac.Name("data-back")),即从一个就绪的备份创建新存储卷,等同于 client.Volumes.Create(ctx, glac.FromBackup(id))(创建期间返回 409 backup_not_ready,失败后返回 409 backup_unusable)。*Backup 具有 BackupInfo 的字段(ID,即 bak-…;VolumeID,以及当时存储卷的名称 VolumeName;Status:making、ready、failed;Reason:manual、daily、before_version;Consistency:frozen、crash;FrozenMS,存储卷的写入方为复制等待了多久;SizeBytes,存储卷当时的数据量,以及 VolumeSizeBytes,它最多能容纳的量,也就是从备份创建的存储卷的容量;StoredBytes,计费依据;OwnBytes,删除它能省下的量;SharedBytes;Shared、Region、Failure、CreatedAt、ReadyAt,即开始计费的时间,以及 ExpiresAt),并自带 Refresh、Restore 和 Delete。
  • 从备份创建的存储卷,大小和类型与备份所来自的存储卷相同(与 glac.FromBackup 一起时不给出 glac.SizeBytes 和 glac.Shared),位于备份所在的区域。它的 Status 在完整拿到备份之前为 restoring,之后为 ready,在此之前不能挂载;FromBackup 指明来源备份。如果被分配到的机器无法恢复它,控制平面会在该区域的另一台机器上再创建一次:这时 RestoreAttempt 为 2,FirstFailure 说明第一次为什么失败。如果这一次也失败,它就是 failed (Failure),只能删除。
  • glac.OfBackup(id)、glac.OfKind("backup") 和 glac.GroupBy("backup") 报告备份保存了多少,计在状态计量项上 (state_gb_hours);client.GetBilling(ctx) 的 Meters.State.BackupsGBHours 说明该计量项中有多少来自备份。

域名

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

调用 作用
client.Domains.Create(ctx, domain, glac.ToApp(…)) 或 glac.ToSandbox(id, port),配合 glac.Proxied(…);Get、List(ctx, glac.OfApp(…))、Delete;d.Update(ctx, …)、d.Refresh、d.Delete 为应用或沙箱端口注册的名称,在 DNS 上能查到 Info().Verification.Records 中的某条记录之前为 pending_verification(自定义域名);项目中已有该域名时返回 409 domain_exists。

浏览器

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

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

session, err := client.Browsers.Create(ctx, glac.BrowserName("agent-42"))
if err != nil {
	return err
}
defer client.Browsers.Delete(ctx, session.ID)
fmt.Println(session.CDPURL) // where a CDP client connects; the credential is in it
  • client.Browsers.Create(ctx, opts...) 创建一个处于运行状态的会话,选项有:glac.BrowserKind("chromium")(有屏幕;默认是 chromium-headless;glac.BrowserKind("lightpanda") 即 lightpanda,没有布局引擎的浏览器)、glac.BrowserName、glac.BrowserRegion、glac.BrowserSleepAfter、glac.BrowserConnectionIdle、glac.BrowserDeleteAfterIdle、glac.BrowserMetadata、glac.BrowserProfile(name, save) 和 glac.BrowserProfileVersion(name, version, save)(在返回会话之前加载项目的一个配置文件;设置了 save 时,会话删除时再保存回去)、glac.BrowserProxy(url)(你自己的 HTTP 代理,付费版可用;见下文)。如果用已有会话的名称创建,会返回那个会话:它会被唤醒,页面都还在。沙箱在运行前就失败的会话不算会话:创建会被拒绝,返回 503 session_failed。
  • client.Browsers.Get(ctx, idOrName)、Delete(ctx, idOrName)(用需要保存的配置文件创建的会话会先保存它)和 DeleteWithoutSavingProfile(ctx, idOrName)。*BrowserSession 包含 ID、Kind、Status、CDPURL(Playwright 和 Puppeteer 用的地址)、LiveURL、Profile(创建它时所用的或最近一次保存的配置文件:Name、Save、Version)、Proxy(它自己的代理的 URL 和 Username,从不包含密码)、它的时间设置,以及 Usage:AwakeSeconds,和 ProxyBytes,即双向经过其代理的流量(ProxySentBytes、ProxyReceivedBytes)。
  • 你自己的代理(glac.BrowserProxy("http://user:password@proxy.example.com:3128"),一个接受 CONNECT 的 HTTP 代理):会话到 80 和 443 端口的连接,经由浏览器旁边的一个中继从这个代理出站,浏览器本身对代理一无所知;凭据留在平台上。免费版上返回 403 proxy_not_on_plan。参见自己的代理。
  • session.Status 为 awake、asleep、unavailable(它的节点无法访问)或 failed(创建得到响应之后它的沙箱失败了:比如浏览器停止且没有恢复),这时 session.Error 说明原因(一个 *BrowserError:Code、Message,按节点的原话;没有失败时为 nil)。browser.failed 事件也会说明这一点:它的数据解码为 EventBrowserFailed(BrowserID、Name、Kind、Error.Code)。

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

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

数据库

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

db, err := client.Databases.Create(ctx, "shop", glac.DatabaseVersion("18"))
if err != nil {
	return err
}
conn, err := pgx.Connect(ctx, db.ConnectionString()) // from a sandbox or a service of the project
  • client.Databases.Create(ctx, name, opts...) 创建一个数据库,并等待它第一次启动(Status 为 ready,或为 creating:这时反复 Get 它,直到就绪):glac.DatabaseVersion、glac.DatabaseRegion、glac.DatabaseInCluster(数据库在集群中的名称,默认 glacdb)、glac.DatabaseSizeOf(vcpus, memoryBytes)、glac.DatabaseStorageOf(sizeBytes, durability)(strict 为默认值,也可以是 standard)、glac.DatabaseSleepAfter、glac.DatabaseSuspendAfter(保存内存、真正休眠之前暂停的秒数,暂停期间连接在一两毫秒内就能得到响应:1 到 86400,默认 300)、glac.DatabaseWakeDailyAt("03:30")(休眠期间每天在那时唤醒;它的 pg_cron 任务无论如何都会在各自的时间唤醒它)、glac.DatabaseMetadata。用已有数据库的名称创建时,返回那个数据库,不带密码。client.Databases.SetSuspendAfter(ctx, idOrName, n) 和 SetWakeDailyAt(ctx, idOrName, "04:00") 修改这两项,所有分支一起修改;0 和 "" 恢复默认(分别为 300 和不唤醒)。免费版数据库的休眠设置是固定的,与 Neon 免费版缩容到零的做法相同:最后一个查询一秒后暂停(sleep_after_seconds 为 1),五分钟后休眠(suspend_after_seconds 为 300);无论创建还是修改,其他值都会以 400 invalid_field 被拒绝。
  • client.Databases.CreateBranch(ctx, idOrName, name, glac.BranchParent("main"), glac.BranchExpiresAt(t)) 创建一个独立分支:它的父分支(默认 main)此刻的状态,不复制任何数据,有自己的 PostgreSQL、端点和云盘,在项目网络上的地址是 <database>-<name>;给出过期时间时,到时会自行删除。它的角色和密码与父分支相同:branch.ConnectionString(ownerPassword)。DeleteBranch(ctx, idOrName, branch) 停止它的 PostgreSQL 并删除它的云盘;不能删除 main (409 root_branch),也不能删除有其他分支从它创建的分支 (409 has_branches)。
  • 创建时的 glac.DatabaseHistorySeconds(n)、client.Databases.SetHistory(ctx, idOrName, n)、client.Databases.Restore(ctx, idOrName, t, glac.Exactly()),以及 glac.BranchAt(t)、glac.BranchAtExactly(t)(Database.HistorySeconds、History):数据库的历史(slice DB8 和 DB5):根分支按时间最多能恢复到多久以前,或从多久以前创建分支:默认一天;付费版可设为 0(不保留)到 30 天,免费版为一天。恢复会把根分支原地带回过去:恢复到整分钟或某个版本自身的时刻时,使用云盘版本窗口中该时刻或之前最新的版本(每个有写入的 30 秒一个版本,最多保留一天);更早的时刻、精确到秒的时刻,或带 glac.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、FromLSN、ToLSN):被放弃的 WAL 从何时开始,归档到何时为止无法恢复任何时间(缺口之后第一个基础备份完成的时间;在有基础备份之前为 null),以及缺口在日志中的起止位置。缺口内的时间只能从云盘的版本窗口恢复(整分钟,或某个版本自身的时刻);精确到秒或精确时刻的恢复返回 400 invalid_history。缺口也会以事件的形式报告:首次发现缺口时发出 database.history_gap(数据库运行期间,每个缺口一次),缺口之后归档重新可用于恢复时发出 database.history_resumed;event.Decode(&gap) 把它们的数据读入 EventDatabaseGap:数据库,缺口的 From、To(前者为 nil)、FromLSN 和 ToLSN,Segments(放弃了多少个 16 MB 的分段),以及前者才有的 Error(对象存储最后一次拒绝时返回的内容)。
  • 创建时的 glac.DatabaseTiered() 和 client.Databases.SetTiering(ctx, idOrName, on) 用来开启或关闭分层存储(slice DB18:付费版可用;每个分支的 PostgreSQL 会为此重启一次);Database.Tiering 说明是否已开启,以及目前按多少计费 (TieredBytes)。
  • Get(ctx, idOrName)、List(ctx[, name])、Delete(ctx, idOrName, finalSnapshot)(除非 finalSnapshot 为 false,否则它的云盘会保留一个快照七天)、Branches(ctx, idOrName)(先是 main,然后是独立分支)、Branch(ctx, idOrName, "main")、Sleep 和 Wake(某个分支,立即执行)、ResetPassword(ctx, idOrName, role)(新密码只出现在这次响应中;"" 表示数据库的所有者)。
  • client.Databases.Roles(ctx, idOrName) 列出根分支中可登录的角色,直接从 PostgreSQL 读取([]DatabaseRole:Name、Owner):它的所有者和你创建的角色 (CREATE ROLE … LOGIN);平台自己的角色不在其中。ResetRolePassword(ctx, idOrName, role) 给其中任何一个角色设置新密码,只出现在这次响应中:一个 *DatabaseRoleSecret,带有该角色的 Password 和根分支上该角色的连接字符串 (ConnectionString());所有者的连接字符串由 API 响应的数据库生成。独立分支保留它创建时的密码。
  • db.ConnectionString() 和 branch.ConnectionString(password):libpq、pgx 和 database/sql 的驱动接受的连接字符串,API 给出公网连接字符串时用公网的,否则用项目网络上的,并带有密码(百分号编码)。db.DefaultBranch 包含 ConnectionURI、PooledURI、PrivateURI、Endpoint 和 State。

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

托管应用

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

rivet, err := client.Catalogue.Get(ctx, "rivet", "")        // its parameters, and a month of it in Cube's meters
engine, err := client.Installations.Create(ctx, "rivet", glac.InstallationCreate{Parameters: map[string]any{"disk_gib": 20}})
fmt.Println(rivet.Calculator.Month, engine.Info().Outputs["endpoint"])   // RIVET_ENDPOINT, its token in it
调用 作用
client.Catalogue.List(ctx), client.Catalogue.Get(ctx, app, version) 目录中的应用,每个都是一个 CatalogueApp(App、Version、Title、Summary、Verified、Licence、Versions、Parameters:一个 JSON Schema,Outputs、带有 Month 的 Calculator);查看一个应用的最新版本或指定的版本("":最新版本),以及它的 Definition。
client.Installations.Create(ctx, app, glac.InstallationCreate{Name, Version, Region, Parameters, Requires, NoWait}) 安装它,并等它就绪(NoWait:立即返回)。安装失败时是一个 *InstallationError(带有 Installation),解包后是错误码为 installation_failed 的 *Error;如果 context 的截止时间先到,则为 wait_timeout。
client.Installations.List(ctx, app), client.Installations.Get(ctx, idOrName) 项目的安装,最新的在前(app 为 "":全部);查看一个。
inst.Update(ctx, glac.InstallationUpdate{Version, Parameters, NoWait}), inst.Delete(ctx), inst.Refresh(ctx), inst.Wait(ctx) 换一个版本,或以合并补丁的方式修改参数(值为 nil 时把参数恢复为默认值);连同它的所有组成部分一起删除(Status():deleted 或 deleting)。
inst.ID(), Name(), Status(), Info() 按 API 给出的样子:Info() 包含 Outputs、SecretOutputs、Parameters、Apps、Volumes、Buckets、Requires、Failure。

托管应用属于控制平面。

机密与共享变量

  • client.Secrets.Put(ctx, "DB_URL", value, glac.AllowedHosts("db.example.com")) 保存一个 API 永远不会返回的值,以及它可以发往的主机;client.Secrets.Update(ctx, name, hosts...) 只修改主机;还有 List、Delete。会把它发往其他主机的调用会被拒绝,返回 403 secret_host_not_allowed。
  • client.Variables.Put(ctx, "REGION", "eu")、List、Delete:项目的共享变量,应用的 Env 以 "${{ shared.REGION }}" 引用它们。

项目、账户与账单

平台本身的调用是客户端的方法:client.GetProject 和 client.ListEvents(ctx, OfType(…));client.GetAccount 和 client.UpdateAccount(ctx, EgressOverageCap(n));client.GetUsage 和 client.GetUsageReport(ctx, Start(t), GroupBy(…));client.GetBilling、client.UpdateBilling(ctx, Plan("paid"))、client.GetBalance、client.UpdateBalance(ctx, AutoReload(true))、client.TopUp(ctx, 20)、client.ListCharges 和 client.ListLedger。项目、项目事件和账单都属于控制平面。

事件的数据。控制平面的每种事件,在这个包中都有一个对应其数据的结构体,以描述中该事件的 schema 命名,用 e.Decode(&v) 读取 (var made glac.EventDatabase; err := e.Decode(&made))。数据库的事件:glac.EventDatabase(database.created,附带它的 region 和 PostgreSQL version;database.deleted,附带 final_snapshot_id,即保留七天的云盘快照)、glac.EventDatabaseBranch(database.branch_created,附带它的 parent;database.branch_deleted,附带它的 reason)、glac.EventDatabaseRestored(database.restored:时刻 at,以及 old_branch_id,即保存恢复之前状态的分支)、glac.EventDatabaseArchive(database.archive_failing:since、连续失败的分段数 failures、archived_until,即在此期间恢复能追溯多远,以及对象存储的 error;database.archive_recovered:recovered_at)和 glac.EventDatabaseGap。其他事件也一样:沙箱的(glac.EventSandboxSlept、glac.EventSandboxWoke、glac.EventSandboxMoved、glac.EventSandboxDeleted 等)、应用及其键的(glac.EventApp、glac.EventAppFailed、glac.EventKeyCreated 等)、项目的部署和密钥、备份、域名、导出目标和云盘快照的(glac.EventProjectDeployed、glac.EventBackup、glac.EventDomain、glac.EventDestination、glac.EventDriveSnapshot 等),以及账户的套餐、充值、退款、撤回、余额、点数、存储、出站流量上限、支出上限、冻结和欠款的(glac.EventSubscriptionPeriod、glac.EventTopupCollected、glac.EventWithdrawalReceived、glac.EventBalance、glac.EventInDebt 等)。某类事件并非都有的字段,或为 null 的字段,取零值(零值另有含义时使用指针)。

Webhook、API 密钥、支出上限、项目。client.Webhooks.Create(ctx, url, glac.WebhookEvents(…), glac.WebhookEnabled(…)) 把项目的事件发到你的地址(为每次投递签名的 Secret 只出现在这次响应中);还有 Get、List、Update(ctx, id, glac.WebhookURL(…)…)、Delete。client.Keys.Create(ctx, glac.ProjectKeyName(…), glac.ProjectKeyScopes(…), glac.ProjectKeyExpiresAt(t)) 创建一个 API 密钥(密钥本身 Key 只出现在这次响应中);还有 Get、List、Delete。client.Limits(ctx) 和 client.UpdateLimits(ctx, glac.SpendLimit(50)) (glac.NoSpendLimit()) 是项目的支出上限和本月已支出的金额,sbx.Limits、sbx.UpdateLimits 则是某个沙箱自己的。client.RequestLimit(ctx, limit, wanted, why) 申请提高账户的某项可应要求提高的限制(按名称指定的上限、"volumes" 或 "volume_bytes";后两者中的大小以字节为单位;同一项限制已有申请在等待时返回 409 limit_request_pending),client.ListLimitRequests(ctx, glac.PageSize(n), glac.After(cursor)) 和 client.LimitRequest(ctx, 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;沙箱(以及实例)的 Info().StartCommand 说明它的启动命令如何运行、在机器中被重新启动了多少次,以及上一次是如何结束的。client.UpdateProject(ctx, glac.DeleteAfterIdleDays(30)) (glac.NeverDeleteIdle()) 设置沙箱休眠多少天后被删除。属于控制平面。Webhook 和 API 密钥需要有 admin 权限范围的密钥,设置项目的上限和 UpdateProject 也一样;设置沙箱的上限需要 write。

日志导出目标、审计日志、计费文件。client.Destinations 是项目中应用的日志(以及指标和请求的追踪:glac.DestinationSignals("logs", "traces"))导出的地方(OpenTelemetry 端点):Create(ctx, name, glac.DestinationEndpoint(u), glac.DestinationHeaders(h)) 创建时会立即用一条记录测试 (Status.Connected),还有 Get、List、Update、Delete、Test(立即发送一条记录);请求头的值是凭据,只在创建或修改导出目标时发送,不会出现在任何响应中(返回的内容只有请求头的名称)。client.ListAudit(ctx, glac.PageSize(n)) 是项目审计日志的一页,最新的在前(谁、什么方法、什么路径、什么状态):每一次不是读取的调用(被拒绝的也算),以及运营方对应用日志的每一次读取。client.GetStatement(ctx, "2026-09") 是某个月的对账单(“” 表示本月),其中每个数据库单独一行(Databases:它这个月的用量,其中历史和分层数据单列,以及按标价折合多少),client.ListInvoices(ctx) 是 Stripe 上的发票(附 PDF 链接),client.ListNotices(ctx) 是发给账户的邮件,client.ListWithdrawals(ctx) 是现在可以撤回的购买和已做的撤回(欧洲经济区或英国的消费者可以在 14 天内撤回一笔购买),client.Withdraw(ctx, chargeID, name, email) 是买方本人的撤回声明,立即受理。属于控制平面。创建、修改、测试和删除导出目标、读取审计日志以及撤回,需要有 admin 权限范围的密钥;其他调用只需要有读取权限的密钥。

bill, err := client.GetBilling(ctx)
if err != nil {
	log.Fatal(err)
}
cpu := bill.Meters.CPU
fmt.Println(bill.Plan, cpu.Used, cpu.Unit, "used") // free 7.5 cpu_seconds used

账户的 Email 是它的通知发往的地址,用 glac.AccountEmail(address) 设置:EmailVerified 说明它是否已验证(只给已验证的地址发邮件:未验证的地址会先收到一个链接),EmailUndeliverable(一个 *EmailUndeliverable:Why、Since、Detail)说明发往它的邮件被退回了;在此期间通知会暂缓发送(通知发往哪里)。账户的 Language 是它的通知使用的语言(一个 NoticeLanguage:Code 为 “en”、“de” 或 “fr”,Source 为 “account”、“billing_address” 或 “default”);client.UpdateAccount(ctx, glac.AccountLanguage("de")) 选择语言,glac.NoAccountLanguage() 撤销选择,重新由账单地址决定。撤回的回执和购买的确认用这种语言撰写;其他所有通知都是英文。

账户的 Keeps(一个 AccountKeeps,在控制平面后面)是密钥所属项目在云盘和存储卷上的保有量,按创建时所受限制的数字给出(403 drive_limit_reached、volume_limit_reached),每项都附带项目已有的量:Volumes(一个 KeepsCount:Limit、Used,以及 Source,为 “plan” 或 “operator”)、VolumeBytes(一个 KeepsSize:单个的大小上限,以及它的 Source)、VolumesBytes(一个 KeepsTotal:全部合计的大小上限,Limit 为 nil 表示不限,以及 Used),以及 UnsizedBytes,即不指定大小时创建的大小。账户自己的数字 (“operator”) 用 client.RequestLimit(ctx, "volumes", …) 或 "volume_bytes" 申请。

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

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

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

错误

非零退出码是一个结果:读取 res.ExitCode 即可。API 的错误是一个 *glac.Error,带有 Status、Type、Code、Param、Retryable、RequestID 和 Message,以及指向错误页面上对应错误码的 DocsURL。连不上、超时或 context 结束,也是一个 *glac.Error,Status 为 0;errors.Is(err, context.DeadlineExceeded) 仍然有效。与费用有关的错误(402 account_in_debt、403 trust_limit_exceeded、409 no_card、400 start_now_required 等)带有控制平面自己的错误信息,说明怎样解除。

var e *glac.Error
if errors.As(err, &e) {
	fmt.Println(e.Status, e.Code, e.RequestID)
}

值得了解的行为

  • 休眠的沙箱。不会有调用因为沙箱在休眠或正在启动而失败:它会等待,然后继续。res.Woke 和 res.Wake 说明 Run 是否唤醒了它,以及唤醒用了多久;客户端上的 OnResponse(f) 能看到每个调用的同样信息。
  • 重试。没有响应,或出现 retryable 错误时,会重试两次 (Retries(n)),间隔从 0.2 秒开始翻倍。每个 POST 都带有 Idempotency-Key;IdempotencyKey(key) 让重启后的程序能拿回它之前创建的沙箱、命令或版本。
  • 超时。调用一直持续到它的 context 结束;RequestTimeout(默认 15 分钟)是等待 API 的时长。Timeout(d) 是命令的时限:超时后命令被终止,Status 为 timed_out。
  • 时长与 null。时长和时间分别是 time.Duration 和 time.Time。API 的 null 有特定含义时,有对应的选项(NeverSleep()、NoName()、NoEgress()、NoSleepNotice()、DefaultSleepHoldMax()),没有给出的选项不会改变任何东西。
  • Goroutine。Client(连同它的各产品服务)、Sandbox、Command 和 Files 都可以在多个 goroutine 中使用。
导航

输入以搜索……

↑↓ 切换↵ 选择Esc 关闭