@@ -15,22 +15,41 @@ package signedrequests
1515
1616import (
1717 "context"
18+ "encoding/base64"
19+ "encoding/json"
1820 "errors"
1921 "fmt"
2022 "io"
2123 "strconv"
2224 "time"
2325
26+ "cloud.google.com/go/compute/metadata"
2427 "cloud.google.com/go/storage"
2528 "github.com/thanos-io/objstore/providers/gcs"
2629 "golang.org/x/oauth2/google"
30+ iamcredentials "google.golang.org/api/iamcredentials/v1"
2731 "google.golang.org/api/option"
2832 "gopkg.in/yaml.v3"
2933)
3034
3135type GCSClient struct {
3236 bucket * storage.BucketHandle
3337 closer io.Closer
38+
39+ // googleAccessID is the service account email used to sign URLs. Detected
40+ // once at construction time; passing it explicitly into SignedURLOptions
41+ // lets us also override SignBytes, which is what enables trace-context
42+ // propagation into the IAM SignBlob call.
43+ googleAccessID string
44+
45+ // privateKey is set when the service account JSON contains one, enabling
46+ // local signing without a round-trip to IAM.
47+ privateKey []byte
48+
49+ // iamService is used for remote signing via IAM SignBlob when no private
50+ // key is available. It is context-aware, so spans emitted by its HTTP
51+ // transport are parented to the caller's trace.
52+ iamService * iamcredentials.Service
3453}
3554
3655func NewGCSClient (ctx context.Context , conf []byte ) (* GCSClient , error ) {
@@ -47,28 +66,107 @@ func NewGCSBucketWithConfig(ctx context.Context, gc gcs.Config) (*GCSClient, err
4766 return nil , errors .New ("missing Google Cloud Storage bucket name for stored blocks" )
4867 }
4968
50- var opts []option.ClientOption
51-
52- // If ServiceAccount is provided, use them in GCS client, otherwise fallback to Google default logic.
69+ var (
70+ creds * google.Credentials
71+ err error
72+ )
5373 if gc .ServiceAccount != "" {
54- credentials , err := google .CredentialsFromJSONWithType (ctx , []byte (gc .ServiceAccount ), google .ServiceAccount , storage .ScopeFullControl )
55- if err != nil {
56- return nil , fmt .Errorf ("create credentials from JSON: %w" , err )
57- }
58- opts = append (opts , option .WithCredentials (credentials ))
74+ creds , err = google .CredentialsFromJSONWithType (ctx , []byte (gc .ServiceAccount ), google .ServiceAccount , storage .ScopeFullControl )
75+ } else {
76+ creds , err = google .FindDefaultCredentials (ctx , storage .ScopeFullControl )
77+ }
78+ if err != nil {
79+ return nil , fmt .Errorf ("load credentials: %w" , err )
5980 }
6081
61- opts = append (opts , option .WithUserAgent ("parca" ))
82+ opts := []option.ClientOption {
83+ option .WithCredentials (creds ),
84+ option .WithUserAgent ("parca" ),
85+ }
6286
6387 gcsClient , err := storage .NewClient (ctx , opts ... )
6488 if err != nil {
6589 return nil , err
6690 }
6791
68- return & GCSClient {
69- bucket : gcsClient .Bucket (gc .Bucket ),
70- closer : gcsClient ,
71- }, nil
92+ googleAccessID , privateKey , err := signingIdentity (ctx , creds )
93+ if err != nil {
94+ return nil , fmt .Errorf ("resolve signing identity: %w" , err )
95+ }
96+
97+ c := & GCSClient {
98+ bucket : gcsClient .Bucket (gc .Bucket ),
99+ closer : gcsClient ,
100+ googleAccessID : googleAccessID ,
101+ privateKey : privateKey ,
102+ }
103+
104+ if len (privateKey ) == 0 {
105+ iamService , err := iamcredentials .NewService (ctx , opts ... )
106+ if err != nil {
107+ return nil , fmt .Errorf ("create iamcredentials service: %w" , err )
108+ }
109+ c .iamService = iamService
110+ }
111+
112+ return c , nil
113+ }
114+
115+ // signingIdentity returns the service account email and (optionally) a private
116+ // key for URL signing. Mirrors storage.BucketHandle.detectDefaultGoogleAccessID,
117+ // which is unexported — we need this surfaced because overriding SignBytes (to
118+ // thread context through to IAM) means the library no longer fills in
119+ // GoogleAccessID for our closure.
120+ func signingIdentity (ctx context.Context , creds * google.Credentials ) (string , []byte , error ) {
121+ if len (creds .JSON ) > 0 {
122+ var sa struct {
123+ ClientEmail string `json:"client_email"`
124+ PrivateKey string `json:"private_key"`
125+ }
126+ if err := json .Unmarshal (creds .JSON , & sa ); err == nil && sa .ClientEmail != "" {
127+ return sa .ClientEmail , []byte (sa .PrivateKey ), nil
128+ }
129+ }
130+
131+ if ! metadata .OnGCE () {
132+ return "" , nil , errors .New ("could not resolve service account email from credentials JSON and not running on GCE" )
133+ }
134+ email , err := metadata .EmailWithContext (ctx , "default" )
135+ if err != nil {
136+ return "" , nil , fmt .Errorf ("read service account email from GCE metadata: %w" , err )
137+ }
138+ if email == "" {
139+ return "" , nil , errors .New ("empty service account email from GCE metadata" )
140+ }
141+ return email , nil , nil
142+ }
143+
144+ // signBytesWithContext returns a SignBytes function that calls IAM SignBlob
145+ // with the given context. Using our context here means the HTTP span emitted
146+ // by the IAM client inherits the caller's trace.
147+ func (c * GCSClient ) signBytesWithContext (ctx context.Context ) func ([]byte ) ([]byte , error ) {
148+ return func (in []byte ) ([]byte , error ) {
149+ resp , err := c .iamService .Projects .ServiceAccounts .SignBlob (
150+ fmt .Sprintf ("projects/-/serviceAccounts/%s" , c .googleAccessID ),
151+ & iamcredentials.SignBlobRequest {
152+ Payload : base64 .StdEncoding .EncodeToString (in ),
153+ },
154+ ).Context (ctx ).Do ()
155+ if err != nil {
156+ return nil , fmt .Errorf ("iam sign blob: %w" , err )
157+ }
158+ return base64 .StdEncoding .DecodeString (resp .SignedBlob )
159+ }
160+ }
161+
162+ func (c * GCSClient ) signedURLOptions (ctx context.Context , base * storage.SignedURLOptions ) * storage.SignedURLOptions {
163+ base .GoogleAccessID = c .googleAccessID
164+ if len (c .privateKey ) > 0 {
165+ base .PrivateKey = c .privateKey
166+ } else {
167+ base .SignBytes = c .signBytesWithContext (ctx )
168+ }
169+ return base
72170}
73171
74172func (c * GCSClient ) Close () error {
@@ -81,22 +179,22 @@ func (c *GCSClient) SignedPUT(
81179 size int64 ,
82180 expiry time.Time ,
83181) (string , error ) {
84- return c .bucket .SignedURL (objectKey , & storage.SignedURLOptions {
182+ return c .bucket .SignedURL (objectKey , c . signedURLOptions ( ctx , & storage.SignedURLOptions {
85183 Method : "PUT" ,
86184 Expires : expiry ,
87185 Headers : []string {
88186 "X-Upload-Content-Length:" + strconv .FormatInt (size , 10 ),
89187 },
90- })
188+ }))
91189}
92190
93191func (c * GCSClient ) SignedGET (
94192 ctx context.Context ,
95193 objectKey string ,
96194 expiry time.Time ,
97195) (string , error ) {
98- return c .bucket .SignedURL (objectKey , & storage.SignedURLOptions {
196+ return c .bucket .SignedURL (objectKey , c . signedURLOptions ( ctx , & storage.SignedURLOptions {
99197 Method : "GET" ,
100198 Expires : expiry ,
101- })
199+ }))
102200}
0 commit comments