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  }