Skip to content

blob/gcsblob: support the gRPC API and Rapid Storage (zonal) buckets - #3772

Open
stanhu wants to merge 2 commits into
google:masterfrom
stanhu:gocloud-rapid-storage-support
Open

blob/gcsblob: support the gRPC API and Rapid Storage (zonal) buckets#3772
stanhu wants to merge 2 commits into
google:masterfrom
stanhu:gocloud-rapid-storage-support

Conversation

@stanhu

@stanhu stanhu commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

This pull request adds support for the Cloud Storage gRPC API to gcsblob, and on top of it, support for Rapid Storage (zonal) buckets.

Updated after review. Options.UseGRPC and Options.UseZonalAPIs are gone, replaced by a Dial plus a constructor following secrets/gcpkms. TestConformanceGRPC is now a record/replay test with golden files, and passes all 88 checks. Details in "What changed since the review" at the bottom.

API

func DialGRPC(ctx context.Context, ts gcp.TokenSource, opts ...option.ClientOption) (*storage.Client, func(), error)
func OpenBucketGRPC(client *storage.Client, bucketName string, opts *Options) (*blob.Bucket, error)
c, cleanup, err := gcsblob.DialGRPC(ctx, ts,
	storage.WithAppendableUploads(), storage.WithGRPCBidiReads())
if err != nil { ... }
defer cleanup()
b, err := gcsblob.OpenBucketGRPC(c, "my-rapid-bucket", nil)

OpenBucketGRPC takes no context, matching gcpkms.OpenKeeper: once a client exists there is nothing left to cancel. Options.Client, which has accepted a *storage.Client since v0.44.0, keeps working unchanged.

For callers whose entire configuration is a URL string, and who therefore have nowhere to put a client, there are two query parameters:

Parameter Effect
grpc=true Uses the gRPC transport.
zonal=true Additionally enables the zonal bucket APIs. Implies grpc=true.
b, err := blob.OpenBucket(ctx, "gs://my-rapid-bucket?zonal=true")

Combining zonal=true with an explicit grpc=false is rejected rather than silently overridden. URLOpener gains a TokenSource, populated by lazyCredsOpener from the credentials it already resolves, because a gRPC client cannot reuse an HTTP one.

Why Rapid Storage needs more than a transport switch

Zonal buckets accept only appendable object uploads. Every ordinary write is rejected, on both transports:

gs://bucket?grpc=true   InvalidArgument: This bucket type only supports appendable objects
gs://bucket             googleapi: Error 400: This bucket requires appendable objects

An appendable object is uploaded over a bidirectional stream. It becomes visible as soon as the first bytes are flushed and stays open for further writes until something finalizes it. Ordinary uploads, one-shot or resumable, produce an object only once the whole payload has been sent. Zonal buckets support the appendable form and nothing else.

So two things are needed:

  1. storage.WithAppendableUploads() and storage.WithGRPCBidiReads() on the client. The first makes ObjectHandle.NewWriter default Writer.Append to true, which selects the appendable upload path; the second switches reads to the bidirectional API. These were a single experimental.WithZonalBucketAPIs() until storage v1.67.0 split them and graduated both out of experimental.
  2. Writer.FinalizeOnClose on the writer. An appendable object stays open by default, so Close leaves an unfinalized object exposing only the bytes that happened to be flushed. For a payload small enough to fit in one buffer that is a zero-length object, even though Close returned no error.

Reads already worked on both transports without any change.

Notes for review

FinalizeOnClose is set unconditionally in NewTypedWriter. This looked risky to me too, so I traced it. The field never appears in http_client.go, so the JSON/HTTP writer cannot see it. In grpc_writer.go the only read is gRPCAppendBidiWriteBufferSender.send, and pickBufferSender returns that sender only when Writer.Append is set. It is dead code on every path gcsblob uses today.

Buckets now close the client they created. Close was a no-op. A gRPC client owns a connection pool, so every OpenBucketURL with grpc/zonal leaked one for the process lifetime. Clients supplied by the caller, through OpenBucketGRPC or Options.Client, are left alone.

Emulator handling on the gRPC path is left to the storage library. Reusing STORAGE_EMULATOR_HOST would not work: it is the HTTP endpoint, and a local emulator needs a separate port for gRPC. Passing it to option.WithEndpoint alongside option.WithoutAuthentication would also still dial over TLS, because skipping credentials does not make the transport plaintext. defaultGRPCOptions already reads STORAGE_EMULATOR_HOST_GRPC, strips the scheme, dials insecurely and disables client metrics, and those defaults are merged ahead of caller-supplied options. lazyCredsOpener checks that variable too.

gRPC on its own is not a speedup. From an n2-standard-4 in us-central1-c, 1 KiB objects each read exactly once, n=1000:

Bucket JSON/HTTP p50 JSON/HTTP p99 gRPC p50 gRPC p99
Rapid Storage (zonal), zonal=true * * 12.0 ms 34.5 ms
NAM4 dual-region 35.4 ms 69.2 ms 35.5 ms 73.2 ms
US multi-region 61.2 ms 150.9 ms 59.2 ms 124.6 ms

* Not comparable: a zonal bucket rejects every write over JSON/HTTP.

On the standard buckets plain gRPC is a wash, and on a smaller e2-standard-4 it was consistently slower, so the docs say to measure before enabling it. The win is the zonal bucket: 2.9x faster than NAM4 and 5.1x faster than multi-region.

Testing

go test ./blob/gcsblob/ passes and the existing replay tests are unaffected. New unit tests cover the grpc and zonal parameters, their invalid values, the zonal=true plus grpc=false conflict, and OpenBucketGRPC including that the caller's client survives bucket.Close.

TestConformanceGRPC runs the full drivertest suite over gRPC. Against a US multi-region bucket all 88 checks pass, repeatedly.

It records to and replays from testdata/TestConformanceGRPC, the same way TestConformance does. 79 golden files, 748 KB. Four consecutive offline replays with credentials pointed at nonexistent paths give identical results.

Getting there needed three fixes to grpcreplay, all now merged and released in v1.5.0, which master already depends on:

go-replayers#70 storage reads objects with a zero-copy codec, so RecvMsg is handed a *mem.BufferSlice rather than a proto.Message and grpcreplay panicked on the first read.
go-replayers#71 A subtest that makes no RPCs produced a 9-byte golden file the replayer rejected with "missing initial state". Seven subtests here do that.
go-replayers#72 Concurrent writes over bidirectional streams were matched to recorded streams by method alone, non-deterministically. Replaying one recording three times gave three different answers.

The golden files need regenerating. grpcBucketName is currently a personal bucket, because the bucket name is part of every recorded gRPC request and a replay only matches against the bucket that was recorded. It is deliberately a separate constant from bucketName so that regenerating the gRPC files cannot invalidate the JSON/HTTP ones. The constant and testdata/TestConformanceGRPC have to be regenerated together, with --record.

I also verified against a real Rapid Storage bucket that writes, reads, attributes, range reads and 1 MiB writes all succeed where they fail outright on master, and that objects written through zonal=true come back finalized, size=23 with a non-zero Finalized, against size=0 without the FinalizeOnClose line.

TestConformanceGRPCZonal is skipped

It is present but skipped unconditionally, because it cannot pass. Deleting one t.Skip enables it once specific conformance tests can be disabled. Against a Rapid Storage bucket, 55 checks pass and 33 fail, none of them driver bugs:

  • Rapid storage class objects do not support rewrite accounts for three of the five failing groups. Only TestCopy is about copying; TestKeys and TestAs copy incidentally, and TestKeys alone contributes 19 failures because it copies once per weird key.
  • Listing with a delimiter other than / fails with Invalid argument. That is a hierarchical namespace restriction, which Rapid Storage inherits by requiring HNS, rather than anything to do with zonal buckets or gRPC.
  • TestWrite hits the per-object mutation rate limit. Not a Rapid Storage limit at all, just the general GCS cap, and it only appeared when running from a VM in the bucket's zone, fast enough to trip it. The failure count varied with distance: 27 from a laptop, 33 from in-zone.

drivertest cannot express any of this. Its only opt-out is the Unimplemented error code, and all six places that honor it guard SignedURL; testCopy treats any error from Copy as a failure. The test's doc comment names the exact subtests to disable and why, so it can be switched on by deleting one t.Skip once selective disabling exists.

What this PR does not do

It does not deliver the sub-millisecond reads Rapid Storage is advertised for.

Sub-millisecond is possible, but under certain conditions. You only get it on later reads of one particular object, from a process that has kept a bidirectional read stream open to that object. The first read of any given object costs 11 to 12 ms no matter which API you use.

Same 1 KiB object, same client, n=1000:

Read p50 min
Never read before, via NewReader. This is what the driver does. 12.0 ms 6.7 ms
Never read before, by opening a MultiRangeDownloader for it 11.1 ms 6.3 ms
Same object again, on the stream already open 0.95 ms 0.59 ms
Same object again, with a ReadHandle cached from the earlier read 4.3 ms 2.6 ms

Getting there means keeping state alive between reads. Two ways:

  • Cache open MultiRangeDownloaders per object on the driver's bucket. Reaches the third row. Needs idle expiry, cleanup from bucket.Close, and an adapter from Add's callback to io.Reader.
  • Cache ReadHandles per object. Much smaller, but only reaches the fourth row.

Neither has anywhere to live today. driver.Bucket.NewRangeReader hands back a fresh driver.Reader per call, and the portable layer makes a new one for every read and discards it on a Seek.

What changed since the review

  • Options.UseGRPC and Options.UseZonalAPIs removed. Replaced by DialGRPC and OpenBucketGRPC, per your gcpkms suggestion.
  • The half-used gcp.HTTPClient is gone from the gRPC path. You were right that it was odd. It was not entirely ignored, its OAuth2 token source was reused, but everything else about it was dropped, and when the transport was not exactly an *oauth2.Transport the code silently fell back to option.WithoutAuthentication. A caller with a wrapped transport got an anonymous client and 403s at request time. Now a nil token source means unauthenticated explicitly, and a gRPC URL with neither credentials nor anonymous=true is an error.
  • Client ownership fixed. See the note above; Close used to leak.
  • Rebased onto master, which picks up 12ac9a59 and fixes the golangci-lint failure. That failure was never related to this change; it was Go 1.27 against golangci-lint v2.12.
  • TestConformanceGRPC and TestConformanceGRPCZonal added, with the caveats above. The zonal one is skipped unconditionally with a comment naming the subtests that need disabling and why, per your suggestion.
  • TestConformanceGRPC is now a record/replay test with committed golden files, passing 88 of 88. This needed three grpcreplay fixes, all merged and released in v1.5.0.
  • Migrated off experimental.WithZonalBucketAPIs, which storage v1.67.0 removed. It is now storage.WithAppendableUploads() plus storage.WithGRPCBidiReads(), both of which have graduated out of experimental, so this PR no longer depends on an experimental package.

Two things I would rather you decide:

  1. Should Options.Client be marked deprecated now that OpenBucketGRPC exists? It is released API, added in ab74300b, so it cannot be withdrawn, but it is now a second way to do the same thing.
  2. Should the grpc and zonal URL parameters stay? You said the URL option is fine, and I have kept them, but they are the part of this PR that adds surface area. The argument for them is that a caller configured purely by a connection string has no other way to reach a Rapid Storage bucket; anyone writing Go already has OpenBucketGRPC.

@vangent

vangent commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Can you merge with HEAD? I think that might fix the golangci-lint problem.

Comment thread blob/gcsblob/gcsblob.go Outdated
option.WithEndpoint("http://" + host + "/storage/v1/"),
option.WithHTTPClient(http.DefaultClient),
}
// storage.NewClient and storage.NewGRPCClient share the same signature; the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the "client *gcp.HTTPClient" passed in to the constructor here is getting ignored? That seems odd.

Maybe enforce that client is nil to make that more clear?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenKeeper for KMS has a constructor that takes a client (so that the caller can do whatever with it); maybe that's a better pattern here?

I.e., the grpc=true URL option is fine, and controls what the URL opener does, but there's no "UseGRPC" Option; instead, there are two separate OpenBucket constructors, one for HTTP and one for gRPC, where the latter takes a storage.Client, and we provide a Dial to create it pre-wrapped similar to KMS ("cloudkms.NewKeyManagementClient(ctx, option.WithTokenSource(ts), useragent.ClientOption("secrets"))").

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vangent I've updated this pull request to have:

func OpenBucket(ctx context.Context, client *gcp.HTTPClient, bucketName string, opts *Options) (*blob.Bucket, error)
func OpenBucketGRPC(client *storage.Client, bucketName string, opts *Options) (*blob.Bucket, error) {

I've kept grpc=true and zonal=true for query parameter support because this is the standard way to access a bucket with OpenBucketURL.

h.closer()
}

func TestConformance(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to merge this without running it through the conformance test.

You should be able to make a new function here, TestConformanceGRPC (and maybe another one, TestConformanceGRPCZonal), that uses a different newHarness-equivalent function (or refactor newHarness) that creates a gRPC client etc.

To generate the golden files locally you'll need to update the constants at the top of the file and run with --record. I'll ask you to revert the constant changes before merging, and I'll re-generate the golden files with our bucket after that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestConformanceGRPCZonal currently fails for a number of reasons:

TestCopy/Works
  got unexpected error copying blob: (code=InvalidArgument):
  Rapid storage class objects do not support rewrite.

TestListDelimiters/backslash
  (code=InvalidArgument): Invalid argument.        # non-"/" delimiter on an HNS bucket

TestWrite/write_with_explicit_ContentType_overrides_discovery
  NewWriter or Close got err (code=ResourceExhausted):
  The object <rapid bucket>/blob-for-reading exceeded the rate limit for object
  mutation operations (create, update, and delete).

https://docs.cloud.google.com/storage/docs/rapid/rapid-bucket mentions that object rewrites are not supported (https://docs.cloud.google.com/storage/docs/json_api/v1/objects/rewrite).

Rapid Storage also requires / as the delimeter (https://cloud.google.com/blog/products/storage-data-transfer/understanding-new-cloud-storage-hierarchical-namespace), so those tests fail as well.

For now I'll omit TestConformanceGRPCZonal until there's a better way to selectively disable conformance tests.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you go ahead and add the test, but comment it out with some comments explaining the above? When I have time I'll try to make it easier to disable specific conformance tests (with explanation) so that they can be enabled.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or "skip" rather than comment out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vangent Done!

To generate the golden files locally you'll need to update the constants at the top of the file and run with --record. I'll ask you to revert the constant changes before merging, and I'll re-generate the golden files with our bucket after that.

In order to generate the golden files for gRPC, we need this fix for go-replayers: google/go-replayers#70

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch 3 times, most recently from 3c0fa39 to 9983ce2 Compare September 1, 2026 19:00
@vangent

vangent commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Let me know when you're ready for another round of review, you'll need to upload the golden files as part of the PR for it to pass I think.

@codecov

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.02439% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.04%. Comparing base (5dbc7eb) to head (959234e).

Files with missing lines Patch % Lines
blob/gcsblob/gcsblob.go 89.02% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3772      +/-   ##
==========================================
+ Coverage   79.97%   80.04%   +0.07%     
==========================================
  Files         104      104              
  Lines       12219    12284      +65     
==========================================
+ Hits         9772     9833      +61     
- Misses       2446     2450       +4     
  Partials        1        1              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch from 9983ce2 to 7e35981 Compare September 2, 2026 17:35
@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch from 7e35981 to 959234e Compare September 2, 2026 18:56
vangent pushed a commit to google/go-replayers that referenced this pull request Sep 3, 2026
grpcreplay assumed every message on a stream was a proto.Message and did
an unchecked type assertion in message.set. RPCs that install a custom
gRPC codec break this assumption. The GCS client's zero-copy ReadObject
codec forces grpc.ForceCodecV2 and receives each message into a
*mem.BufferSlice, so RecvMsg panics:

  interface conversion: *mem.BufferSlice is not protoreflect.ProtoMessage

Recording writes worked, but reads could not be captured, which is why
the go-cloud gcsblob gRPC test ran only against a real bucket.

Record such messages as raw wire bytes instead. message now holds either
a proto.Message or a []byte; message.set type-switches and materializes a
copy of the BufferSlice (gRPC frees the buffers after RecvMsg returns).
A new raw_message field on the Entry proto carries the bytes, and replay
delivers them back into the caller's *mem.BufferSlice, mirroring the
codec's Unmarshal.

This requires google.golang.org/grpc v1.67.1 for the mem package.

Reported in google/go-cloud#3772
@vangent

vangent commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

I updated deps so that we've got the latest go-replayers including your contribution, so you should be able to add the gRPC replay golden files now.

@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch from 959234e to 6911d6d Compare September 7, 2026 04:57
@stanhu

stanhu commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

I updated deps so that we've got the latest go-replayers including your contribution, so you should be able to add the gRPC replay golden files now.

Thanks! It looks like go-replayers still needs a few other fixes:

@vangent

vangent commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

I updated deps so that we've got the latest go-replayers including your contribution, so you should be able to add the gRPC replay golden files now.

Thanks! It looks like go-replayers still needs a few other fixes:

Done

Rapid Storage (zonal) buckets cannot be used through this driver at
all. Every write is rejected, on both transports:

  gs://bucket?grpc=true
    InvalidArgument: This bucket type only supports appendable objects
  gs://bucket
    googleapi: Error 400: This bucket requires appendable objects

An appendable object is uploaded over a bidirectional stream. It
becomes visible as soon as the first bytes are flushed and stays open
for further writes until something finalizes it. Ordinary uploads,
one-shot or resumable, produce an object only once the whole payload
has been sent. Zonal buckets support the appendable form and nothing
else, which is why both transports reject a normal write.

Two things are needed. experimental.WithZonalBucketAPIs makes
ObjectHandle.NewWriter default Writer.Append to true, selecting the
appendable upload path, and switches reads to the bidirectional API.
Writer.FinalizeOnClose then makes Close finalize the object; without it
Close leaves an unfinalized object exposing only whatever prefix was
flushed, which for a small payload is a zero-length object even though
Close reported no error.

Reads already worked over both transports without any change.

The new API is a Dial plus a constructor, following secrets/gcpkms:

	func DialGRPC(ctx context.Context, ts gcp.TokenSource, opts ...option.ClientOption) (*storage.Client, func(), error)
	func OpenBucketGRPC(client *storage.Client, bucketName string, opts *Options) (*blob.Bucket, error)

	c, cleanup, err := gcsblob.DialGRPC(ctx, ts, experimental.WithZonalBucketAPIs())
	defer cleanup()
	b, err := gcsblob.OpenBucketGRPC(c, "my-rapid-bucket", nil)

OpenBucketGRPC takes no context, matching gcpkms.OpenKeeper: once a
client exists there is nothing left to cancel. Options.Client, which
has accepted a *storage.Client since v0.44.0, keeps working.

For callers whose whole configuration is a URL string and who therefore
cannot supply a client, the URL opener grows two parameters. grpc=true
selects the gRPC transport; zonal=true additionally enables the zonal
APIs and implies grpc=true. Combining zonal=true with an explicit
grpc=false is a contradiction and is rejected rather than silently
overridden. URLOpener gains a TokenSource, populated by lazyCredsOpener
from the credentials it already resolves, because a gRPC client cannot
reuse an HTTP one. A URL that asks for gRPC without a token source and
without anonymous=true is now an error instead of quietly producing an
unauthenticated client.

Buckets that build their own client close it in Close, which was
previously a no-op. A gRPC client owns a connection pool, so otherwise
every OpenBucketURL leaked one for the process lifetime. Clients passed
in by the caller are left alone.

FinalizeOnClose is set unconditionally in NewTypedWriter. The storage
library reads it only on the appendable write path, which
pickBufferSender selects solely when Writer.Append is set, so the
JSON/HTTP and plain gRPC paths are unaffected.

Emulator support on the gRPC path is left to storage.NewGRPCClient.
Reusing STORAGE_EMULATOR_HOST would not work: it is the HTTP endpoint,
and a local emulator needs a separate port for gRPC. Passing it to
option.WithEndpoint alongside option.WithoutAuthentication would also
still dial over TLS, since skipping credentials does not make the
transport plaintext. defaultGRPCOptions already reads
STORAGE_EMULATOR_HOST_GRPC, strips the scheme, dials insecurely and
disables client metrics, and those defaults are merged ahead of
caller-supplied options. lazyCredsOpener checks that variable too, so
pointing only the gRPC one at an emulator does not trigger an
Application Default Credentials lookup.

gRPC is not a speedup on its own. Measured from an n2-standard-4 in
us-central1-c, 1 KiB objects each read exactly once, n=1000, p50: on a
NAM4 dual-region bucket 35.5ms over gRPC against 35.4ms over JSON, and
on a US multi-region bucket 59.2ms against 61.2ms. The win comes from
the zonal bucket, at 12.0ms. The UseGRPC docs say to measure first.
@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch 2 times, most recently from e58b4a9 to 8a23bf5 Compare September 8, 2026 04:36
Add TestConformanceGRPC, which runs the full drivertest suite over the
Cloud Storage gRPC API, recording to and replaying from golden files the
same way TestConformance does. All 88 checks pass.

grpcBucketName is separate from bucketName because the bucket name is
part of every recorded gRPC request, so a replay only matches against
the bucket it was recorded from. Keeping the two apart means
regenerating one set of golden files cannot invalidate the other. These
were recorded against a personal bucket, so the constant and
testdata/TestConformanceGRPC need regenerating together; the procedure
is in the test's doc comment.

TestConformanceGRPCZonal covers the same suite with the zonal bucket
APIs enabled, and is skipped unconditionally. Against a Rapid Storage
bucket 55 checks pass and 33 fail, every one a Cloud Storage restriction
rather than a driver bug, and drivertest cannot express "this driver
does not support X": its only opt-out is gcerrors.Unimplemented, every
place honoring it guards SignedURL, and testCopy treats any error from
Copy as a failure. The test carries a comment naming the subtests to
disable and why, so it can be enabled by deleting one t.Skip:

  - TestCopy, TestKeys and TestAs, because Rapid Storage does not
    support object rewrite. TestKeys accounts for 19 of the 33 failures
    on its own, since it copies once per key it exercises.
  - TestListDelimiters/backslash and TestListDelimiters/abc, because a
    hierarchical namespace, which Rapid Storage requires, only supports
    "/" as a delimiter.
  - Four TestWrite subtests that rewrite one object in a tight loop and
    hit the general per-object mutation rate limit. Not a Rapid Storage
    restriction, and only visible when the client is close enough to
    trip it, so it may not need a permanent skip.

SignedURL is left unexercised: with no GoogleAccessID the driver reports
Unimplemented and drivertest skips those checks, so HTTPClient returns
nil. Signing is client-side and does not depend on the transport.
@stanhu
stanhu force-pushed the gocloud-rapid-storage-support branch from 8a23bf5 to 8584a07 Compare September 8, 2026 04:46
@stanhu

stanhu commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! I've updated the conformance tests. To re-record the golden files:

  1. Point grpcBucketName at a bucket to which you can write
  2. rm -rf blob/gcsblob/testdata/TestConformanceGRPC
  3. go test ./blob/gcsblob/ -run 'TestConformanceGRPC$' -record

Recording needs Application Default Credentials with write access to that bucket. Restricting -run keeps it from also re-recording TestConformance, which needs a private key for SignedURL. The bucket name is part of every recorded request, so grpcBucketName and the golden files always have to be regenerated together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants