github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/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 // - wait: The poll interval, in time.ParseDuration formats. 139 // Defaults to 30s. 140 type URLOpener struct { 141 // Client must be set to a non-nil client authenticated with 142 // Cloud RuntimeConfigurator scope or equivalent. 143 Client pb.RuntimeConfigManagerClient 144 145 // Decoder specifies the decoder to use if one is not specified in the URL. 146 // Defaults to runtimevar.BytesDecoder. 147 Decoder *runtimevar.Decoder 148 149 // Options specifies the options to pass to New. 150 Options Options 151 } 152 153 // OpenVariableURL opens a gcpruntimeconfig Variable for u. 154 func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) { 155 q := u.Query() 156 157 decoderName := q.Get("decoder") 158 q.Del("decoder") 159 decoder, err := runtimevar.DecoderByName(ctx, decoderName, o.Decoder) 160 if err != nil { 161 return nil, fmt.Errorf("open variable %v: invalid decoder: %v", u, err) 162 } 163 opts := o.Options 164 if s := q.Get("wait"); s != "" { 165 q.Del("wait") 166 d, err := time.ParseDuration(s) 167 if err != nil { 168 return nil, fmt.Errorf("open variable %v: invalid wait %q: %v", u, s, err) 169 } 170 opts.WaitDuration = d 171 } 172 173 for param := range q { 174 return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param) 175 } 176 return OpenVariable(o.Client, path.Join(u.Host, u.Path), decoder, &opts) 177 } 178 179 // Options sets options. 180 type Options struct { 181 // WaitDuration controls the rate at which Parameter Store is polled. 182 // Defaults to 30 seconds. 183 WaitDuration time.Duration 184 } 185 186 // OpenVariable constructs a *runtimevar.Variable backed by variableKey in 187 // GCP Cloud Runtime Configurator. 188 // 189 // A variableKey will look like: 190 // 191 // projects/[project_id]/configs/[CONFIG_ID]/variables/[VARIABLE_NAME] 192 // 193 // You can use the full string (e.g., copied from the GCP Console), or 194 // construct one from its parts using VariableKey. 195 // 196 // See https://cloud.google.com/deployment-manager/runtime-configurator/ for 197 // more details. 198 // 199 // Runtime Configurator returns raw bytes; provide a decoder to decode the raw bytes 200 // into the appropriate type for runtimevar.Snapshot.Value. 201 // See the runtimevar package documentation for examples of decoders. 202 func OpenVariable(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) { 203 w, err := newWatcher(client, variableKey, decoder, opts) 204 if err != nil { 205 return nil, err 206 } 207 return runtimevar.New(w), nil 208 } 209 210 var variableKeyRE = regexp.MustCompile("^projects/.+/configs/.+/variables/.+$") 211 212 func newWatcher(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (driver.Watcher, error) { 213 if opts == nil { 214 opts = &Options{} 215 } 216 if !variableKeyRE.MatchString(variableKey) { 217 return nil, fmt.Errorf("invalid variableKey %q; must match %v", variableKey, variableKeyRE) 218 } 219 return &watcher{ 220 client: client, 221 wait: driver.WaitDuration(opts.WaitDuration), 222 name: variableKey, 223 decoder: decoder, 224 }, nil 225 } 226 227 // VariableKey constructs a GCP Runtime Configurator variable key from 228 // component parts. See 229 // https://cloud.google.com/deployment-manager/runtime-configurator/ 230 // for more details. 231 func VariableKey(projectID gcp.ProjectID, configID, variableName string) string { 232 return fmt.Sprintf("projects/%s/configs/%s/variables/%s", projectID, configID, variableName) 233 } 234 235 // state implements driver.State. 236 type state struct { 237 val interface{} 238 raw *pb.Variable 239 updateTime time.Time 240 rawBytes []byte 241 err error 242 } 243 244 // Value implements driver.State.Value. 245 func (s *state) Value() (interface{}, error) { 246 return s.val, s.err 247 } 248 249 // UpdateTime implements driver.State.UpdateTime. 250 func (s *state) UpdateTime() time.Time { 251 return s.updateTime 252 } 253 254 // As implements driver.State.As. 255 func (s *state) As(i interface{}) bool { 256 if s.raw == nil { 257 return false 258 } 259 p, ok := i.(**pb.Variable) 260 if !ok { 261 return false 262 } 263 *p = s.raw 264 return true 265 } 266 267 // errorState returns a new State with err, unless prevS also represents 268 // the same error, in which case it returns nil. 269 func errorState(err error, prevS driver.State) driver.State { 270 s := &state{err: err} 271 if prevS == nil { 272 return s 273 } 274 prev := prevS.(*state) 275 if prev.err == nil { 276 // New error. 277 return s 278 } 279 if equivalentError(err, prev.err) { 280 // Same error, return nil to indicate no change. 281 return nil 282 } 283 return s 284 } 285 286 // equivalentError returns true iff err1 and err2 represent an equivalent error; 287 // i.e., we don't want to return it to the user as a different error. 288 func equivalentError(err1, err2 error) bool { 289 if err1 == err2 || err1.Error() == err2.Error() { 290 return true 291 } 292 code1, code2 := status.Code(err1), status.Code(err2) 293 return code1 != codes.OK && code1 != codes.Unknown && code1 == code2 294 } 295 296 // watcher implements driver.Watcher for configurations provided by the Runtime Configurator 297 // service. 298 type watcher struct { 299 client pb.RuntimeConfigManagerClient 300 wait time.Duration 301 name string 302 decoder *runtimevar.Decoder 303 } 304 305 // WatchVariable implements driver.WatchVariable. 306 func (w *watcher) WatchVariable(ctx context.Context, prev driver.State) (driver.State, time.Duration) { 307 // Get the variable from the backend. 308 vpb, err := w.client.GetVariable(ctx, &pb.GetVariableRequest{Name: w.name}) 309 if err != nil { 310 return errorState(err, prev), w.wait 311 } 312 updateTime, err := parseUpdateTime(vpb) 313 if err != nil { 314 return errorState(err, prev), w.wait 315 } 316 // See if it's the same raw bytes as before. 317 b := bytesFromProto(vpb) 318 if prev != nil && bytes.Equal(b, prev.(*state).rawBytes) { 319 // No change! 320 return nil, w.wait 321 } 322 323 // Decode the value. 324 val, err := w.decoder.Decode(ctx, b) 325 if err != nil { 326 return errorState(err, prev), w.wait 327 } 328 return &state{val: val, raw: vpb, updateTime: updateTime, rawBytes: b}, w.wait 329 } 330 331 // Close implements driver.Close. 332 func (w *watcher) Close() error { 333 return nil 334 } 335 336 // ErrorAs implements driver.ErrorAs. 337 func (w *watcher) ErrorAs(err error, i interface{}) bool { 338 // FromError converts err to a *status.Status. 339 s, _ := status.FromError(err) 340 if p, ok := i.(**status.Status); ok { 341 *p = s 342 return true 343 } 344 return false 345 } 346 347 // ErrorCode implements driver.ErrorCode. 348 func (*watcher) ErrorCode(err error) gcerrors.ErrorCode { 349 return gcerr.GRPCCode(err) 350 } 351 352 func bytesFromProto(vpb *pb.Variable) []byte { 353 // Proto may contain either bytes or text. If it contains text content, convert that to []byte. 354 if _, isBytes := vpb.GetContents().(*pb.Variable_Value); isBytes { 355 return vpb.GetValue() 356 } 357 return []byte(vpb.GetText()) 358 } 359 360 func parseUpdateTime(vpb *pb.Variable) (time.Time, error) { 361 return vpb.GetUpdateTime().AsTime(), nil 362 }