362 lines
11 KiB
Go
362 lines
11 KiB
Go
// Copyright 2018 The Go Cloud Development Kit Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package gcpruntimeconfig provides a runtimevar implementation with
|
|
// variables read from GCP Cloud Runtime Configurator
|
|
// (https://cloud.google.com/deployment-manager/runtime-configurator).
|
|
// Use OpenVariable to construct a *runtimevar.Variable.
|
|
//
|
|
// # URLs
|
|
//
|
|
// For runtimevar.OpenVariable, gcpruntimeconfig registers for the scheme
|
|
// "gcpruntimeconfig".
|
|
// The default URL opener will creating a connection using use default
|
|
// credentials from the environment, as described in
|
|
// https://cloud.google.com/docs/authentication/production.
|
|
// To customize the URL opener, or for more details on the URL format,
|
|
// see URLOpener.
|
|
// See https://gocloud.dev/concepts/urls/ for background information.
|
|
//
|
|
// # As
|
|
//
|
|
// gcpruntimeconfig exposes the following types for As:
|
|
// - Snapshot: *pb.Variable
|
|
// - Error: *status.Status
|
|
package gcpruntimeconfig // import "gocloud.dev/runtimevar/gcpruntimeconfig"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/wire"
|
|
"gocloud.dev/gcerrors"
|
|
"gocloud.dev/gcp"
|
|
"gocloud.dev/internal/gcerr"
|
|
"gocloud.dev/internal/useragent"
|
|
"gocloud.dev/runtimevar"
|
|
"gocloud.dev/runtimevar/driver"
|
|
pb "google.golang.org/genproto/googleapis/cloud/runtimeconfig/v1beta1"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/oauth"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
// endpoint is the address of the GCP Runtime Configurator API.
|
|
endPoint = "runtimeconfig.googleapis.com:443"
|
|
)
|
|
|
|
// Dial opens a gRPC connection to the Runtime Configurator API using
|
|
// credentials from ts. It is provided as an optional helper with useful
|
|
// defaults.
|
|
//
|
|
// The second return value is a function that should be called to clean up
|
|
// the connection opened by Dial.
|
|
func Dial(ctx context.Context, ts gcp.TokenSource) (pb.RuntimeConfigManagerClient, func(), error) {
|
|
conn, err := grpc.DialContext(ctx, endPoint,
|
|
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
|
|
grpc.WithPerRPCCredentials(oauth.TokenSource{TokenSource: ts}),
|
|
useragent.GRPCDialOption("runtimevar"),
|
|
)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return pb.NewRuntimeConfigManagerClient(conn), func() { conn.Close() }, nil
|
|
}
|
|
|
|
func init() {
|
|
runtimevar.DefaultURLMux().RegisterVariable(Scheme, new(lazyCredsOpener))
|
|
}
|
|
|
|
// Set holds Wire providers for this package.
|
|
var Set = wire.NewSet(
|
|
Dial,
|
|
wire.Struct(new(URLOpener), "Client"),
|
|
)
|
|
|
|
// lazyCredsOpener obtains Application Default Credentials on the first call
|
|
// to OpenVariableURL.
|
|
type lazyCredsOpener struct {
|
|
init sync.Once
|
|
opener *URLOpener
|
|
err error
|
|
}
|
|
|
|
func (o *lazyCredsOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) {
|
|
o.init.Do(func() {
|
|
creds, err := gcp.DefaultCredentials(ctx)
|
|
if err != nil {
|
|
o.err = err
|
|
return
|
|
}
|
|
client, _, err := Dial(ctx, creds.TokenSource)
|
|
if err != nil {
|
|
o.err = err
|
|
return
|
|
}
|
|
o.opener = &URLOpener{Client: client}
|
|
})
|
|
if o.err != nil {
|
|
return nil, fmt.Errorf("open variable %v: %v", u, o.err)
|
|
}
|
|
return o.opener.OpenVariableURL(ctx, u)
|
|
}
|
|
|
|
// Scheme is the URL scheme gcpruntimeconfig registers its URLOpener under on runtimevar.DefaultMux.
|
|
const Scheme = "gcpruntimeconfig"
|
|
|
|
// URLOpener opens gcpruntimeconfig URLs like "gcpruntimeconfig://projects/[project_id]/configs/[CONFIG_ID]/variables/[VARIABLE_NAME]".
|
|
//
|
|
// The URL Host+Path are used as the GCP Runtime Configurator Variable key;
|
|
// see https://cloud.google.com/deployment-manager/runtime-configurator/
|
|
// for more details.
|
|
//
|
|
// The following query parameters are supported:
|
|
//
|
|
// - decoder: The decoder to use. Defaults to URLOpener.Decoder, or
|
|
// runtimevar.BytesDecoder if URLOpener.Decoder is nil.
|
|
// See runtimevar.DecoderByName for supported values.
|
|
// - wait: The poll interval, in time.ParseDuration formats.
|
|
// Defaults to 30s.
|
|
type URLOpener struct {
|
|
// Client must be set to a non-nil client authenticated with
|
|
// Cloud RuntimeConfigurator scope or equivalent.
|
|
Client pb.RuntimeConfigManagerClient
|
|
|
|
// Decoder specifies the decoder to use if one is not specified in the URL.
|
|
// Defaults to runtimevar.BytesDecoder.
|
|
Decoder *runtimevar.Decoder
|
|
|
|
// Options specifies the options to pass to New.
|
|
Options Options
|
|
}
|
|
|
|
// OpenVariableURL opens a gcpruntimeconfig Variable for u.
|
|
func (o *URLOpener) OpenVariableURL(ctx context.Context, u *url.URL) (*runtimevar.Variable, error) {
|
|
q := u.Query()
|
|
|
|
decoderName := q.Get("decoder")
|
|
q.Del("decoder")
|
|
decoder, err := runtimevar.DecoderByName(ctx, decoderName, o.Decoder)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open variable %v: invalid decoder: %v", u, err)
|
|
}
|
|
opts := o.Options
|
|
if s := q.Get("wait"); s != "" {
|
|
q.Del("wait")
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open variable %v: invalid wait %q: %v", u, s, err)
|
|
}
|
|
opts.WaitDuration = d
|
|
}
|
|
|
|
for param := range q {
|
|
return nil, fmt.Errorf("open variable %v: invalid query parameter %q", u, param)
|
|
}
|
|
return OpenVariable(o.Client, path.Join(u.Host, u.Path), decoder, &opts)
|
|
}
|
|
|
|
// Options sets options.
|
|
type Options struct {
|
|
// WaitDuration controls the rate at which Parameter Store is polled.
|
|
// Defaults to 30 seconds.
|
|
WaitDuration time.Duration
|
|
}
|
|
|
|
// OpenVariable constructs a *runtimevar.Variable backed by variableKey in
|
|
// GCP Cloud Runtime Configurator.
|
|
//
|
|
// A variableKey will look like:
|
|
//
|
|
// projects/[project_id]/configs/[CONFIG_ID]/variables/[VARIABLE_NAME]
|
|
//
|
|
// You can use the full string (e.g., copied from the GCP Console), or
|
|
// construct one from its parts using VariableKey.
|
|
//
|
|
// See https://cloud.google.com/deployment-manager/runtime-configurator/ for
|
|
// more details.
|
|
//
|
|
// Runtime Configurator returns raw bytes; provide a decoder to decode the raw bytes
|
|
// into the appropriate type for runtimevar.Snapshot.Value.
|
|
// See the runtimevar package documentation for examples of decoders.
|
|
func OpenVariable(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (*runtimevar.Variable, error) {
|
|
w, err := newWatcher(client, variableKey, decoder, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return runtimevar.New(w), nil
|
|
}
|
|
|
|
var variableKeyRE = regexp.MustCompile("^projects/.+/configs/.+/variables/.+$")
|
|
|
|
func newWatcher(client pb.RuntimeConfigManagerClient, variableKey string, decoder *runtimevar.Decoder, opts *Options) (driver.Watcher, error) {
|
|
if opts == nil {
|
|
opts = &Options{}
|
|
}
|
|
if !variableKeyRE.MatchString(variableKey) {
|
|
return nil, fmt.Errorf("invalid variableKey %q; must match %v", variableKey, variableKeyRE)
|
|
}
|
|
return &watcher{
|
|
client: client,
|
|
wait: driver.WaitDuration(opts.WaitDuration),
|
|
name: variableKey,
|
|
decoder: decoder,
|
|
}, nil
|
|
}
|
|
|
|
// VariableKey constructs a GCP Runtime Configurator variable key from
|
|
// component parts. See
|
|
// https://cloud.google.com/deployment-manager/runtime-configurator/
|
|
// for more details.
|
|
func VariableKey(projectID gcp.ProjectID, configID, variableName string) string {
|
|
return fmt.Sprintf("projects/%s/configs/%s/variables/%s", projectID, configID, variableName)
|
|
}
|
|
|
|
// state implements driver.State.
|
|
type state struct {
|
|
val interface{}
|
|
raw *pb.Variable
|
|
updateTime time.Time
|
|
rawBytes []byte
|
|
err error
|
|
}
|
|
|
|
// Value implements driver.State.Value.
|
|
func (s *state) Value() (interface{}, error) {
|
|
return s.val, s.err
|
|
}
|
|
|
|
// UpdateTime implements driver.State.UpdateTime.
|
|
func (s *state) UpdateTime() time.Time {
|
|
return s.updateTime
|
|
}
|
|
|
|
// As implements driver.State.As.
|
|
func (s *state) As(i interface{}) bool {
|
|
if s.raw == nil {
|
|
return false
|
|
}
|
|
p, ok := i.(**pb.Variable)
|
|
if !ok {
|
|
return false
|
|
}
|
|
*p = s.raw
|
|
return true
|
|
}
|
|
|
|
// errorState returns a new State with err, unless prevS also represents
|
|
// the same error, in which case it returns nil.
|
|
func errorState(err error, prevS driver.State) driver.State {
|
|
s := &state{err: err}
|
|
if prevS == nil {
|
|
return s
|
|
}
|
|
prev := prevS.(*state)
|
|
if prev.err == nil {
|
|
// New error.
|
|
return s
|
|
}
|
|
if equivalentError(err, prev.err) {
|
|
// Same error, return nil to indicate no change.
|
|
return nil
|
|
}
|
|
return s
|
|
}
|
|
|
|
// equivalentError returns true iff err1 and err2 represent an equivalent error;
|
|
// i.e., we don't want to return it to the user as a different error.
|
|
func equivalentError(err1, err2 error) bool {
|
|
if err1 == err2 || err1.Error() == err2.Error() {
|
|
return true
|
|
}
|
|
code1, code2 := status.Code(err1), status.Code(err2)
|
|
return code1 != codes.OK && code1 != codes.Unknown && code1 == code2
|
|
}
|
|
|
|
// watcher implements driver.Watcher for configurations provided by the Runtime Configurator
|
|
// service.
|
|
type watcher struct {
|
|
client pb.RuntimeConfigManagerClient
|
|
wait time.Duration
|
|
name string
|
|
decoder *runtimevar.Decoder
|
|
}
|
|
|
|
// WatchVariable implements driver.WatchVariable.
|
|
func (w *watcher) WatchVariable(ctx context.Context, prev driver.State) (driver.State, time.Duration) {
|
|
// Get the variable from the backend.
|
|
vpb, err := w.client.GetVariable(ctx, &pb.GetVariableRequest{Name: w.name})
|
|
if err != nil {
|
|
return errorState(err, prev), w.wait
|
|
}
|
|
updateTime, err := parseUpdateTime(vpb)
|
|
if err != nil {
|
|
return errorState(err, prev), w.wait
|
|
}
|
|
// See if it's the same raw bytes as before.
|
|
b := bytesFromProto(vpb)
|
|
if prev != nil && bytes.Equal(b, prev.(*state).rawBytes) {
|
|
// No change!
|
|
return nil, w.wait
|
|
}
|
|
|
|
// Decode the value.
|
|
val, err := w.decoder.Decode(ctx, b)
|
|
if err != nil {
|
|
return errorState(err, prev), w.wait
|
|
}
|
|
return &state{val: val, raw: vpb, updateTime: updateTime, rawBytes: b}, w.wait
|
|
}
|
|
|
|
// Close implements driver.Close.
|
|
func (w *watcher) Close() error {
|
|
return nil
|
|
}
|
|
|
|
// ErrorAs implements driver.ErrorAs.
|
|
func (w *watcher) ErrorAs(err error, i interface{}) bool {
|
|
// FromError converts err to a *status.Status.
|
|
s, _ := status.FromError(err)
|
|
if p, ok := i.(**status.Status); ok {
|
|
*p = s
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ErrorCode implements driver.ErrorCode.
|
|
func (*watcher) ErrorCode(err error) gcerrors.ErrorCode {
|
|
return gcerr.GRPCCode(err)
|
|
}
|
|
|
|
func bytesFromProto(vpb *pb.Variable) []byte {
|
|
// Proto may contain either bytes or text. If it contains text content, convert that to []byte.
|
|
if _, isBytes := vpb.GetContents().(*pb.Variable_Value); isBytes {
|
|
return vpb.GetValue()
|
|
}
|
|
return []byte(vpb.GetText())
|
|
}
|
|
|
|
func parseUpdateTime(vpb *pb.Variable) (time.Time, error) {
|
|
return vpb.GetUpdateTime().AsTime(), nil
|
|
}
|