github.com/thiagoyeds/go-cloud@v0.26.0/runtimevar/gcpruntimeconfig/gcpruntimeconfig.go (about) 1 // Copyright 2018 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 gcpruntimeconfig provides a runtimevar implementation with 16 // variables read from GCP Cloud Runtime Configurator 17 // (https://cloud.google.com/deployment-manager/runtime-configurator). 18 // Use OpenVariable to construct a *runtimevar.Variable. 19 // 20 // URLs 21 // 22 // For runtimevar.OpenVariable, gcpruntimeconfig registers for the scheme 23 // "gcpruntimeconfig". 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 // gcpruntimeconfig exposes the following types for As: 34 // - Snapshot: *pb.Variable 35 // - Error: *status.Status 36 package gcpruntimeconfig // import "gocloud.dev/runtimevar/gcpruntimeconfig" 37 38 import ( 39 "bytes" 40 "context" 41 "fmt" 42 "net/url" 43 "path" 44 "regexp" 45 "sync" 46 "time" 47 48 "github.com/google/wire" 49 "gocloud.dev/gcerrors" 50 "gocloud.dev/gcp" 51 "gocloud.dev/internal/gcerr" 52 "gocloud.dev/internal/useragent" 53 "gocloud.dev/runtimevar" 54 "gocloud.dev/runtimevar/driver" 55 pb "google.golang.org/genproto/googleapis/cloud/runtimeconfig/v1beta1" 56 "google.golang.org/grpc" 57 "google.golang.org/grpc/codes" 58 "google.golang.org/grpc/credentials" 59 "google.golang.org/grpc/credentials/oauth" 60 "google.golang.org/grpc/status" 61 ) 62 63 const ( 64 // endpoint is the address of the GCP Runtime Configurator API. 65 endPoint = "runtimeconfig.googleapis.com:443" 66 ) 67 68 // Dial opens a gRPC connection to the Runtime Configurator API using 69 // credentials from ts. It is provided as an optional helper with useful 70 // defaults. 71 // 72 // The second return value is a function that should be called to clean up 73 // the connection opened by Dial. 74 func Dial(ctx context.Context, ts gcp.TokenSource) (pb.RuntimeConfigManagerClient, func(), error) { 75 conn, err := grpc.DialContext(ctx, endPoint, 76 grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")), 77 grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}), 78 useragent.GRPCDialOption("runtimevar"), 79 ) 80 if err != nil { 81 return nil, nil, err 82 } 83 return pb.NewRuntimeConfigManagerClient(conn), func() { conn.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 gcpruntimeconfig registers its URLOpener under on runtimevar.DefaultMux. 125 const Scheme = "gcpruntimeconfig" 126 127 // URLOpener opens gcpruntimeconfig URLs like "gcpruntimeconfig://projects/[project_id]/configs/[CONFIG_ID]/variables/[VARIABLE_NAME]". 128 // 129 // The URL Host+Path are used as the GCP Runtime Configurator Variable key; 130 // see https://cloud.google.com/deployment-manager/runtime-configurator/ 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 // Cloud RuntimeConfigurator scope or equivalent. 141 Client pb.RuntimeConfigManagerClient 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 gcpruntimeconfig Variable for u. 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 Parameter Store is polled. 171 // Defaults to 30 seconds. 172 WaitDuration time.Duration 173 } 174 175 // OpenVariable constructs a *runtimevar.Variable backed by variableKey in 176 // GCP Cloud Runtime Configurator. 177 // 178 // A variableKey will look like: 179 // projects/[project_id]/configs/[CONFIG_ID]/variables/[VARIABLE_NAME] 180 // 181 // You can use the full string (e.g., copied from the GCP Console), or 182 // construct one from its parts using VariableKey. 183 // 184 // See https://cloud.google.com/deployment-manager/runtime-configurator/ for 185 // more details. 186 // 187 // Runtime Configurator returns raw bytes; provide a decoder to decode the raw bytes 188 // into the appropriate type for runtimevar.Snapshot.Value. 189 // See the runtimevar package documentation for examples of decoders. 190 func OpenVariable(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) { 191 w, err := newWatcher(client, variableKey, decoder, opts) 192 if err != nil { 193 return nil, err 194 } 195 return runtimevar.New(w), nil 196 } 197 198 var variableKeyRE = regexp.MustCompile("^projects/.+/configs/.+/variables/.+$") 199 200 func newWatcher(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (driver.Watcher, error) { 201 if opts == nil { 202 opts = &Options{} 203 } 204 if !variableKeyRE.MatchString(variableKey) { 205 return nil, fmt.Errorf("invalid variableKey %q; must match %v", variableKey, variableKeyRE) 206 } 207 return &watcher{ 208 client: client, 209 wait: driver.WaitDuration(opts.WaitDuration), 210 name: variableKey, 211 decoder: decoder, 212 }, nil 213 } 214 215 // VariableKey constructs a GCP Runtime Configurator variable key from 216 // component parts. See 217 // https://cloud.google.com/deployment-manager/runtime-configurator/ 218 // for more details. 219 func VariableKey(projectID gcp.ProjectID, configID, variableName string) string { 220 return fmt.Sprintf("projects/%s/configs/%s/variables/%s", projectID, configID, variableName) 221 } 222 223 // state implements driver.State. 224 type state struct { 225 val interface{} 226 raw *pb.Variable 227 updateTime time.Time 228 rawBytes []byte 229 err error 230 } 231 232 // Value implements driver.State.Value. 233 func (s *state) Value() (interface{}, error) { 234 return s.val, s.err 235 } 236 237 // UpdateTime implements driver.State.UpdateTime. 238 func (s *state) UpdateTime() time.Time { 239 return s.updateTime 240 } 241 242 // As implements driver.State.As. 243 func (s *state) As(i interface{}) bool { 244 if s.raw == nil { 245 return false 246 } 247 p, ok := i.(**pb.Variable) 248 if !ok { 249 return false 250 } 251 *p = s.raw 252 return true 253 } 254 255 // errorState returns a new State with err, unless prevS also represents 256 // the same error, in which case it returns nil. 257 func errorState(err error, prevS driver.State) driver.State { 258 s := &state{err: err} 259 if prevS == nil { 260 return s 261 } 262 prev := prevS.(*state) 263 if prev.err == nil { 264 // New error. 265 return s 266 } 267 if equivalentError(err, prev.err) { 268 // Same error, return nil to indicate no change. 269 return nil 270 } 271 return s 272 } 273 274 // equivalentError returns true iff err1 and err2 represent an equivalent error; 275 // i.e., we don't want to return it to the user as a different error. 276 func equivalentError(err1, err2 error) bool { 277 if err1 == err2 || err1.Error() == err2.Error() { 278 return true 279 } 280 code1, code2 := status.Code(err1), status.Code(err2) 281 return code1 != codes.OK && code1 != codes.Unknown && code1 == code2 282 } 283 284 // watcher implements driver.Watcher for configurations provided by the Runtime Configurator 285 // service. 286 type watcher struct { 287 client pb.RuntimeConfigManagerClient 288 wait time.Duration 289 name string 290 decoder *runtimevar.Decoder 291 } 292 293 // WatchVariable implements driver.WatchVariable. 294 func (w *watcher) WatchVariable(ctx context.Context, prev driver.State) (driver.State, time.Duration) { 295 // Get the variable from the backend. 296 vpb, err := w.client.GetVariable(ctx, &pb.GetVariableRequest{Name: w.name}) 297 if err != nil { 298 return errorState(err, prev), w.wait 299 } 300 updateTime, err := parseUpdateTime(vpb) 301 if err != nil { 302 return errorState(err, prev), w.wait 303 } 304 // See if it's the same raw bytes as before. 305 b := bytesFromProto(vpb) 306 if prev != nil && bytes.Equal(b, prev.(*state).rawBytes) { 307 // No change! 308 return nil, w.wait 309 } 310 311 // Decode the value. 312 val, err := w.decoder.Decode(ctx, b) 313 if err != nil { 314 return errorState(err, prev), w.wait 315 } 316 return &state{val: val, raw: vpb, updateTime: updateTime, rawBytes: b}, w.wait 317 } 318 319 // Close implements driver.Close. 320 func (w *watcher) Close() error { 321 return nil 322 } 323 324 // ErrorAs implements driver.ErrorAs. 325 func (w *watcher) ErrorAs(err error, i interface{}) bool { 326 // FromError converts err to a *status.Status. 327 s, _ := status.FromError(err) 328 if p, ok := i.(**status.Status); ok { 329 *p = s 330 return true 331 } 332 return false 333 } 334 335 // ErrorCode implements driver.ErrorCode. 336 func (*watcher) ErrorCode(err error) gcerrors.ErrorCode { 337 return gcerr.GRPCCode(err) 338 } 339 340 func bytesFromProto(vpb *pb.Variable) []byte { 341 // Proto may contain either bytes or text. If it contains text content, convert that to []byte. 342 if _, isBytes := vpb.GetContents().(*pb.Variable_Value); isBytes { 343 return vpb.GetValue() 344 } 345 return []byte(vpb.GetText()) 346 } 347 348 func parseUpdateTime(vpb *pb.Variable) (time.Time, error) { 349 return vpb.GetUpdateTime().AsTime(), nil 350 }