github.com/thiagoyeds/go-cloud@v0.26.0/runtimevar/gcpsecretmanager/gcpsecretmanager.go (about) 1 // Copyright 2020 The Go Cloud Development Kit Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // https://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package gcpsecretmanager provides a runtimevar implementation with 16 // secrets read from GCP Secret Manager 17 // (https://cloud.google.com/secret-manager). 18 // Use OpenVariable to construct a *runtimevar.Variable. 19 // 20 // URLs 21 // 22 // For runtimevar.OpenVariable, gcpsecretmanager registers for the scheme 23 // "gcpsecretmanager". 24 // The default URL opener will creating a connection using use default 25 // credentials from the environment, as described in 26 // https://cloud.google.com/docs/authentication/production. 27 // To customize the URL opener, or for more details on the URL format, 28 // see URLOpener. 29 // See https://gocloud.dev/concepts/urls/ for background information. 30 // 31 // As 32 // 33 // gcpsecretmanager exposes the following types for As: 34 // - Snapshot: *secretmanagerpb.AccessSecretVersionResponse 35 // - Error: *status.Status 36 package gcpsecretmanager // import "gocloud.dev/runtimevar/gcpsecretmanager" 37 38 import ( 39 "bytes" 40 "context" 41 "errors" 42 "fmt" 43 "net/url" 44 "path" 45 "regexp" 46 "sync" 47 "time" 48 49 secretmanager "cloud.google.com/go/secretmanager/apiv1" 50 "github.com/google/wire" 51 "gocloud.dev/gcerrors" 52 "gocloud.dev/gcp" 53 "gocloud.dev/internal/gcerr" 54 "gocloud.dev/runtimevar" 55 "gocloud.dev/runtimevar/driver" 56 "google.golang.org/api/option" 57 secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" 58 "google.golang.org/grpc" 59 "google.golang.org/grpc/codes" 60 "google.golang.org/grpc/credentials" 61 "google.golang.org/grpc/credentials/oauth" 62 "google.golang.org/grpc/status" 63 ) 64 65 // Dial opens a gRPC connection to the Secret Manager API using 66 // credentials from ts. It is provided as an optional helper with useful 67 // defaults. 68 // 69 // The second return value is a function that should be called to clean up 70 // the connection opened by Dial. 71 func Dial(ctx context.Context, ts gcp.TokenSource) (*secretmanager.Client, func(), error) { 72 client, err := secretmanager.NewClient(ctx, 73 option.WithGRPCDialOption( 74 grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), 75 ), 76 option.WithTokenSource(oauth.TokenSource{TokenSource: ts}), 77 option.WithUserAgent("runtimevar"), 78 ) 79 if err != nil { 80 return nil, nil, err 81 } 82 83 return client, func() { _ = client.Close() }, nil 84 } 85 86 func init() { 87 runtimevar.DefaultURLMux().RegisterVariable(Scheme, new(lazyCredsOpener)) 88 } 89 90 // Set holds Wire providers for this package. 91 var Set = wire.NewSet( 92 Dial, 93 wire.Struct(new(URLOpener), "Client"), 94 ) 95 96 // lazyCredsOpener obtains Application Default Credentials on the first call 97 // to OpenVariableURL. 98 type lazyCredsOpener struct { 99 init sync.Once 100 opener *URLOpener 101 err error 102 } 103 104 func (o *lazyCredsOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) { 105 o.init.Do(func() { 106 creds, err := gcp.DefaultCredentials(ctx) 107 if err != nil { 108 o.err = err 109 return 110 } 111 client, _, err := Dial(ctx, creds.TokenSource) 112 if err != nil { 113 o.err = err 114 return 115 } 116 o.opener = &URLOpener{Client: client} 117 }) 118 if o.err != nil { 119 return nil, fmt.Errorf("open variable %v: %v", u, o.err) 120 } 121 return o.opener.OpenVariableURL(ctx, u) 122 } 123 124 // Scheme is the URL scheme gcpsecretmanager registers its URLOpener under on runtimevar.DefaultMux. 125 const Scheme = "gcpsecretmanager" 126 127 // URLOpener opens gcpsecretmanager URLs like "gcpsecretmanager://projects/[project_id]/secrets/[secret_id]". 128 // 129 // The URL Host+Path are used as the GCP Secret Manager secret key; 130 // see https://cloud.google.com/secret-manager 131 // for more details. 132 // 133 // The following query parameters are supported: 134 // 135 // - decoder: The decoder to use. Defaults to URLOpener.Decoder, or 136 // runtimevar.BytesDecoder if URLOpener.Decoder is nil. 137 // See runtimevar.DecoderByName for supported values. 138 type URLOpener struct { 139 // Client must be set to a non-nil client authenticated with 140 // Secret Manager scope or equivalent. 141 Client *secretmanager.Client 142 143 // Decoder specifies the decoder to use if one is not specified in the URL. 144 // Defaults to runtimevar.BytesDecoder. 145 Decoder *runtimevar.Decoder 146 147 // Options specifies the options to pass to New. 148 Options Options 149 } 150 151 // OpenVariableURL opens a gcpsecretmanager Secret. 152 func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) { 153 q := u.Query() 154 155 decoderName := q.Get("decoder") 156 q.Del("decoder") 157 decoder, err := runtimevar.DecoderByName(ctx, decoderName, o.Decoder) 158 if err != nil { 159 return nil, fmt.Errorf("open variable %v: invalid decoder: %v", u, err) 160 } 161 162 for param := range q { 163 return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param) 164 } 165 return OpenVariable(o.Client, path.Join(u.Host, u.Path), decoder, &o.Options) 166 } 167 168 // Options sets options. 169 type Options struct { 170 // WaitDuration controls the rate at which Secret Manager is polled. 171 // Defaults to 30 seconds. 172 WaitDuration time.Duration 173 } 174 175 // OpenVariable constructs a *runtimevar.Variable backed by secretKey in GCP Secret Manager. 176 // 177 // A secretKey will look like: 178 // projects/[project_id]/secrets/[secret_id] 179 // 180 // A project ID is a unique, user-assigned ID of the Project. 181 // It must be 6 to 30 lowercase letters, digits, or hyphens. 182 // It must start with a letter. Trailing hyphens are prohibited. 183 // 184 // A secret ID is a string with a maximum length of 255 characters and can 185 // contain uppercase and lowercase letters, numerals, and the hyphen (`-`) and 186 // underscore (`_`) characters. 187 // 188 // gcpsecretmanager package will always use the latest secret value, 189 // so `/version/latest` postfix must NOT be added to the secret key. 190 // 191 // You can use the full string (e.g., copied from the GCP Console), or 192 // construct one from its parts using SecretKey. 193 // 194 // See https://cloud.google.com/secret-manager for more details. 195 // 196 // Secret Manager returns raw bytes; provide a decoder to decode the raw bytes 197 // into the appropriate type for runtimevar.Snapshot.Value. 198 // See the runtimevar package documentation for examples of decoders. 199 func OpenVariable(client *secretmanager.Client, secretKey string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) { 200 w, err := newWatcher(client, secretKey, decoder, opts) 201 if err != nil { 202 return nil, err 203 } 204 return runtimevar.New(w), nil 205 } 206 207 var secretKeyRE = regexp.MustCompile("^projects/[a-z][a-z0-9_\\-]{4,28}[a-z0-9_]/secrets/[a-zA-Z0-9_\\-]{1,255}$") 208 209 const latestVersion = "/versions/latest" 210 211 func newWatcher(client *secretmanager.Client, secretKey string, decoder *runtimevar.Decoder, opts *Options) (driver.Watcher, error) { 212 if opts == nil { 213 opts = &Options{} 214 } 215 216 if !secretKeyRE.MatchString(secretKey) { 217 return nil, fmt.Errorf("invalid secretKey %q; must match %v", secretKey, secretKeyRE) 218 } 219 220 return &watcher{ 221 client: client, 222 wait: driver.WaitDuration(opts.WaitDuration), 223 name: secretKey, 224 decoder: decoder, 225 }, nil 226 } 227 228 // SecretKey constructs a GCP Secret Manager secret key from component parts. 229 // See https://cloud.google.com/secret-manager for more details. 230 func SecretKey(projectID gcp.ProjectID, secretID string) string { 231 return "projects/" + string(projectID) + "/secrets/" + secretID 232 } 233 234 // state implements driver.State. 235 type state struct { 236 val interface{} 237 raw *secretmanagerpb.AccessSecretVersionResponse 238 updateTime time.Time 239 rawBytes []byte 240 err error 241 } 242 243 // Value implements driver.State.Value. 244 func (s *state) Value() (interface{}, error) { 245 return s.val, s.err 246 } 247 248 // UpdateTime implements driver.State.UpdateTime. 249 func (s *state) UpdateTime() time.Time { 250 return s.updateTime 251 } 252 253 // As implements driver.State.As. 254 func (s *state) As(i interface{}) bool { 255 if s.raw == nil { 256 return false 257 } 258 p, ok := i.(**secretmanagerpb.AccessSecretVersionResponse) 259 if !ok { 260 return false 261 } 262 *p = s.raw 263 return true 264 } 265 266 // errorState returns a new State with err, unless prevS also represents 267 // the same error, in which case it returns nil. 268 func errorState(err error, prevS driver.State) driver.State { 269 s := &state{err: err} 270 if prevS == nil { 271 return s 272 } 273 prev := prevS.(*state) 274 if prev.err == nil { 275 // New error. 276 return s 277 } 278 if equivalentError(err, prev.err) { 279 // Same error, return nil to indicate no change. 280 return nil 281 } 282 return s 283 } 284 285 // equivalentError returns true iff err1 and err2 represent an equivalent error; 286 // i.e., we don't want to return it to the user as a different error. 287 func equivalentError(err1, err2 error) bool { 288 if err1 == err2 || err1.Error() == err2.Error() { 289 return true 290 } 291 code1, code2 := status.Code(err1), status.Code(err2) 292 return code1 != codes.OK && code1 != codes.Unknown && code1 == code2 293 } 294 295 // watcher implements driver.Watcher for secrets provided by the Secret Manager service. 296 type watcher struct { 297 client *secretmanager.Client 298 wait time.Duration 299 name string 300 decoder *runtimevar.Decoder 301 } 302 303 // WatchVariable implements driver.WatchVariable. 304 func (w *watcher) WatchVariable(ctx context.Context, prev driver.State) (driver.State, time.Duration) { 305 latest := w.name + latestVersion 306 307 secret, err := w.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{Name: latest}) 308 if err != nil { 309 return errorState(err, prev), w.wait 310 } 311 312 if secret == nil || secret.Payload == nil || secret.Payload.Data == nil { 313 return errorState(errors.New("invalid secret payload"), prev), w.wait 314 } 315 316 meta, err := w.client.GetSecretVersion(ctx, &secretmanagerpb.GetSecretVersionRequest{Name: latest}) 317 if err != nil { 318 return errorState(err, prev), w.wait 319 } 320 321 createTime := meta.CreateTime.AsTime() 322 323 // See if it's the same raw bytes as before. 324 if prev != nil { 325 prevState, ok := prev.(*state) 326 if ok && prevState != nil && bytes.Equal(secret.Payload.Data, prevState.rawBytes) { 327 // No change! 328 return nil, w.wait 329 } 330 } 331 332 // Decode the value. 333 val, err := w.decoder.Decode(ctx, secret.Payload.Data) 334 if err != nil { 335 return errorState(err, prev), w.wait 336 } 337 338 // A secret version is immutable. 339 // The latest secret value creation time is the last time the secret value has been changed. 340 // Hence set updateTime as createTime. 341 return &state{val: val, raw: secret, updateTime: createTime, rawBytes: secret.Payload.Data}, w.wait 342 } 343 344 // Close implements driver.Close. 345 func (w *watcher) Close() error { 346 return nil 347 } 348 349 // ErrorAs implements driver.ErrorAs. 350 func (w *watcher) ErrorAs(err error, i interface{}) bool { 351 // FromError converts err to a *status.Status. 352 s, _ := status.FromError(err) 353 if p, ok := i.(**status.Status); ok { 354 *p = s 355 return true 356 } 357 return false 358 } 359 360 // ErrorCode implements driver.ErrorCode. 361 func (*watcher) ErrorCode(err error) gcerrors.ErrorCode { 362 return gcerr.GRPCCode(err) 363 }