remove consul
serviceregistration/consul is preserved
This commit is contained in:
parent
747df1e9e8
commit
2acf0a21fd
|
@ -1,54 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const operationPrefixConsul = "consul"
|
||||
|
||||
// ReportedVersion is used to report a specific version to Vault.
|
||||
var ReportedVersion = ""
|
||||
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := Backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Backend() *backend {
|
||||
var b backend
|
||||
b.Backend = &framework.Backend{
|
||||
PathsSpecial: &logical.Paths{
|
||||
SealWrapStorage: []string{
|
||||
"config/access",
|
||||
},
|
||||
},
|
||||
|
||||
Paths: []*framework.Path{
|
||||
pathConfigAccess(&b),
|
||||
pathListRoles(&b),
|
||||
pathRoles(&b),
|
||||
pathToken(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
secretToken(&b),
|
||||
},
|
||||
BackendType: logical.TypeLogical,
|
||||
RunningVersion: ReportedVersion,
|
||||
}
|
||||
|
||||
return &b
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
*framework.Backend
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,29 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error, error) {
|
||||
conf, userErr, intErr := b.readConfigAccess(ctx, s)
|
||||
if intErr != nil {
|
||||
return nil, nil, intErr
|
||||
}
|
||||
if userErr != nil {
|
||||
return nil, userErr, nil
|
||||
}
|
||||
if conf == nil {
|
||||
return nil, nil, fmt.Errorf("no error received but no configuration found")
|
||||
}
|
||||
|
||||
consulConf := conf.NewConfig()
|
||||
client, err := api.NewClient(consulConf)
|
||||
return client, nil, err
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/builtin/logical/consul"
|
||||
"github.com/hashicorp/vault/sdk/plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClientMeta := &api.PluginAPIClientMeta{}
|
||||
flags := apiClientMeta.FlagSet()
|
||||
flags.Parse(os.Args[1:])
|
||||
|
||||
tlsConfig := apiClientMeta.GetTLSConfig()
|
||||
tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)
|
||||
|
||||
if err := plugin.ServeMultiplex(&plugin.ServeOpts{
|
||||
BackendFactoryFunc: consul.Factory,
|
||||
// set the TLSProviderFunc so that the plugin maintains backwards
|
||||
// compatibility with Vault versions that don’t support plugin AutoMTLS
|
||||
TLSProviderFunc: tlsProviderFunc,
|
||||
}); err != nil {
|
||||
logger := hclog.New(&hclog.LoggerOptions{})
|
||||
|
||||
logger.Error("plugin shutting down", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -1,174 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathConfigAccess(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/access",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixConsul,
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"address": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Consul server address",
|
||||
},
|
||||
|
||||
"scheme": {
|
||||
Type: framework.TypeString,
|
||||
Description: "URI scheme for the Consul address",
|
||||
|
||||
// https would be a better default but Consul on its own
|
||||
// defaults to HTTP access, and when HTTPS is enabled it
|
||||
// disables HTTP, so there isn't really any harm done here.
|
||||
Default: "http",
|
||||
},
|
||||
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Token for API calls",
|
||||
},
|
||||
|
||||
"ca_cert": {
|
||||
Type: framework.TypeString,
|
||||
Description: `CA certificate to use when verifying Consul server certificate,
|
||||
must be x509 PEM encoded.`,
|
||||
},
|
||||
|
||||
"client_cert": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Client certificate used for Consul's TLS communication,
|
||||
must be x509 PEM encoded and if this is set you need to also set client_key.`,
|
||||
},
|
||||
|
||||
"client_key": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Client key used for Consul's TLS communication,
|
||||
must be x509 PEM encoded and if this is set you need to also set client_cert.`,
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigAccessRead,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationSuffix: "access-configuration",
|
||||
},
|
||||
},
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.pathConfigAccessWrite,
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationVerb: "configure",
|
||||
OperationSuffix: "access",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) readConfigAccess(ctx context.Context, storage logical.Storage) (*accessConfig, error, error) {
|
||||
entry, err := storage.Get(ctx, "config/access")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, fmt.Errorf("access credentials for the backend itself haven't been configured; please configure them at the '/config/access' endpoint"), nil
|
||||
}
|
||||
|
||||
conf := &accessConfig{}
|
||||
if err := entry.DecodeJSON(conf); err != nil {
|
||||
return nil, nil, fmt.Errorf("error reading consul access configuration: %w", err)
|
||||
}
|
||||
|
||||
return conf, nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathConfigAccessRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
conf, userErr, intErr := b.readConfigAccess(ctx, req.Storage)
|
||||
if intErr != nil {
|
||||
return nil, intErr
|
||||
}
|
||||
if userErr != nil {
|
||||
return logical.ErrorResponse(userErr.Error()), nil
|
||||
}
|
||||
if conf == nil {
|
||||
return nil, fmt.Errorf("no user error reported but consul access configuration not found")
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"address": conf.Address,
|
||||
"scheme": conf.Scheme,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
config := accessConfig{
|
||||
Address: data.Get("address").(string),
|
||||
Scheme: data.Get("scheme").(string),
|
||||
Token: data.Get("token").(string),
|
||||
CACert: data.Get("ca_cert").(string),
|
||||
ClientCert: data.Get("client_cert").(string),
|
||||
ClientKey: data.Get("client_key").(string),
|
||||
}
|
||||
|
||||
// If a token has not been given by the user, we try to boostrap the ACL
|
||||
// support
|
||||
if config.Token == "" {
|
||||
consulConf := config.NewConfig()
|
||||
client, err := api.NewClient(consulConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, _, err := client.ACL().Bootstrap()
|
||||
if err != nil {
|
||||
return logical.ErrorResponse("Token not provided and failed to bootstrap ACLs: %s", err), nil
|
||||
}
|
||||
config.Token = token.SecretID
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config/access", config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type accessConfig struct {
|
||||
Address string `json:"address"`
|
||||
Scheme string `json:"scheme"`
|
||||
Token string `json:"token"`
|
||||
CACert string `json:"ca_cert"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
}
|
||||
|
||||
func (conf *accessConfig) NewConfig() *api.Config {
|
||||
consulConf := api.DefaultNonPooledConfig()
|
||||
consulConf.Address = conf.Address
|
||||
consulConf.Scheme = conf.Scheme
|
||||
consulConf.Token = conf.Token
|
||||
consulConf.TLSConfig.CAPem = []byte(conf.CACert)
|
||||
consulConf.TLSConfig.CertPEM = []byte(conf.ClientCert)
|
||||
consulConf.TLSConfig.KeyPEM = []byte(conf.ClientKey)
|
||||
|
||||
return consulConf
|
||||
}
|
|
@ -1,294 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathListRoles(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/?$",
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixConsul,
|
||||
OperationSuffix: "roles",
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ListOperation: b.pathRoleList,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pathRoles(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixConsul,
|
||||
OperationSuffix: "role",
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the role.",
|
||||
},
|
||||
|
||||
// The "policy" and "token_type" parameters were deprecated in Consul back in version 1.4.
|
||||
// They have been removed from Consul as of version 1.11. Consider removing them here in the future.
|
||||
"policy": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Policy document, base64 encoded. Required
|
||||
for 'client' tokens. Required for Consul pre-1.4.`,
|
||||
Deprecated: true,
|
||||
},
|
||||
|
||||
"token_type": {
|
||||
Type: framework.TypeString,
|
||||
Default: "client",
|
||||
Description: `Which type of token to create: 'client' or 'management'. If
|
||||
a 'management' token, the "policy", "policies", and "consul_roles" parameters are not
|
||||
required. Defaults to 'client'.`,
|
||||
Deprecated: true,
|
||||
},
|
||||
|
||||
"policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `Use "consul_policies" instead.`,
|
||||
Deprecated: true,
|
||||
},
|
||||
|
||||
"consul_policies": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `List of policies to attach to the token. Either "consul_policies"
|
||||
or "consul_roles" are required for Consul 1.5 and above, or just "consul_policies" if
|
||||
using Consul 1.4.`,
|
||||
},
|
||||
|
||||
"consul_roles": {
|
||||
Type: framework.TypeCommaStringSlice,
|
||||
Description: `List of Consul roles to attach to the token. Either "policies"
|
||||
or "consul_roles" are required for Consul 1.5 and above.`,
|
||||
},
|
||||
|
||||
"local": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `Indicates that the token should not be replicated globally
|
||||
and instead be local to the current datacenter. Available in Consul 1.4 and above.`,
|
||||
},
|
||||
|
||||
"ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "TTL for the Consul token created from the role.",
|
||||
},
|
||||
|
||||
"max_ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "Max TTL for the Consul token created from the role.",
|
||||
},
|
||||
|
||||
"lease": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: `Use "ttl" instead.`,
|
||||
Deprecated: true,
|
||||
},
|
||||
|
||||
"consul_namespace": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Indicates which namespace that the token will be
|
||||
created within. Defaults to 'default'. Available in Consul 1.7 and above.`,
|
||||
},
|
||||
|
||||
"partition": {
|
||||
Type: framework.TypeString,
|
||||
Description: `Indicates which admin partition that the token
|
||||
will be created within. Defaults to 'default'. Available in Consul 1.11 and above.`,
|
||||
},
|
||||
|
||||
"service_identities": {
|
||||
Type: framework.TypeStringSlice,
|
||||
Description: `List of Service Identities to attach to the
|
||||
token, separated by semicolons. Available in Consul 1.5 or above.`,
|
||||
},
|
||||
|
||||
"node_identities": {
|
||||
Type: framework.TypeStringSlice,
|
||||
Description: `List of Node Identities to attach to the
|
||||
token. Available in Consul 1.8.1 or above.`,
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathRolesRead,
|
||||
logical.UpdateOperation: b.pathRolesWrite,
|
||||
logical.DeleteOperation: b.pathRolesDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
entries, err := req.Storage.List(ctx, "policy/")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return logical.ListResponse(entries), nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
|
||||
entry, err := req.Storage.Get(ctx, "policy/"+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var roleConfigData roleConfig
|
||||
if err := entry.DecodeJSON(&roleConfigData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if roleConfigData.TokenType == "" {
|
||||
roleConfigData.TokenType = "client"
|
||||
}
|
||||
|
||||
// Generate the response
|
||||
resp := &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"lease": int64(roleConfigData.TTL.Seconds()),
|
||||
"ttl": int64(roleConfigData.TTL.Seconds()),
|
||||
"max_ttl": int64(roleConfigData.MaxTTL.Seconds()),
|
||||
"token_type": roleConfigData.TokenType,
|
||||
"local": roleConfigData.Local,
|
||||
"consul_namespace": roleConfigData.ConsulNamespace,
|
||||
"partition": roleConfigData.Partition,
|
||||
},
|
||||
}
|
||||
if roleConfigData.Policy != "" {
|
||||
resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(roleConfigData.Policy))
|
||||
}
|
||||
if len(roleConfigData.Policies) > 0 {
|
||||
resp.Data["consul_policies"] = roleConfigData.Policies
|
||||
}
|
||||
if len(roleConfigData.ConsulRoles) > 0 {
|
||||
resp.Data["consul_roles"] = roleConfigData.ConsulRoles
|
||||
}
|
||||
if len(roleConfigData.ServiceIdentities) > 0 {
|
||||
resp.Data["service_identities"] = roleConfigData.ServiceIdentities
|
||||
}
|
||||
if len(roleConfigData.NodeIdentities) > 0 {
|
||||
resp.Data["node_identities"] = roleConfigData.NodeIdentities
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
tokenType := d.Get("token_type").(string)
|
||||
policy := d.Get("policy").(string)
|
||||
consulPolicies := d.Get("consul_policies").([]string)
|
||||
policies := d.Get("policies").([]string)
|
||||
roles := d.Get("consul_roles").([]string)
|
||||
serviceIdentities := d.Get("service_identities").([]string)
|
||||
nodeIdentities := d.Get("node_identities").([]string)
|
||||
|
||||
switch tokenType {
|
||||
case "client":
|
||||
if policy == "" && len(policies) == 0 && len(consulPolicies) == 0 &&
|
||||
len(roles) == 0 && len(serviceIdentities) == 0 && len(nodeIdentities) == 0 {
|
||||
return logical.ErrorResponse(
|
||||
"Use either a policy document, a list of policies or roles, or a set of service or node identities, depending on your Consul version"), nil
|
||||
}
|
||||
case "management":
|
||||
default:
|
||||
return logical.ErrorResponse("token_type must be \"client\" or \"management\""), nil
|
||||
}
|
||||
|
||||
if len(consulPolicies) == 0 {
|
||||
consulPolicies = policies
|
||||
}
|
||||
|
||||
policyRaw, err := base64.StdEncoding.DecodeString(policy)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf(
|
||||
"Error decoding policy base64: %s", err)), nil
|
||||
}
|
||||
|
||||
var ttl time.Duration
|
||||
ttlRaw, ok := d.GetOk("ttl")
|
||||
if ok {
|
||||
ttl = time.Second * time.Duration(ttlRaw.(int))
|
||||
} else {
|
||||
leaseParamRaw, ok := d.GetOk("lease")
|
||||
if ok {
|
||||
ttl = time.Second * time.Duration(leaseParamRaw.(int))
|
||||
}
|
||||
}
|
||||
|
||||
var maxTTL time.Duration
|
||||
maxTTLRaw, ok := d.GetOk("max_ttl")
|
||||
if ok {
|
||||
maxTTL = time.Second * time.Duration(maxTTLRaw.(int))
|
||||
}
|
||||
|
||||
name := d.Get("name").(string)
|
||||
local := d.Get("local").(bool)
|
||||
namespace := d.Get("consul_namespace").(string)
|
||||
partition := d.Get("partition").(string)
|
||||
entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{
|
||||
Policy: string(policyRaw),
|
||||
Policies: consulPolicies,
|
||||
ConsulRoles: roles,
|
||||
ServiceIdentities: serviceIdentities,
|
||||
NodeIdentities: nodeIdentities,
|
||||
TokenType: tokenType,
|
||||
TTL: ttl,
|
||||
MaxTTL: maxTTL,
|
||||
Local: local,
|
||||
ConsulNamespace: namespace,
|
||||
Partition: partition,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := req.Storage.Put(ctx, entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
name := d.Get("name").(string)
|
||||
if err := req.Storage.Delete(ctx, "policy/"+name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type roleConfig struct {
|
||||
Policy string `json:"policy"`
|
||||
Policies []string `json:"policies"`
|
||||
ConsulRoles []string `json:"consul_roles"`
|
||||
ServiceIdentities []string `json:"service_identities"`
|
||||
NodeIdentities []string `json:"node_identities"`
|
||||
TTL time.Duration `json:"lease"`
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
TokenType string `json:"token_type"`
|
||||
Local bool `json:"local"`
|
||||
ConsulNamespace string `json:"consul_namespace"`
|
||||
Partition string `json:"partition"`
|
||||
}
|
|
@ -1,182 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenPolicyType = "token"
|
||||
)
|
||||
|
||||
func pathToken(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "creds/" + framework.GenericNameRegex("role"),
|
||||
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: operationPrefixConsul,
|
||||
OperationVerb: "generate",
|
||||
OperationSuffix: "credentials",
|
||||
},
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"role": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Name of the role.",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.pathTokenRead,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
role := d.Get("role").(string)
|
||||
entry, err := req.Storage.Get(ctx, "policy/"+role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving role: %w", err)
|
||||
}
|
||||
if entry == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("role %q not found", role)), nil
|
||||
}
|
||||
|
||||
var roleConfigData roleConfig
|
||||
if err := entry.DecodeJSON(&roleConfigData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if roleConfigData.TokenType == "" {
|
||||
roleConfigData.TokenType = "client"
|
||||
}
|
||||
|
||||
// Get the consul client
|
||||
c, userErr, intErr := b.client(ctx, req.Storage)
|
||||
if intErr != nil {
|
||||
return nil, intErr
|
||||
}
|
||||
if userErr != nil {
|
||||
return logical.ErrorResponse(userErr.Error()), nil
|
||||
}
|
||||
|
||||
// Generate a name for the token
|
||||
tokenName := fmt.Sprintf("Vault %s %s %d", role, req.DisplayName, time.Now().UnixNano())
|
||||
|
||||
writeOpts := &api.WriteOptions{}
|
||||
writeOpts = writeOpts.WithContext(ctx)
|
||||
|
||||
// Create an ACLEntry for Consul pre 1.4
|
||||
if (roleConfigData.Policy != "" && roleConfigData.TokenType == "client") ||
|
||||
(roleConfigData.Policy == "" && roleConfigData.TokenType == "management") {
|
||||
token, _, err := c.ACL().Create(&api.ACLEntry{
|
||||
Name: tokenName,
|
||||
Type: roleConfigData.TokenType,
|
||||
Rules: roleConfigData.Policy,
|
||||
}, writeOpts)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// Use the helper to create the secret
|
||||
s := b.Secret(SecretTokenType).Response(map[string]interface{}{
|
||||
"token": token,
|
||||
}, map[string]interface{}{
|
||||
"token": token,
|
||||
"role": role,
|
||||
})
|
||||
s.Secret.TTL = roleConfigData.TTL
|
||||
s.Secret.MaxTTL = roleConfigData.MaxTTL
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Create an ACLToken for Consul 1.4 and above
|
||||
policyLinks := []*api.ACLTokenPolicyLink{}
|
||||
for _, policyName := range roleConfigData.Policies {
|
||||
policyLinks = append(policyLinks, &api.ACLTokenPolicyLink{
|
||||
Name: policyName,
|
||||
})
|
||||
}
|
||||
|
||||
roleLinks := []*api.ACLTokenRoleLink{}
|
||||
for _, roleName := range roleConfigData.ConsulRoles {
|
||||
roleLinks = append(roleLinks, &api.ACLTokenRoleLink{
|
||||
Name: roleName,
|
||||
})
|
||||
}
|
||||
|
||||
aclServiceIdentities := parseServiceIdentities(roleConfigData.ServiceIdentities)
|
||||
aclNodeIdentities := parseNodeIdentities(roleConfigData.NodeIdentities)
|
||||
|
||||
token, _, err := c.ACL().TokenCreate(&api.ACLToken{
|
||||
Description: tokenName,
|
||||
Policies: policyLinks,
|
||||
Roles: roleLinks,
|
||||
ServiceIdentities: aclServiceIdentities,
|
||||
NodeIdentities: aclNodeIdentities,
|
||||
Local: roleConfigData.Local,
|
||||
Namespace: roleConfigData.ConsulNamespace,
|
||||
Partition: roleConfigData.Partition,
|
||||
}, writeOpts)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// Use the helper to create the secret
|
||||
s := b.Secret(SecretTokenType).Response(map[string]interface{}{
|
||||
"token": token.SecretID,
|
||||
"accessor": token.AccessorID,
|
||||
"local": token.Local,
|
||||
"consul_namespace": token.Namespace,
|
||||
"partition": token.Partition,
|
||||
}, map[string]interface{}{
|
||||
"token": token.AccessorID,
|
||||
"role": role,
|
||||
"version": tokenPolicyType,
|
||||
})
|
||||
s.Secret.TTL = roleConfigData.TTL
|
||||
s.Secret.MaxTTL = roleConfigData.MaxTTL
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func parseServiceIdentities(data []string) []*api.ACLServiceIdentity {
|
||||
aclServiceIdentities := []*api.ACLServiceIdentity{}
|
||||
|
||||
for _, serviceIdentity := range data {
|
||||
entry := &api.ACLServiceIdentity{}
|
||||
components := strings.Split(serviceIdentity, ":")
|
||||
entry.ServiceName = components[0]
|
||||
if len(components) == 2 {
|
||||
entry.Datacenters = strings.Split(components[1], ",")
|
||||
}
|
||||
aclServiceIdentities = append(aclServiceIdentities, entry)
|
||||
}
|
||||
|
||||
return aclServiceIdentities
|
||||
}
|
||||
|
||||
func parseNodeIdentities(data []string) []*api.ACLNodeIdentity {
|
||||
aclNodeIdentities := []*api.ACLNodeIdentity{}
|
||||
|
||||
for _, nodeIdentity := range data {
|
||||
entry := &api.ACLNodeIdentity{}
|
||||
components := strings.Split(nodeIdentity, ":")
|
||||
entry.NodeName = components[0]
|
||||
if len(components) > 1 {
|
||||
entry.Datacenter = components[1]
|
||||
}
|
||||
aclNodeIdentities = append(aclNodeIdentities, entry)
|
||||
}
|
||||
|
||||
return aclNodeIdentities
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func TestToken_parseServiceIdentities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []*api.ACLServiceIdentity
|
||||
}{
|
||||
{
|
||||
name: "No datacenters",
|
||||
args: []string{"myservice-1"},
|
||||
want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: nil}},
|
||||
},
|
||||
{
|
||||
name: "One datacenter",
|
||||
args: []string{"myservice-1:dc1"},
|
||||
want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1"}}},
|
||||
},
|
||||
{
|
||||
name: "Multiple datacenters",
|
||||
args: []string{"myservice-1:dc1,dc2,dc3"},
|
||||
want: []*api.ACLServiceIdentity{{ServiceName: "myservice-1", Datacenters: []string{"dc1", "dc2", "dc3"}}},
|
||||
},
|
||||
{
|
||||
name: "Missing service name with datacenter",
|
||||
args: []string{":dc1"},
|
||||
want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: []string{"dc1"}}},
|
||||
},
|
||||
{
|
||||
name: "Missing service name and missing datacenter",
|
||||
args: []string{""},
|
||||
want: []*api.ACLServiceIdentity{{ServiceName: "", Datacenters: nil}},
|
||||
},
|
||||
{
|
||||
name: "Multiple service identities",
|
||||
args: []string{"myservice-1:dc1", "myservice-2:dc1", "myservice-3:dc1,dc2"},
|
||||
want: []*api.ACLServiceIdentity{
|
||||
{ServiceName: "myservice-1", Datacenters: []string{"dc1"}},
|
||||
{ServiceName: "myservice-2", Datacenters: []string{"dc1"}},
|
||||
{ServiceName: "myservice-3", Datacenters: []string{"dc1", "dc2"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseServiceIdentities(tt.args); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseServiceIdentities() = {%s:%v}, want {%s:%v}", got[0].ServiceName, got[0].Datacenters, tt.want[0].ServiceName, tt.want[0].Datacenters)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToken_parseNodeIdentities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want []*api.ACLNodeIdentity
|
||||
}{
|
||||
{
|
||||
name: "No datacenter",
|
||||
args: []string{"server-1"},
|
||||
want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: ""}},
|
||||
},
|
||||
{
|
||||
name: "One datacenter",
|
||||
args: []string{"server-1:dc1"},
|
||||
want: []*api.ACLNodeIdentity{{NodeName: "server-1", Datacenter: "dc1"}},
|
||||
},
|
||||
{
|
||||
name: "Missing node name with datacenter",
|
||||
args: []string{":dc1"},
|
||||
want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: "dc1"}},
|
||||
},
|
||||
{
|
||||
name: "Missing node name and missing datacenter",
|
||||
args: []string{""},
|
||||
want: []*api.ACLNodeIdentity{{NodeName: "", Datacenter: ""}},
|
||||
},
|
||||
{
|
||||
name: "Multiple node identities",
|
||||
args: []string{"server-1:dc1", "server-2:dc1", "server-3:dc1"},
|
||||
want: []*api.ACLNodeIdentity{
|
||||
{NodeName: "server-1", Datacenter: "dc1"},
|
||||
{NodeName: "server-2", Datacenter: "dc1"},
|
||||
{NodeName: "server-3", Datacenter: "dc1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := parseNodeIdentities(tt.args); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseNodeIdentities() = {%s:%s}, want {%s:%s}", got[0].NodeName, got[0].Datacenter, tt.want[0].NodeName, tt.want[0].Datacenter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
SecretTokenType = "token"
|
||||
)
|
||||
|
||||
func secretToken(b *backend) *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: SecretTokenType,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Request token",
|
||||
},
|
||||
},
|
||||
|
||||
Renew: b.secretTokenRenew,
|
||||
Revoke: b.secretTokenRevoke,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) secretTokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
resp := &logical.Response{Secret: req.Secret}
|
||||
roleRaw, ok := req.Secret.InternalData["role"]
|
||||
if !ok {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
role, ok := roleRaw.(string)
|
||||
if !ok {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
entry, err := req.Storage.Get(ctx, "policy/"+role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving role: %w", err)
|
||||
}
|
||||
if entry == nil {
|
||||
return logical.ErrorResponse(fmt.Sprintf("issuing role %q not found", role)), nil
|
||||
}
|
||||
|
||||
var result roleConfig
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Secret.TTL = result.TTL
|
||||
resp.Secret.MaxTTL = result.MaxTTL
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *backend) secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
c, userErr, intErr := b.client(ctx, req.Storage)
|
||||
if intErr != nil {
|
||||
return nil, intErr
|
||||
}
|
||||
if userErr != nil {
|
||||
// Returning logical.ErrorResponse from revocation function is risky
|
||||
return nil, userErr
|
||||
}
|
||||
|
||||
tokenRaw, ok := req.Secret.InternalData["token"]
|
||||
if !ok {
|
||||
// We return nil here because this is a pre-0.5.3 problem and there is
|
||||
// nothing we can do about it. We already can't revoke the lease
|
||||
// properly if it has been renewed and this is documented pre-0.5.3
|
||||
// behavior with a security bulletin about it.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var version string
|
||||
versionRaw, ok := req.Secret.InternalData["version"]
|
||||
if ok {
|
||||
version = versionRaw.(string)
|
||||
}
|
||||
|
||||
// Extract Consul Namespace and Partition info from secret
|
||||
var revokeWriteOptions *api.WriteOptions
|
||||
var namespace, partition string
|
||||
|
||||
namespaceRaw, ok := req.Data["consul_namespace"]
|
||||
if ok {
|
||||
namespace = namespaceRaw.(string)
|
||||
}
|
||||
partitionRaw, ok := req.Data["partition"]
|
||||
if ok {
|
||||
partition = partitionRaw.(string)
|
||||
}
|
||||
|
||||
revokeWriteOptions = &api.WriteOptions{
|
||||
Namespace: namespace,
|
||||
Partition: partition,
|
||||
}
|
||||
|
||||
switch version {
|
||||
case "":
|
||||
// Pre 1.4 tokens
|
||||
_, err := c.ACL().Destroy(tokenRaw.(string), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case tokenPolicyType:
|
||||
_, err := c.ACL().TokenDelete(tokenRaw.(string), revokeWriteOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid version string in data: %s", version)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
|
@ -85,7 +85,6 @@ func (b *BaseCommand) PredictVaultAvailableMounts() complete.Predictor {
|
|||
// This list does not contain deprecated backends. At present, there is no
|
||||
// API that lists all available secret backends, so this is hard-coded :(.
|
||||
return complete.PredictSet(
|
||||
"consul",
|
||||
"database",
|
||||
"generic",
|
||||
"pki",
|
||||
|
|
|
@ -40,7 +40,6 @@ import (
|
|||
logicalKv "github.com/hashicorp/vault-plugin-secrets-kv"
|
||||
logicalDb "github.com/hashicorp/vault/builtin/logical/database"
|
||||
|
||||
physConsul "github.com/hashicorp/vault/physical/consul"
|
||||
physRaft "github.com/hashicorp/vault/physical/raft"
|
||||
physFile "github.com/hashicorp/vault/sdk/physical/file"
|
||||
physInmem "github.com/hashicorp/vault/sdk/physical/inmem"
|
||||
|
@ -162,7 +161,6 @@ var (
|
|||
}
|
||||
|
||||
physicalBackends = map[string]physical.Factory{
|
||||
"consul": physConsul.NewConsulBackend,
|
||||
"file_transactional": physFile.NewTransactionalFileBackend,
|
||||
"file": physFile.NewFileBackend,
|
||||
"inmem_ha": physInmem.NewInmemHA,
|
||||
|
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/hashicorp/vault/helper/metricsutil"
|
||||
"github.com/hashicorp/vault/internalshared/configutil"
|
||||
"github.com/hashicorp/vault/internalshared/listenerutil"
|
||||
physconsul "github.com/hashicorp/vault/physical/consul"
|
||||
"github.com/hashicorp/vault/physical/raft"
|
||||
"github.com/hashicorp/vault/sdk/physical"
|
||||
sr "github.com/hashicorp/vault/serviceregistration"
|
||||
|
@ -317,28 +316,6 @@ func (c *OperatorDiagnoseCommand) offlineDiagnostics(ctx context.Context) error
|
|||
diagnose.RaftStorageQuorum(ctx, (*backend).(*raft.RaftBackend))
|
||||
}
|
||||
|
||||
// Consul storage checks
|
||||
if config.Storage != nil && config.Storage.Type == storageTypeConsul {
|
||||
diagnose.Test(ctx, "Check Consul TLS", func(ctx context.Context) error {
|
||||
err := physconsul.SetupSecureTLS(ctx, api.DefaultConfig(), config.Storage.Config, server.logger, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
diagnose.Test(ctx, "Check Consul Direct Storage Access", func(ctx context.Context) error {
|
||||
dirAccess := diagnose.ConsulDirectAccess(config.Storage.Config)
|
||||
if dirAccess != "" {
|
||||
diagnose.Warn(ctx, dirAccess)
|
||||
}
|
||||
if dirAccess == diagnose.DirAccessErr {
|
||||
diagnose.Advise(ctx, diagnose.DirAccessAdvice)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Attempt to use storage backend
|
||||
if !c.skipEndEnd && config.Storage.Type != storageTypeRaft {
|
||||
diagnose.Test(ctx, "Check Storage Access", diagnose.WithTimeout(30*time.Second, func(ctx context.Context) error {
|
||||
|
@ -552,15 +529,6 @@ SEALFAIL:
|
|||
}
|
||||
return nil
|
||||
})
|
||||
if config.HAStorage != nil && config.HAStorage.Type == storageTypeConsul {
|
||||
diagnose.Test(ctx, "Check Consul TLS", func(ctx context.Context) error {
|
||||
err = physconsul.SetupSecureTLS(ctx, api.DefaultConfig(), config.HAStorage.Config, server.logger, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
|
|
|
@ -86,8 +86,7 @@ const (
|
|||
|
||||
// Even though there are more types than the ones below, the following consts
|
||||
// are declared internally for value comparison and reusability.
|
||||
storageTypeRaft = "raft"
|
||||
storageTypeConsul = "consul"
|
||||
storageTypeRaft = "raft"
|
||||
)
|
||||
|
||||
type ServerCommand struct {
|
||||
|
@ -145,7 +144,6 @@ type ServerCommand struct {
|
|||
flagDevClusterJson string
|
||||
flagTestVerifyOnly bool
|
||||
flagTestServerConfig bool
|
||||
flagDevConsul bool
|
||||
flagExitOnCoreShutdown bool
|
||||
}
|
||||
|
||||
|
@ -367,13 +365,6 @@ func (c *ServerCommand) Flags() *FlagSets {
|
|||
Hidden: true,
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: "dev-consul",
|
||||
Target: &c.flagDevConsul,
|
||||
Default: false,
|
||||
Hidden: true,
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: "dev-cluster-json",
|
||||
Target: &c.flagDevClusterJson,
|
||||
|
@ -789,16 +780,6 @@ func (c *ServerCommand) setupStorage(config *server.Config) (physical.Backend, e
|
|||
|
||||
// Do any custom configuration needed per backend
|
||||
switch config.Storage.Type {
|
||||
case storageTypeConsul:
|
||||
if config.ServiceRegistration == nil {
|
||||
// If Consul is configured for storage and service registration is unconfigured,
|
||||
// use Consul for service registration without requiring additional configuration.
|
||||
// This maintains backward-compatibility.
|
||||
config.ServiceRegistration = &server.ServiceRegistration{
|
||||
Type: "consul",
|
||||
Config: config.Storage.Config,
|
||||
}
|
||||
}
|
||||
case storageTypeRaft:
|
||||
if envCA := os.Getenv("VAULT_CLUSTER_ADDR"); envCA != "" {
|
||||
config.ClusterAddr = envCA
|
||||
|
@ -935,8 +916,6 @@ func configureDevTLS(c *ServerCommand) (func(), *server.Config, string, error) {
|
|||
var devStorageType string
|
||||
|
||||
switch {
|
||||
case c.flagDevConsul:
|
||||
devStorageType = "consul"
|
||||
case c.flagDevHA && c.flagDevTransactional:
|
||||
devStorageType = "inmem_transactional_ha"
|
||||
case !c.flagDevHA && c.flagDevTransactional:
|
||||
|
@ -1017,7 +996,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
}
|
||||
|
||||
// Automatically enable dev mode if other dev flags are provided.
|
||||
if c.flagDevConsul || c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS {
|
||||
if c.flagDevHA || c.flagDevTransactional || c.flagDevLeasedKV || c.flagDevThreeNode || c.flagDevFourCluster || c.flagDevAutoSeal || c.flagDevKVV1 || c.flagDevTLS {
|
||||
c.flagDev = true
|
||||
}
|
||||
|
||||
|
@ -1319,7 +1298,7 @@ func (c *ServerCommand) Run(args []string) int {
|
|||
|
||||
if !storageSupportedForEnt(&coreConfig) {
|
||||
c.UI.Warn("")
|
||||
c.UI.Warn(wrapAtLength(fmt.Sprintf("WARNING: storage configured to use %q which is not supported for Vault Enterprise, must be \"raft\" or \"consul\"", coreConfig.StorageType)))
|
||||
c.UI.Warn(wrapAtLength(fmt.Sprintf("WARNING: storage configured to use %q which is not supported for Vault Enterprise, must be \"raft\"", coreConfig.StorageType)))
|
||||
c.UI.Warn("")
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import (
|
|||
credOkta "github.com/hashicorp/vault/builtin/credential/okta"
|
||||
credRadius "github.com/hashicorp/vault/builtin/credential/radius"
|
||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
logicalConsul "github.com/hashicorp/vault/builtin/logical/consul"
|
||||
logicalNomad "github.com/hashicorp/vault/builtin/logical/nomad"
|
||||
logicalPki "github.com/hashicorp/vault/builtin/logical/pki"
|
||||
logicalRabbit "github.com/hashicorp/vault/builtin/logical/rabbitmq"
|
||||
|
@ -121,7 +120,6 @@ func newRegistry() *registry {
|
|||
Factory: removedFactory,
|
||||
DeprecationStatus: consts.Removed,
|
||||
},
|
||||
"consul": {Factory: logicalConsul.Factory},
|
||||
"kubernetes": {Factory: logicalKube.Factory},
|
||||
"kv": {Factory: logicalKv.Factory},
|
||||
"mysql": {
|
||||
|
|
|
@ -1,784 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
"github.com/hashicorp/consul/api"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/tlsutil"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/physical"
|
||||
"github.com/hashicorp/vault/vault/diagnose"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
// consistencyModeDefault is the configuration value used to tell
|
||||
// consul to use default consistency.
|
||||
consistencyModeDefault = "default"
|
||||
|
||||
// consistencyModeStrong is the configuration value used to tell
|
||||
// consul to use strong consistency.
|
||||
consistencyModeStrong = "strong"
|
||||
|
||||
// nonExistentKey is used as part of a capabilities check against Consul
|
||||
nonExistentKey = "F35C28E1-7035-40BB-B865-6BED9E3A1B28"
|
||||
)
|
||||
|
||||
// Verify ConsulBackend satisfies the correct interfaces
|
||||
var (
|
||||
_ physical.Backend = (*ConsulBackend)(nil)
|
||||
_ physical.FencingHABackend = (*ConsulBackend)(nil)
|
||||
_ physical.Lock = (*ConsulLock)(nil)
|
||||
_ physical.Transactional = (*ConsulBackend)(nil)
|
||||
|
||||
GetInTxnDisabledError = errors.New("get operations inside transactions are disabled in consul backend")
|
||||
)
|
||||
|
||||
// ConsulBackend is a physical backend that stores data at specific
|
||||
// prefix within Consul. It is used for most production situations as
|
||||
// it allows Vault to run on multiple machines in a highly-available manner.
|
||||
// failGetInTxn is only used in tests.
|
||||
type ConsulBackend struct {
|
||||
logger log.Logger
|
||||
client *api.Client
|
||||
path string
|
||||
kv *api.KV
|
||||
txn *api.Txn
|
||||
permitPool *physical.PermitPool
|
||||
consistencyMode string
|
||||
sessionTTL string
|
||||
lockWaitTime time.Duration
|
||||
failGetInTxn *uint32
|
||||
activeNodeLock atomic.Pointer[ConsulLock]
|
||||
}
|
||||
|
||||
// NewConsulBackend constructs a Consul backend using the given API client
|
||||
// and the prefix in the KV store.
|
||||
func NewConsulBackend(conf map[string]string, logger log.Logger) (physical.Backend, error) {
|
||||
// Get the path in Consul
|
||||
path, ok := conf["path"]
|
||||
if !ok {
|
||||
path = "vault/"
|
||||
}
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config path set", "path", path)
|
||||
}
|
||||
|
||||
// Ensure path is suffixed but not prefixed
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
logger.Warn("appending trailing forward slash to path")
|
||||
path += "/"
|
||||
}
|
||||
if strings.HasPrefix(path, "/") {
|
||||
logger.Warn("trimming path of its forward slash")
|
||||
path = strings.TrimPrefix(path, "/")
|
||||
}
|
||||
|
||||
sessionTTL := api.DefaultLockSessionTTL
|
||||
sessionTTLStr, ok := conf["session_ttl"]
|
||||
if ok {
|
||||
_, err := parseutil.ParseDurationSecond(sessionTTLStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid session_ttl: %w", err)
|
||||
}
|
||||
sessionTTL = sessionTTLStr
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config session_ttl set", "session_ttl", sessionTTL)
|
||||
}
|
||||
}
|
||||
|
||||
lockWaitTime := api.DefaultLockWaitTime
|
||||
lockWaitTimeRaw, ok := conf["lock_wait_time"]
|
||||
if ok {
|
||||
d, err := parseutil.ParseDurationSecond(lockWaitTimeRaw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid lock_wait_time: %w", err)
|
||||
}
|
||||
lockWaitTime = d
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config lock_wait_time set", "lock_wait_time", d)
|
||||
}
|
||||
}
|
||||
|
||||
maxParStr, ok := conf["max_parallel"]
|
||||
var maxParInt int
|
||||
if ok {
|
||||
maxParInt, err := strconv.Atoi(maxParStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed parsing max_parallel parameter: %w", err)
|
||||
}
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("max_parallel set", "max_parallel", maxParInt)
|
||||
}
|
||||
}
|
||||
|
||||
consistencyMode, ok := conf["consistency_mode"]
|
||||
if ok {
|
||||
switch consistencyMode {
|
||||
case consistencyModeDefault, consistencyModeStrong:
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid consistency_mode value: %q", consistencyMode)
|
||||
}
|
||||
} else {
|
||||
consistencyMode = consistencyModeDefault
|
||||
}
|
||||
|
||||
// Configure the client
|
||||
consulConf := api.DefaultConfig()
|
||||
// Set MaxIdleConnsPerHost to the number of processes used in expiration.Restore
|
||||
consulConf.Transport.MaxIdleConnsPerHost = consts.ExpirationRestoreWorkerCount
|
||||
|
||||
if err := SetupSecureTLS(context.Background(), consulConf, conf, logger, false); err != nil {
|
||||
return nil, fmt.Errorf("client setup failed: %w", err)
|
||||
}
|
||||
|
||||
consulConf.HttpClient = &http.Client{Transport: consulConf.Transport}
|
||||
client, err := api.NewClient(consulConf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("client setup failed: %w", err)
|
||||
}
|
||||
|
||||
// Set up the backend
|
||||
c := &ConsulBackend{
|
||||
logger: logger,
|
||||
path: path,
|
||||
client: client,
|
||||
kv: client.KV(),
|
||||
txn: client.Txn(),
|
||||
permitPool: physical.NewPermitPool(maxParInt),
|
||||
consistencyMode: consistencyMode,
|
||||
sessionTTL: sessionTTL,
|
||||
lockWaitTime: lockWaitTime,
|
||||
failGetInTxn: new(uint32),
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func SetupSecureTLS(ctx context.Context, consulConf *api.Config, conf map[string]string, logger log.Logger, isDiagnose bool) error {
|
||||
if addr, ok := conf["address"]; ok {
|
||||
consulConf.Address = addr
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config address set", "address", addr)
|
||||
}
|
||||
|
||||
// Copied from the Consul API module; set the Scheme based on
|
||||
// the protocol field if address looks ike a URL.
|
||||
// This can enable the TLS configuration below.
|
||||
parts := strings.SplitN(addr, "://", 2)
|
||||
if len(parts) == 2 {
|
||||
if parts[0] == "http" || parts[0] == "https" {
|
||||
consulConf.Scheme = parts[0]
|
||||
consulConf.Address = parts[1]
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config address parsed", "scheme", parts[0])
|
||||
logger.Debug("config scheme parsed", "address", parts[1])
|
||||
}
|
||||
} // allow "unix:" or whatever else consul supports in the future
|
||||
}
|
||||
}
|
||||
if scheme, ok := conf["scheme"]; ok {
|
||||
consulConf.Scheme = scheme
|
||||
if logger.IsDebug() {
|
||||
logger.Debug("config scheme set", "scheme", scheme)
|
||||
}
|
||||
}
|
||||
if token, ok := conf["token"]; ok {
|
||||
consulConf.Token = token
|
||||
logger.Debug("config token set")
|
||||
}
|
||||
|
||||
if consulConf.Scheme == "https" {
|
||||
if isDiagnose {
|
||||
certPath, okCert := conf["tls_cert_file"]
|
||||
keyPath, okKey := conf["tls_key_file"]
|
||||
if okCert && okKey {
|
||||
warnings, err := diagnose.TLSFileChecks(certPath, keyPath)
|
||||
for _, warning := range warnings {
|
||||
diagnose.Warn(ctx, warning)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("key or cert path: %s, %s, cannot be loaded from consul config file", certPath, keyPath)
|
||||
}
|
||||
|
||||
// Use the parsed Address instead of the raw conf['address']
|
||||
tlsClientConfig, err := tlsutil.SetupTLSConfig(conf, consulConf.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consulConf.Transport.TLSClientConfig = tlsClientConfig
|
||||
if err := http2.ConfigureTransport(consulConf.Transport); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Debug("configured TLS")
|
||||
} else {
|
||||
if isDiagnose {
|
||||
diagnose.Skipped(ctx, "HTTPS is not used, Skipping TLS verification.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpandedCapabilitiesAvailable tests to see if Consul has KVGetOrEmpty and 128 entries per transaction available
|
||||
func (c *ConsulBackend) ExpandedCapabilitiesAvailable(ctx context.Context) bool {
|
||||
available := false
|
||||
|
||||
maxEntries := 128
|
||||
ops := make([]*api.TxnOp, maxEntries)
|
||||
for i := 0; i < maxEntries; i++ {
|
||||
ops[i] = &api.TxnOp{KV: &api.KVTxnOp{
|
||||
Key: c.path + nonExistentKey,
|
||||
Verb: api.KVGetOrEmpty,
|
||||
}}
|
||||
}
|
||||
|
||||
c.permitPool.Acquire()
|
||||
defer c.permitPool.Release()
|
||||
|
||||
queryOpts := &api.QueryOptions{}
|
||||
queryOpts = queryOpts.WithContext(ctx)
|
||||
|
||||
ok, resp, _, err := c.txn.Txn(ops, queryOpts)
|
||||
if ok && len(resp.Errors) == 0 && err == nil {
|
||||
available = true
|
||||
}
|
||||
|
||||
return available
|
||||
}
|
||||
|
||||
func (c *ConsulBackend) writeTxnOps(ctx context.Context, len int) ([]*api.TxnOp, string) {
|
||||
if len < 1 {
|
||||
len = 1
|
||||
}
|
||||
ops := make([]*api.TxnOp, 0, len+1)
|
||||
|
||||
// If we don't have a lock yet, return a transaction with no session check. We
|
||||
// need to do this to allow writes during cluster initialization before there
|
||||
// is an active node.
|
||||
lock := c.activeNodeLock.Load()
|
||||
if lock == nil {
|
||||
return ops, ""
|
||||
}
|
||||
|
||||
lockKey, lockSession := lock.Info()
|
||||
if lockKey == "" || lockSession == "" {
|
||||
return ops, ""
|
||||
}
|
||||
|
||||
// If the context used to write has been marked as a special case write that
|
||||
// happens outside of a lock then don't add the session check.
|
||||
if physical.IsUnfencedWrite(ctx) {
|
||||
return ops, ""
|
||||
}
|
||||
|
||||
// Insert the session check operation at index 0. This will allow us later to
|
||||
// work out easily if a write failure is because of the session check.
|
||||
ops = append(ops, &api.TxnOp{
|
||||
KV: &api.KVTxnOp{
|
||||
Verb: api.KVCheckSession,
|
||||
Key: lockKey,
|
||||
Session: lockSession,
|
||||
},
|
||||
})
|
||||
return ops, lockSession
|
||||
}
|
||||
|
||||
// Transaction is used to run multiple entries via a transaction.
|
||||
func (c *ConsulBackend) Transaction(ctx context.Context, txns []*physical.TxnEntry) error {
|
||||
return c.txnInternal(ctx, txns, "transaction")
|
||||
}
|
||||
|
||||
func (c *ConsulBackend) txnInternal(ctx context.Context, txns []*physical.TxnEntry, apiOpName string) error {
|
||||
if len(txns) == 0 {
|
||||
return nil
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"consul", apiOpName}, time.Now())
|
||||
|
||||
failGetInTxn := atomic.LoadUint32(c.failGetInTxn)
|
||||
for _, t := range txns {
|
||||
if t.Operation == physical.GetOperation && failGetInTxn != 0 {
|
||||
return GetInTxnDisabledError
|
||||
}
|
||||
}
|
||||
|
||||
ops, sessionID := c.writeTxnOps(ctx, len(txns))
|
||||
for _, t := range txns {
|
||||
o, err := c.makeApiTxn(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting physical transactions into api transactions: %w", err)
|
||||
}
|
||||
|
||||
ops = append(ops, o)
|
||||
}
|
||||
|
||||
c.permitPool.Acquire()
|
||||
defer c.permitPool.Release()
|
||||
|
||||
var retErr *multierror.Error
|
||||
kvMap := make(map[string][]byte, 0)
|
||||
|
||||
queryOpts := &api.QueryOptions{}
|
||||
queryOpts = queryOpts.WithContext(ctx)
|
||||
|
||||
ok, resp, _, err := c.txn.Txn(ops, queryOpts)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "is too large") {
|
||||
return fmt.Errorf("%s: %w", physical.ErrValueTooLarge, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if ok && len(resp.Errors) == 0 {
|
||||
// Loop over results and cache them in a map. Note that we're only caching
|
||||
// the first time we see a key, which _should_ correspond to a Get
|
||||
// operation, since we expect those come first in our txns slice (though
|
||||
// after check-session).
|
||||
for _, txnr := range resp.Results {
|
||||
if len(txnr.KV.Value) > 0 {
|
||||
// We need to trim the Consul kv path (typically "vault/") from the key
|
||||
// otherwise it won't match the transaction entries we have.
|
||||
key := strings.TrimPrefix(txnr.KV.Key, c.path)
|
||||
if _, found := kvMap[key]; !found {
|
||||
kvMap[key] = txnr.KV.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(resp.Errors) > 0 {
|
||||
for _, res := range resp.Errors {
|
||||
retErr = multierror.Append(retErr, errors.New(res.What))
|
||||
if res.OpIndex == 0 && sessionID != "" {
|
||||
// We added a session check (sessionID not empty) so an error at OpIndex
|
||||
// 0 means that we failed that session check. We don't attempt to string
|
||||
// match because Consul can return at least three different errors here
|
||||
// with no common string. In all cases though failing this check means
|
||||
// we no longer hold the lock because it was released, modified or
|
||||
// deleted. Rather than just continuing to try writing until the
|
||||
// blocking query manages to notice we're no longer the lock holder
|
||||
// (which can take 10s of seconds even in good network conditions in my
|
||||
// testing) we can now Unlock directly here. Our ConsulLock now has a
|
||||
// shortcut that will cause the lock to close the leaderCh immediately
|
||||
// when we call without waiting for the blocking query to return (unlike
|
||||
// Consul's current Lock implementation). But before we unlock, we
|
||||
// should re-load the lock and ensure it's still the same instance we
|
||||
// just tried to write with in case this goroutine is somehow really
|
||||
// delayed and we actually acquired a whole new lock in the meantime!
|
||||
lock := c.activeNodeLock.Load()
|
||||
if lock != nil {
|
||||
_, lockSessionID := lock.Info()
|
||||
if sessionID == lockSessionID {
|
||||
c.logger.Warn("session check failed on write, we lost active node lock, stepping down", "err", res.What)
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if retErr != nil {
|
||||
return retErr
|
||||
}
|
||||
|
||||
// Loop over our get transactions and populate any values found in our map cache.
|
||||
for _, t := range txns {
|
||||
if val, ok := kvMap[t.Entry.Key]; ok && t.Operation == physical.GetOperation {
|
||||
newVal := make([]byte, len(val))
|
||||
copy(newVal, val)
|
||||
t.Entry.Value = newVal
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConsulBackend) makeApiTxn(txn *physical.TxnEntry) (*api.TxnOp, error) {
|
||||
op := &api.KVTxnOp{
|
||||
Key: c.path + txn.Entry.Key,
|
||||
}
|
||||
switch txn.Operation {
|
||||
case physical.GetOperation:
|
||||
op.Verb = api.KVGetOrEmpty
|
||||
case physical.DeleteOperation:
|
||||
op.Verb = api.KVDelete
|
||||
case physical.PutOperation:
|
||||
op.Verb = api.KVSet
|
||||
op.Value = txn.Entry.Value
|
||||
default:
|
||||
return nil, fmt.Errorf("%q is not a supported transaction operation", txn.Operation)
|
||||
}
|
||||
|
||||
return &api.TxnOp{KV: op}, nil
|
||||
}
|
||||
|
||||
// Put is used to insert or update an entry
|
||||
func (c *ConsulBackend) Put(ctx context.Context, entry *physical.Entry) error {
|
||||
txns := []*physical.TxnEntry{
|
||||
{
|
||||
Operation: physical.PutOperation,
|
||||
Entry: entry,
|
||||
},
|
||||
}
|
||||
return c.txnInternal(ctx, txns, "put")
|
||||
}
|
||||
|
||||
// Get is used to fetch an entry
|
||||
func (c *ConsulBackend) Get(ctx context.Context, key string) (*physical.Entry, error) {
|
||||
defer metrics.MeasureSince([]string{"consul", "get"}, time.Now())
|
||||
|
||||
c.permitPool.Acquire()
|
||||
defer c.permitPool.Release()
|
||||
|
||||
queryOpts := &api.QueryOptions{}
|
||||
queryOpts = queryOpts.WithContext(ctx)
|
||||
|
||||
if c.consistencyMode == consistencyModeStrong {
|
||||
queryOpts.RequireConsistent = true
|
||||
}
|
||||
|
||||
pair, _, err := c.kv.Get(c.path+key, queryOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pair == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ent := &physical.Entry{
|
||||
Key: key,
|
||||
Value: pair.Value,
|
||||
}
|
||||
return ent, nil
|
||||
}
|
||||
|
||||
// Delete is used to permanently delete an entry
|
||||
func (c *ConsulBackend) Delete(ctx context.Context, key string) error {
|
||||
txns := []*physical.TxnEntry{
|
||||
{
|
||||
Operation: physical.DeleteOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: key,
|
||||
},
|
||||
},
|
||||
}
|
||||
return c.txnInternal(ctx, txns, "delete")
|
||||
}
|
||||
|
||||
// List is used to list all the keys under a given
|
||||
// prefix, up to the next prefix.
|
||||
func (c *ConsulBackend) List(ctx context.Context, prefix string) ([]string, error) {
|
||||
defer metrics.MeasureSince([]string{"consul", "list"}, time.Now())
|
||||
scan := c.path + prefix
|
||||
|
||||
// The TrimPrefix call below will not work correctly if we have "//" at the
|
||||
// end. This can happen in cases where you are e.g. listing the root of a
|
||||
// prefix in a logical backend via "/" instead of ""
|
||||
if strings.HasSuffix(scan, "//") {
|
||||
scan = scan[:len(scan)-1]
|
||||
}
|
||||
|
||||
c.permitPool.Acquire()
|
||||
defer c.permitPool.Release()
|
||||
|
||||
queryOpts := &api.QueryOptions{}
|
||||
queryOpts = queryOpts.WithContext(ctx)
|
||||
|
||||
out, _, err := c.kv.Keys(scan, "/", queryOpts)
|
||||
for idx, val := range out {
|
||||
out[idx] = strings.TrimPrefix(val, scan)
|
||||
}
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (c *ConsulBackend) FailGetInTxn(fail bool) {
|
||||
var val uint32
|
||||
if fail {
|
||||
val = 1
|
||||
}
|
||||
atomic.StoreUint32(c.failGetInTxn, val)
|
||||
}
|
||||
|
||||
// LockWith is used for mutual exclusion based on the given key.
|
||||
func (c *ConsulBackend) LockWith(key, value string) (physical.Lock, error) {
|
||||
cl := &ConsulLock{
|
||||
logger: c.logger,
|
||||
client: c.client,
|
||||
key: c.path + key,
|
||||
value: value,
|
||||
consistencyMode: c.consistencyMode,
|
||||
sessionTTL: c.sessionTTL,
|
||||
lockWaitTime: c.lockWaitTime,
|
||||
}
|
||||
return cl, nil
|
||||
}
|
||||
|
||||
// HAEnabled indicates whether the HA functionality should be exposed.
|
||||
// Currently always returns true.
|
||||
func (c *ConsulBackend) HAEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// DetectHostAddr is used to detect the host address by asking the Consul agent
|
||||
func (c *ConsulBackend) DetectHostAddr() (string, error) {
|
||||
agent := c.client.Agent()
|
||||
self, err := agent.Self()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addr, ok := self["Member"]["Addr"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to convert an address to string")
|
||||
}
|
||||
return addr, nil
|
||||
}
|
||||
|
||||
// RegisterActiveNodeLock is called after active node lock is obtained to allow
|
||||
// us to fence future writes.
|
||||
func (c *ConsulBackend) RegisterActiveNodeLock(l physical.Lock) error {
|
||||
cl, ok := l.(*ConsulLock)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid Lock type")
|
||||
}
|
||||
c.activeNodeLock.Store(cl)
|
||||
key, sessionID := cl.Info()
|
||||
c.logger.Info("registered active node lock", "key", key, "sessionID", sessionID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConsulLock is used to provide the Lock interface backed by Consul. We work
|
||||
// around some limitations of Consuls api.Lock noted in
|
||||
// https://github.com/hashicorp/consul/issues/18271 by creating and managing the
|
||||
// session ourselves, while using Consul's Lock to do the heavy lifting.
|
||||
type ConsulLock struct {
|
||||
logger log.Logger
|
||||
client *api.Client
|
||||
key string
|
||||
value string
|
||||
consistencyMode string
|
||||
sessionTTL string
|
||||
lockWaitTime time.Duration
|
||||
|
||||
mu sync.Mutex // protects session state
|
||||
session *lockSession
|
||||
// sessionID is a copy of the value from session.id. We use a separate field
|
||||
// because `Info` needs to keep returning the same sessionID after Unlock has
|
||||
// cleaned up the session state so that we continue to fence any writes still
|
||||
// in flight after the lock is Unlocked. It's easier to reason about that as a
|
||||
// separate field rather than keeping an already-terminated session object
|
||||
// around. Once Lock is called again this will be replaced (while mu is
|
||||
// locked) with the new session ID. Must hold mu to read or write this.
|
||||
sessionID string
|
||||
}
|
||||
|
||||
type lockSession struct {
|
||||
// id is immutable after the session is created so does not need mu held
|
||||
id string
|
||||
|
||||
// mu protects the lock and unlockCh to ensure they are only cleaned up once
|
||||
mu sync.Mutex
|
||||
lock *api.Lock
|
||||
unlockCh chan struct{}
|
||||
}
|
||||
|
||||
func (s *lockSession) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
lockHeld := false
|
||||
defer func() {
|
||||
if !lockHeld {
|
||||
s.cleanupLocked()
|
||||
}
|
||||
}()
|
||||
|
||||
consulLeaderCh, err := s.lock.Lock(stopCh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if consulLeaderCh == nil {
|
||||
// If both leaderCh and err are nil from Consul's Lock then it means we
|
||||
// waited for the lockWait without grabbing it.
|
||||
return nil, nil
|
||||
}
|
||||
// We got the Lock, monitor it!
|
||||
lockHeld = true
|
||||
leaderCh := make(chan struct{})
|
||||
go s.monitorLock(leaderCh, s.unlockCh, consulLeaderCh)
|
||||
return leaderCh, nil
|
||||
}
|
||||
|
||||
// monitorLock waits for either unlockCh or consulLeaderCh to close and then
|
||||
// closes leaderCh. It's designed to be run in a separate goroutine. Note that
|
||||
// we pass unlockCh rather than accessing it via the member variable because it
|
||||
// is mutated under the lock during Unlock so reading it from c could be racy.
|
||||
// We just need the chan created at the call site here so we pass it instead of
|
||||
// locking and unlocking in here.
|
||||
func (s *lockSession) monitorLock(leaderCh chan struct{}, unlockCh, consulLeaderCh <-chan struct{}) {
|
||||
select {
|
||||
case <-unlockCh:
|
||||
case <-consulLeaderCh:
|
||||
}
|
||||
// We lost the lock. Close the leaderCh
|
||||
close(leaderCh)
|
||||
|
||||
// Whichever chan closed, cleanup to unwind all the state. If we were
|
||||
// triggered by a cleanup call this will be a no-op, but if not it ensures all
|
||||
// state is cleaned up correctly.
|
||||
s.cleanup()
|
||||
}
|
||||
|
||||
func (s *lockSession) cleanup() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.cleanupLocked()
|
||||
}
|
||||
|
||||
func (s *lockSession) cleanupLocked() {
|
||||
if s.lock != nil {
|
||||
s.lock.Unlock()
|
||||
s.lock = nil
|
||||
}
|
||||
if s.unlockCh != nil {
|
||||
close(s.unlockCh)
|
||||
s.unlockCh = nil
|
||||
}
|
||||
// Don't bother destroying sessions as they will be destroyed after TTL
|
||||
// anyway.
|
||||
}
|
||||
|
||||
func (c *ConsulLock) createSession() (*lockSession, error) {
|
||||
se := &api.SessionEntry{
|
||||
Name: "Vault Lock",
|
||||
TTL: c.sessionTTL,
|
||||
// We use Consul's default LockDelay of 15s by not specifying it
|
||||
}
|
||||
session, _, err := c.client.Session().Create(se, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := &api.LockOptions{
|
||||
Key: c.key,
|
||||
Value: []byte(c.value),
|
||||
Session: session,
|
||||
MonitorRetries: 5,
|
||||
LockWaitTime: c.lockWaitTime,
|
||||
SessionTTL: c.sessionTTL,
|
||||
}
|
||||
lock, err := c.client.LockOpts(opts)
|
||||
if err != nil {
|
||||
// Don't bother destroying sessions as they will be destroyed after TTL
|
||||
// anyway.
|
||||
return nil, fmt.Errorf("failed to create lock: %w", err)
|
||||
}
|
||||
|
||||
unlockCh := make(chan struct{})
|
||||
|
||||
s := &lockSession{
|
||||
id: session,
|
||||
lock: lock,
|
||||
unlockCh: unlockCh,
|
||||
}
|
||||
|
||||
// Start renewals of the session
|
||||
go func() {
|
||||
// Note we capture unlockCh here rather than s.unlockCh because s.unlockCh
|
||||
// is mutated on cleanup which is racy since we don't hold a lock here.
|
||||
// unlockCh will never be mutated though.
|
||||
err := c.client.Session().RenewPeriodic(c.sessionTTL, session, nil, unlockCh)
|
||||
if err != nil {
|
||||
c.logger.Error("failed to renew consul session for more than the TTL, lock lost", "err", err)
|
||||
}
|
||||
// release other resources for this session only i.e. don't c.Unlock as that
|
||||
// might now be locked under a different session).
|
||||
s.cleanup()
|
||||
}()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (c *ConsulLock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.session != nil {
|
||||
return nil, fmt.Errorf("lock instance already locked")
|
||||
}
|
||||
|
||||
session, err := c.createSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
leaderCh, err := session.Lock(stopCh)
|
||||
if leaderCh != nil && err == nil {
|
||||
// We hold the lock, store the session
|
||||
c.session = session
|
||||
c.sessionID = session.id
|
||||
}
|
||||
return leaderCh, err
|
||||
}
|
||||
|
||||
func (c *ConsulLock) Unlock() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.session != nil {
|
||||
c.session.cleanup()
|
||||
c.session = nil
|
||||
// Don't clear c.sessionID since we rely on returning the same old ID after
|
||||
// Unlock until the next Lock.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConsulLock) Value() (bool, string, error) {
|
||||
kv := c.client.KV()
|
||||
|
||||
var queryOptions *api.QueryOptions
|
||||
if c.consistencyMode == consistencyModeStrong {
|
||||
queryOptions = &api.QueryOptions{
|
||||
RequireConsistent: true,
|
||||
}
|
||||
}
|
||||
|
||||
pair, _, err := kv.Get(c.key, queryOptions)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
if pair == nil {
|
||||
return false, "", nil
|
||||
}
|
||||
// Note that held is expected to mean "does _any_ node hold the lock" not
|
||||
// "does this current instance hold the lock" so although we know what our own
|
||||
// session ID is, we don't check it matches here only that there is _some_
|
||||
// session in Consul holding the lock right now.
|
||||
held := pair.Session != ""
|
||||
value := string(pair.Value)
|
||||
return held, value, nil
|
||||
}
|
||||
|
||||
func (c *ConsulLock) Info() (key, sessionid string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.key, c.sessionID
|
||||
}
|
|
@ -1,528 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/consul"
|
||||
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||
"github.com/hashicorp/vault/sdk/physical"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConsul_newConsulBackend(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
consulConfig map[string]string
|
||||
fail bool
|
||||
redirectAddr string
|
||||
checkTimeout time.Duration
|
||||
path string
|
||||
service string
|
||||
address string
|
||||
scheme string
|
||||
token string
|
||||
max_parallel int
|
||||
disableReg bool
|
||||
consistencyMode string
|
||||
}{
|
||||
{
|
||||
name: "Valid default config",
|
||||
consulConfig: map[string]string{},
|
||||
checkTimeout: 5 * time.Second,
|
||||
redirectAddr: "http://127.0.0.1:8200",
|
||||
path: "vault/",
|
||||
service: "vault",
|
||||
address: "127.0.0.1:8500",
|
||||
scheme: "http",
|
||||
token: "",
|
||||
max_parallel: 4,
|
||||
disableReg: false,
|
||||
consistencyMode: "default",
|
||||
},
|
||||
{
|
||||
name: "Valid modified config",
|
||||
consulConfig: map[string]string{
|
||||
"path": "seaTech/",
|
||||
"service": "astronomy",
|
||||
"redirect_addr": "http://127.0.0.2:8200",
|
||||
"check_timeout": "6s",
|
||||
"address": "127.0.0.2",
|
||||
"scheme": "https",
|
||||
"token": "deadbeef-cafeefac-deadc0de-feedface",
|
||||
"max_parallel": "4",
|
||||
"disable_registration": "false",
|
||||
"consistency_mode": "strong",
|
||||
},
|
||||
checkTimeout: 6 * time.Second,
|
||||
path: "seaTech/",
|
||||
service: "astronomy",
|
||||
redirectAddr: "http://127.0.0.2:8200",
|
||||
address: "127.0.0.2",
|
||||
scheme: "https",
|
||||
token: "deadbeef-cafeefac-deadc0de-feedface",
|
||||
max_parallel: 4,
|
||||
consistencyMode: "strong",
|
||||
},
|
||||
{
|
||||
name: "Unix socket",
|
||||
consulConfig: map[string]string{
|
||||
"address": "unix:///tmp/.consul.http.sock",
|
||||
},
|
||||
address: "/tmp/.consul.http.sock",
|
||||
scheme: "http", // Default, not overridden?
|
||||
|
||||
// Defaults
|
||||
checkTimeout: 5 * time.Second,
|
||||
redirectAddr: "http://127.0.0.1:8200",
|
||||
path: "vault/",
|
||||
service: "vault",
|
||||
token: "",
|
||||
max_parallel: 4,
|
||||
disableReg: false,
|
||||
consistencyMode: "default",
|
||||
},
|
||||
{
|
||||
name: "Scheme in address",
|
||||
consulConfig: map[string]string{
|
||||
"address": "https://127.0.0.2:5000",
|
||||
},
|
||||
address: "127.0.0.2:5000",
|
||||
scheme: "https",
|
||||
|
||||
// Defaults
|
||||
checkTimeout: 5 * time.Second,
|
||||
redirectAddr: "http://127.0.0.1:8200",
|
||||
path: "vault/",
|
||||
service: "vault",
|
||||
token: "",
|
||||
max_parallel: 4,
|
||||
disableReg: false,
|
||||
consistencyMode: "default",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
|
||||
be, err := NewConsulBackend(test.consulConfig, logger)
|
||||
if test.fail {
|
||||
if err == nil {
|
||||
t.Fatalf(`Expected config "%s" to fail`, test.name)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if !test.fail && err != nil {
|
||||
t.Fatalf("Expected config %s to not fail: %v", test.name, err)
|
||||
}
|
||||
|
||||
c, ok := be.(*ConsulBackend)
|
||||
if !ok {
|
||||
t.Fatalf("Expected ConsulBackend: %s", test.name)
|
||||
}
|
||||
|
||||
if test.path != c.path {
|
||||
t.Errorf("bad: %s %v != %v", test.name, test.path, c.path)
|
||||
}
|
||||
|
||||
if test.consistencyMode != c.consistencyMode {
|
||||
t.Errorf("bad consistency_mode value: %v != %v", test.consistencyMode, c.consistencyMode)
|
||||
}
|
||||
|
||||
// The configuration stored in the Consul "client" object is not exported, so
|
||||
// we either have to skip validating it, or add a method to export it, or use reflection.
|
||||
consulConfig := reflect.Indirect(reflect.ValueOf(c.client)).FieldByName("config")
|
||||
consulConfigScheme := consulConfig.FieldByName("Scheme").String()
|
||||
consulConfigAddress := consulConfig.FieldByName("Address").String()
|
||||
|
||||
if test.scheme != consulConfigScheme {
|
||||
t.Errorf("bad scheme value: %v != %v", test.scheme, consulConfigScheme)
|
||||
}
|
||||
|
||||
if test.address != consulConfigAddress {
|
||||
t.Errorf("bad address value: %v != %v", test.address, consulConfigAddress)
|
||||
}
|
||||
|
||||
// FIXME(sean@): Unable to test max_parallel
|
||||
// if test.max_parallel != cap(c.permitPool) {
|
||||
// t.Errorf("bad: %v != %v", test.max_parallel, cap(c.permitPool))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsulBackend(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true)
|
||||
defer cleanup()
|
||||
|
||||
client, err := api.NewClient(config.APIConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
randPath := fmt.Sprintf("vault-%d/", time.Now().Unix())
|
||||
defer func() {
|
||||
client.KV().DeleteTree(randPath, nil)
|
||||
}()
|
||||
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
|
||||
b, err := NewConsulBackend(map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": randPath,
|
||||
"max_parallel": "256",
|
||||
}, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
physical.ExerciseBackend(t, b)
|
||||
physical.ExerciseBackend_ListPrefix(t, b)
|
||||
}
|
||||
|
||||
func TestConsul_TooLarge(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true)
|
||||
defer cleanup()
|
||||
|
||||
client, err := api.NewClient(config.APIConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
randPath := fmt.Sprintf("vault-%d/", time.Now().Unix())
|
||||
defer func() {
|
||||
client.KV().DeleteTree(randPath, nil)
|
||||
}()
|
||||
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
|
||||
b, err := NewConsulBackend(map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": randPath,
|
||||
"max_parallel": "256",
|
||||
}, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
zeros := make([]byte, 600000)
|
||||
n, err := rand.Read(zeros)
|
||||
if n != 600000 {
|
||||
t.Fatalf("expected 500k zeros, read %d", n)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = b.Put(context.Background(), &physical.Entry{
|
||||
Key: "foo",
|
||||
Value: zeros,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
|
||||
t.Fatalf("expected value too large error, got %v", err)
|
||||
}
|
||||
|
||||
err = b.(physical.Transactional).Transaction(context.Background(), []*physical.TxnEntry{
|
||||
{
|
||||
Operation: physical.PutOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: "foo",
|
||||
Value: zeros,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), physical.ErrValueTooLarge) {
|
||||
t.Fatalf("expected value too large error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsul_ExpandedCapabilitiesAvailable(t *testing.T) {
|
||||
testCases := map[string]bool{
|
||||
"1.13.5": false,
|
||||
"1.14.3": true,
|
||||
}
|
||||
|
||||
for version, shouldBeAvailable := range testCases {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, version, false, true)
|
||||
defer cleanup()
|
||||
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
backendConfig := map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": "vault/",
|
||||
"max_parallel": "-1",
|
||||
}
|
||||
|
||||
be, err := NewConsulBackend(backendConfig, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := be.(*ConsulBackend)
|
||||
|
||||
isAvailable := b.ExpandedCapabilitiesAvailable(context.Background())
|
||||
if isAvailable != shouldBeAvailable {
|
||||
t.Errorf("%t != %t, version %s\n", isAvailable, shouldBeAvailable, version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsul_TransactionalBackend_GetTransactionsForNonExistentValues(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, "1.14.2", false, true)
|
||||
defer cleanup()
|
||||
|
||||
client, err := api.NewClient(config.APIConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
txns := make([]*physical.TxnEntry, 0)
|
||||
ctx := context.Background()
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
backendConfig := map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": "vault/",
|
||||
"max_parallel": "-1",
|
||||
}
|
||||
|
||||
be, err := NewConsulBackend(backendConfig, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := be.(*ConsulBackend)
|
||||
|
||||
defer func() {
|
||||
_, _ = client.KV().DeleteTree("foo/", nil)
|
||||
}()
|
||||
|
||||
txns = append(txns, &physical.TxnEntry{
|
||||
Operation: physical.GetOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: "foo/bar",
|
||||
},
|
||||
})
|
||||
txns = append(txns, &physical.TxnEntry{
|
||||
Operation: physical.PutOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: "foo/bar",
|
||||
Value: []byte("baz"),
|
||||
},
|
||||
})
|
||||
|
||||
err = b.Transaction(ctx, txns)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// This should return nil, because the key foo/bar didn't exist when we ran that transaction, so the get
|
||||
// should return nil, and the put always returns nil
|
||||
for _, txn := range txns {
|
||||
if txn.Operation == physical.GetOperation {
|
||||
if txn.Entry.Value != nil {
|
||||
t.Fatalf("expected txn.entry.value to be nil but it was %q", string(txn.Entry.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConsul_TransactionalBackend_GetTransactions tests that passing a slice of transactions to the
|
||||
// consul backend will populate values for any transactions that are Get operations.
|
||||
func TestConsul_TransactionalBackend_GetTransactions(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, "1.14.2", false, true)
|
||||
defer cleanup()
|
||||
|
||||
client, err := api.NewClient(config.APIConfig())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
txns := make([]*physical.TxnEntry, 0)
|
||||
ctx := context.Background()
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
backendConfig := map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": "vault/",
|
||||
"max_parallel": "-1",
|
||||
}
|
||||
|
||||
be, err := NewConsulBackend(backendConfig, logger)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := be.(*ConsulBackend)
|
||||
|
||||
defer func() {
|
||||
_, _ = client.KV().DeleteTree("foo/", nil)
|
||||
}()
|
||||
|
||||
// Add some seed values to consul, and prepare our slice of transactions at the same time
|
||||
for i := 0; i < 64; i++ {
|
||||
key := fmt.Sprintf("foo/lol-%d", i)
|
||||
err := b.Put(ctx, &physical.Entry{Key: key, Value: []byte(fmt.Sprintf("value-%d", i))})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
txns = append(txns, &physical.TxnEntry{
|
||||
Operation: physical.GetOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
for i := 0; i < 64; i++ {
|
||||
key := fmt.Sprintf("foo/lol-%d", i)
|
||||
if i%2 == 0 {
|
||||
txns = append(txns, &physical.TxnEntry{
|
||||
Operation: physical.PutOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: key,
|
||||
Value: []byte("lmao"),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
txns = append(txns, &physical.TxnEntry{
|
||||
Operation: physical.DeleteOperation,
|
||||
Entry: &physical.Entry{
|
||||
Key: key,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(txns) != 128 {
|
||||
t.Fatal("wrong number of transactions")
|
||||
}
|
||||
|
||||
err = b.Transaction(ctx, txns)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check that our Get operations were populated with their values
|
||||
for i, txn := range txns {
|
||||
if txn.Operation == physical.GetOperation {
|
||||
val := []byte(fmt.Sprintf("value-%d", i))
|
||||
if !bytes.Equal(val, txn.Entry.Value) {
|
||||
t.Fatalf("expected %s to equal %s but it didn't", hex.EncodeToString(val), hex.EncodeToString(txn.Entry.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsulHABackend(t *testing.T) {
|
||||
cleanup, config := consul.PrepareTestContainer(t, "1.4.4", false, true)
|
||||
defer cleanup()
|
||||
|
||||
client, err := api.NewClient(config.APIConfig())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// We used to use a timestamp here but then if you run multiple instances in
|
||||
// parallel with one Consul they end up conflicting.
|
||||
randPath := fmt.Sprintf("vault-%d/", rand.Int())
|
||||
defer func() {
|
||||
client.KV().DeleteTree(randPath, nil)
|
||||
}()
|
||||
|
||||
logger := logging.NewVaultLogger(log.Debug)
|
||||
backendConfig := map[string]string{
|
||||
"address": config.Address(),
|
||||
"token": config.Token,
|
||||
"path": randPath,
|
||||
"max_parallel": "-1",
|
||||
// We have to wait this out as part of the test so shorten it a little from
|
||||
// the default 15 seconds helps with test run times, especially when running
|
||||
// this in a loop to detect flakes!
|
||||
"lock_wait_time": "3s",
|
||||
}
|
||||
|
||||
b, err := NewConsulBackend(backendConfig, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
b2, err := NewConsulBackend(backendConfig, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
physical.ExerciseHABackend(t, b.(physical.HABackend), b2.(physical.HABackend))
|
||||
|
||||
detect, ok := b.(physical.RedirectDetect)
|
||||
if !ok {
|
||||
t.Fatalf("consul does not implement RedirectDetect")
|
||||
}
|
||||
host, err := detect.DetectHostAddr()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if host == "" {
|
||||
t.Fatalf("bad addr: %v", host)
|
||||
}
|
||||
|
||||
// Calling `Info` on a Lock that has been unlocked must still return the old
|
||||
// sessionID (until it is locked again) otherwise we will fail to fence writes
|
||||
// that are still in flight from before (e.g. queued WAL or Merkle flushes) as
|
||||
// soon as the first one unlocks the session allowing corruption again.
|
||||
l, err := b.(physical.HABackend).LockWith("test-lock-session-info", "bar")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectKey := randPath + "test-lock-session-info"
|
||||
|
||||
cl := l.(*ConsulLock)
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
close(stopCh)
|
||||
})
|
||||
leaderCh, err := cl.Lock(stopCh)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, leaderCh)
|
||||
|
||||
key, sid := cl.Info()
|
||||
require.Equal(t, expectKey, key)
|
||||
require.NotEmpty(t, sid)
|
||||
|
||||
// Now Unlock the lock, sessionID should be reset to empty string
|
||||
err = cl.Unlock()
|
||||
require.NoError(t, err)
|
||||
key2, sid2 := cl.Info()
|
||||
require.Equal(t, key, key2)
|
||||
require.Equal(t, sid, sid2)
|
||||
|
||||
// Lock it again, this should cause a new session to be created so SID should
|
||||
// change.
|
||||
leaderCh, err = cl.Lock(stopCh)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, leaderCh)
|
||||
|
||||
key3, sid3 := cl.Info()
|
||||
require.Equal(t, key, key3)
|
||||
require.NotEqual(t, sid, sid3)
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package consul
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DurationMinusBuffer returns a duration, minus a buffer and jitter
|
||||
// subtracted from the duration. This function is used primarily for
|
||||
// servicing Consul TTL Checks in advance of the TTL.
|
||||
func DurationMinusBuffer(intv time.Duration, buffer time.Duration, jitter int64) time.Duration {
|
||||
d := intv - buffer
|
||||
if jitter == 0 {
|
||||
d -= RandomStagger(d)
|
||||
} else {
|
||||
d -= RandomStagger(time.Duration(int64(d) / jitter))
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// DurationMinusBufferDomain returns the domain of valid durations from a
|
||||
// call to DurationMinusBuffer. This function is used to check user
|
||||
// specified input values to DurationMinusBuffer.
|
||||
func DurationMinusBufferDomain(intv time.Duration, buffer time.Duration, jitter int64) (min time.Duration, max time.Duration) {
|
||||
max = intv - buffer
|
||||
if jitter == 0 {
|
||||
min = max
|
||||
} else {
|
||||
min = max - time.Duration(int64(max)/jitter)
|
||||
}
|
||||
return min, max
|
||||
}
|
||||
|
||||
// RandomStagger returns an interval between 0 and the duration
|
||||
func RandomStagger(intv time.Duration) time.Duration {
|
||||
if intv == 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(uint64(rand.Int63()) % uint64(intv))
|
||||
}
|
|
@ -64,7 +64,6 @@ vault auth enable "radius"
|
|||
vault auth enable "userpass"
|
||||
|
||||
# Enable secrets plugins
|
||||
vault secrets enable "consul"
|
||||
vault secrets enable "database"
|
||||
vault secrets enable "kubernetes"
|
||||
vault secrets enable -path="kv-v1/" -version=1 "kv"
|
||||
|
|
|
@ -35,11 +35,6 @@ const MOUNTABLE_SECRET_ENGINES = [
|
|||
type: 'ad',
|
||||
category: 'cloud',
|
||||
},
|
||||
{
|
||||
displayName: 'Consul',
|
||||
type: 'consul',
|
||||
category: 'infra',
|
||||
},
|
||||
{
|
||||
displayName: 'Databases',
|
||||
type: 'database',
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<WizardSection
|
||||
@headerText="Consul"
|
||||
@headerIcon="consul"
|
||||
@docText="Docs: Consul Secrets"
|
||||
@docPath="/docs/secrets/consul/index.html"
|
||||
>
|
||||
<p>
|
||||
The Consul Secrets Engine generates Consul API tokens dynamically based on Consul ACL policies.
|
||||
</p>
|
||||
</WizardSection>
|
|
@ -1 +0,0 @@
|
|||
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M23.42 27.224a3.517 3.517 0 1 1-.012-7.034 3.517 3.517 0 0 1 .011 7.034" fill="#961D59"/><path d="M30.302 25.466a1.612 1.612 0 1 1 0-3.225 1.612 1.612 0 0 1 0 3.225M36.54 26.94a1.612 1.612 0 1 1 .032-.132c-.018.04-.018.082-.032.132M35.074 22.784a1.61 1.61 0 1 1-.113-3.159 1.613 1.613 0 0 1 1.31 1.224c.046.206.051.42.018.629a1.55 1.55 0 0 1-1.212 1.306M40.957 26.772a1.603 1.603 0 0 1-1.857 1.307 1.611 1.611 0 0 1-1.3-1.867 1.603 1.603 0 0 1 1.856-1.308 1.618 1.618 0 0 1 1.34 1.71.418.418 0 0 0-.03.144M39.65 22.806a1.612 1.612 0 1 1 1.345-1.44 1.603 1.603 0 0 1-1.344 1.44M38.453 32.256a1.612 1.612 0 1 1 .197-.653c-.012.23-.08.455-.197.653M37.836 17.636a1.611 1.611 0 1 1 .61-2.195c.166.28.236.607.2.93a1.612 1.612 0 0 1-.81 1.265" fill="#D62783"/><path d="M23.702 40.12c-9.062 0-16.409-7.282-16.409-16.267 0-8.984 7.347-16.267 16.409-16.267a16.33 16.33 0 0 1 9.97 3.341l-1.994 2.593a13.198 13.198 0 0 0-13.783-1.339c-4.477 2.194-7.306 6.72-7.297 11.67-.01 4.95 2.82 9.475 7.297 11.67a13.198 13.198 0 0 0 13.783-1.34l1.994 2.596a16.358 16.358 0 0 1-9.97 3.344z" fill="#D62783" fill-rule="nonzero"/></g></svg>
|
Before Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in New Issue
Block a user