mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-16 19:47:02 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
378 lines
9.5 KiB
Go
378 lines
9.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package spanner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"cloud.google.com/go/spanner"
|
|
metrics "github.com/armon/go-metrics"
|
|
log "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-secure-stdlib/strutil"
|
|
"github.com/hashicorp/vault/helper/useragent"
|
|
"github.com/hashicorp/vault/sdk/physical"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/api/iterator"
|
|
"google.golang.org/api/option"
|
|
"google.golang.org/grpc/codes"
|
|
)
|
|
|
|
// Verify Backend satisfies the correct interfaces
|
|
var (
|
|
_ physical.Backend = (*Backend)(nil)
|
|
_ physical.Transactional = (*Backend)(nil)
|
|
)
|
|
|
|
const (
|
|
// envDatabase is the name of the environment variable to search for the
|
|
// database name.
|
|
envDatabase = "GOOGLE_SPANNER_DATABASE"
|
|
|
|
// envHAEnabled is the name of the environment variable to search for the
|
|
// boolean indicating if HA is enabled.
|
|
envHAEnabled = "GOOGLE_SPANNER_HA_ENABLED"
|
|
|
|
// envHATable is the name of the environment variable to search for the table
|
|
// name to use for HA.
|
|
envHATable = "GOOGLE_SPANNER_HA_TABLE"
|
|
|
|
// envTable is the name of the environment variable to search for the table
|
|
// name.
|
|
envTable = "GOOGLE_SPANNER_TABLE"
|
|
|
|
// defaultTable is the default table name if none is specified.
|
|
defaultTable = "Vault"
|
|
|
|
// defaultHASuffix is the default suffix to apply to the table name if no
|
|
// HA table is provided.
|
|
defaultHASuffix = "HA"
|
|
)
|
|
|
|
var (
|
|
// metricDelete is the key for the metric for measuring a Delete call.
|
|
metricDelete = []string{"spanner", "delete"}
|
|
|
|
// metricGet is the key for the metric for measuring a Get call.
|
|
metricGet = []string{"spanner", "get"}
|
|
|
|
// metricList is the key for the metric for measuring a List call.
|
|
metricList = []string{"spanner", "list"}
|
|
|
|
// metricPut is the key for the metric for measuring a Put call.
|
|
metricPut = []string{"spanner", "put"}
|
|
|
|
// metricTxn is the key for the metric for measuring a Transaction call.
|
|
metricTxn = []string{"spanner", "txn"}
|
|
)
|
|
|
|
// Backend implements physical.Backend and describes the steps necessary to
|
|
// persist data using Google Cloud Spanner.
|
|
type Backend struct {
|
|
// database is the name of the database to use for data storage and retrieval.
|
|
// This is supplied as part of user configuration.
|
|
database string
|
|
|
|
// table is the name of the table in the database.
|
|
table string
|
|
|
|
// client is the API client and permitPool is the allowed concurrent uses of
|
|
// the client.
|
|
client *spanner.Client
|
|
permitPool *physical.PermitPool
|
|
|
|
// haTable is the name of the table to use for HA in the database.
|
|
haTable string
|
|
|
|
// haEnabled indicates if high availability is enabled. Default: true.
|
|
haEnabled bool
|
|
|
|
// haClient is the API client. This is managed separately from the main client
|
|
// because a flood of requests should not block refreshing the TTLs on the
|
|
// lock.
|
|
//
|
|
// This value will be nil if haEnabled is false.
|
|
haClient *spanner.Client
|
|
|
|
// logger is the internal logger.
|
|
logger log.Logger
|
|
}
|
|
|
|
// NewBackend creates a new Google Spanner storage backend with the given
|
|
// configuration. This uses the official Golang Cloud SDK and therefore supports
|
|
// specifying credentials via envvars, credential files, etc.
|
|
func NewBackend(c map[string]string, logger log.Logger) (physical.Backend, error) {
|
|
logger.Debug("configuring backend")
|
|
|
|
// Database name
|
|
database := os.Getenv(envDatabase)
|
|
if database == "" {
|
|
database = c["database"]
|
|
}
|
|
if database == "" {
|
|
return nil, errors.New("missing database name")
|
|
}
|
|
|
|
// Table name
|
|
table := os.Getenv(envTable)
|
|
if table == "" {
|
|
table = c["table"]
|
|
}
|
|
if table == "" {
|
|
table = defaultTable
|
|
}
|
|
|
|
// HA table name
|
|
haTable := os.Getenv(envHATable)
|
|
if haTable == "" {
|
|
haTable = c["ha_table"]
|
|
}
|
|
if haTable == "" {
|
|
haTable = table + defaultHASuffix
|
|
}
|
|
|
|
// HA configuration
|
|
haClient := (*spanner.Client)(nil)
|
|
haEnabled := false
|
|
haEnabledStr := os.Getenv(envHAEnabled)
|
|
if haEnabledStr == "" {
|
|
haEnabledStr = c["ha_enabled"]
|
|
}
|
|
if haEnabledStr != "" {
|
|
var err error
|
|
haEnabled, err = strconv.ParseBool(haEnabledStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse HA enabled: %w", err)
|
|
}
|
|
}
|
|
if haEnabled {
|
|
logger.Debug("creating HA client")
|
|
var err error
|
|
ctx := context.Background()
|
|
haClient, err = spanner.NewClient(ctx, database,
|
|
option.WithUserAgent(useragent.String()),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create HA client: %w", err)
|
|
}
|
|
}
|
|
|
|
// Max parallel
|
|
maxParallel, err := extractInt(c["max_parallel"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse max_parallel: %w", err)
|
|
}
|
|
|
|
logger.Debug("configuration",
|
|
"database", database,
|
|
"table", table,
|
|
"haEnabled", haEnabled,
|
|
"haTable", haTable,
|
|
"maxParallel", maxParallel,
|
|
)
|
|
|
|
logger.Debug("creating client")
|
|
ctx := context.Background()
|
|
client, err := spanner.NewClient(ctx, database,
|
|
option.WithUserAgent(useragent.String()),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create spanner client: %w", err)
|
|
}
|
|
|
|
return &Backend{
|
|
database: database,
|
|
table: table,
|
|
client: client,
|
|
permitPool: physical.NewPermitPool(maxParallel),
|
|
|
|
haEnabled: haEnabled,
|
|
haTable: haTable,
|
|
haClient: haClient,
|
|
|
|
logger: logger,
|
|
}, nil
|
|
}
|
|
|
|
// Put creates or updates an entry.
|
|
func (b *Backend) Put(ctx context.Context, entry *physical.Entry) error {
|
|
defer metrics.MeasureSince(metricPut, time.Now())
|
|
|
|
// Pooling
|
|
b.permitPool.Acquire()
|
|
defer b.permitPool.Release()
|
|
|
|
// Insert
|
|
m := spanner.InsertOrUpdateMap(b.table, map[string]interface{}{
|
|
"Key": entry.Key,
|
|
"Value": entry.Value,
|
|
})
|
|
if _, err := b.client.Apply(ctx, []*spanner.Mutation{m}); err != nil {
|
|
return fmt.Errorf("failed to put data: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get fetches an entry. If there is no entry, this function returns nil.
|
|
func (b *Backend) Get(ctx context.Context, key string) (*physical.Entry, error) {
|
|
defer metrics.MeasureSince(metricGet, time.Now())
|
|
|
|
// Pooling
|
|
b.permitPool.Acquire()
|
|
defer b.permitPool.Release()
|
|
|
|
// Read
|
|
row, err := b.client.Single().ReadRow(ctx, b.table, spanner.Key{key}, []string{"Value"})
|
|
if spanner.ErrCode(err) == codes.NotFound {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read value for %q: %w", key, err)
|
|
}
|
|
|
|
var value []byte
|
|
if err := row.Column(0, &value); err != nil {
|
|
return nil, fmt.Errorf("failed to decode value into bytes: %w", err)
|
|
}
|
|
|
|
return &physical.Entry{
|
|
Key: key,
|
|
Value: value,
|
|
}, nil
|
|
}
|
|
|
|
// Delete deletes an entry with the given key.
|
|
func (b *Backend) Delete(ctx context.Context, key string) error {
|
|
defer metrics.MeasureSince(metricDelete, time.Now())
|
|
|
|
// Pooling
|
|
b.permitPool.Acquire()
|
|
defer b.permitPool.Release()
|
|
|
|
// Delete
|
|
m := spanner.Delete(b.table, spanner.Key{key})
|
|
if _, err := b.client.Apply(ctx, []*spanner.Mutation{m}); err != nil {
|
|
return fmt.Errorf("failed to delete key: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// List enumerates all keys with the given prefix.
|
|
func (b *Backend) List(ctx context.Context, prefix string) ([]string, error) {
|
|
defer metrics.MeasureSince(metricList, time.Now())
|
|
|
|
// Pooling
|
|
b.permitPool.Acquire()
|
|
defer b.permitPool.Release()
|
|
|
|
// Sanitize
|
|
safeTable := sanitizeTable(b.table)
|
|
|
|
// List
|
|
iter := b.client.Single().Query(ctx, spanner.Statement{
|
|
SQL: "SELECT Key FROM " + safeTable + " WHERE STARTS_WITH(Key, @prefix)",
|
|
Params: map[string]interface{}{
|
|
"prefix": prefix,
|
|
},
|
|
})
|
|
defer iter.Stop()
|
|
|
|
var keys []string
|
|
|
|
for {
|
|
row, err := iter.Next()
|
|
if err == iterator.Done {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read row: %w", err)
|
|
}
|
|
|
|
var key string
|
|
if err := row.Column(0, &key); err != nil {
|
|
return nil, fmt.Errorf("failed to decode key into string: %w", err)
|
|
}
|
|
|
|
// The results will include the full prefix (folder) and any deeply-nested
|
|
// prefixes (subfolders). Vault expects only the top-most things to be
|
|
// included.
|
|
key = strings.TrimPrefix(key, prefix)
|
|
if i := strings.Index(key, "/"); i == -1 {
|
|
// Add objects only from the current 'folder'
|
|
keys = append(keys, key)
|
|
} else {
|
|
// Add truncated 'folder' paths
|
|
keys = strutil.AppendIfMissing(keys, string(key[:i+1]))
|
|
}
|
|
}
|
|
|
|
// Sort because the resulting order is not predictable
|
|
sort.Strings(keys)
|
|
|
|
return keys, nil
|
|
}
|
|
|
|
// Transaction runs multiple entries via a single transaction.
|
|
func (b *Backend) Transaction(ctx context.Context, txns []*physical.TxnEntry) error {
|
|
defer metrics.MeasureSince(metricTxn, time.Now())
|
|
|
|
// Quit early if we can
|
|
if len(txns) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Build all the ops before taking out the pool
|
|
ms := make([]*spanner.Mutation, len(txns))
|
|
for i, tx := range txns {
|
|
op, key, value := tx.Operation, tx.Entry.Key, tx.Entry.Value
|
|
|
|
switch op {
|
|
case physical.DeleteOperation:
|
|
ms[i] = spanner.Delete(b.table, spanner.Key{key})
|
|
case physical.PutOperation:
|
|
ms[i] = spanner.InsertOrUpdateMap(b.table, map[string]interface{}{
|
|
"Key": key,
|
|
"Value": value,
|
|
})
|
|
default:
|
|
return fmt.Errorf("unsupported transaction operation: %q", op)
|
|
}
|
|
}
|
|
|
|
// Pooling
|
|
b.permitPool.Acquire()
|
|
defer b.permitPool.Release()
|
|
|
|
// Transactivate!
|
|
if _, err := b.client.Apply(ctx, ms); err != nil {
|
|
return fmt.Errorf("failed to commit transaction: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractInt is a helper function that takes a string and converts that string
|
|
// to an int, but accounts for the empty string.
|
|
func extractInt(s string) (int, error) {
|
|
if s == "" {
|
|
return 0, nil
|
|
}
|
|
return strconv.Atoi(s)
|
|
}
|
|
|
|
// sanitizeTable attempts to sanitize the table name.
|
|
func sanitizeTable(s string) string {
|
|
end := strings.IndexRune(s, 0)
|
|
if end > -1 {
|
|
s = s[:end]
|
|
}
|
|
return strings.ReplaceAll(s, `"`, `""`)
|
|
}
|