Files
nomad/client/consul/consul.go

203 lines
5.7 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package consul
import (
"context"
"fmt"
"time"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/helper/useragent"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
)
// SupportedProxiesAPI is the interface the Nomad Client uses to request from
// Consul the set of supported proxied to use for Consul Connect.
//
// No ACL requirements
type SupportedProxiesAPI interface {
Proxies() (map[string][]string, error)
}
// SupportedProxiesAPIFunc returns an interface that the Nomad client uses for
// requesting the set of supported proxies from Consul.
type SupportedProxiesAPIFunc func(string) SupportedProxiesAPI
// JWTLoginRequest is an object representing a login request with JWT
type JWTLoginRequest struct {
JWT string
AuthMethodName string
Meta map[string]string
}
// Client is the interface that the nomad client uses to interact with
// Consul tokens
type Client interface {
// DeriveTokenWithJWT logs into Consul using JWT and retrieves a Consul ACL
// token.
DeriveTokenWithJWT(JWTLoginRequest) (*consulapi.ACLToken, error)
RevokeTokens([]*consulapi.ACLToken) error
// TokenPreflightCheck verifies that a token has been replicated before we
// try to use it for registering services or bootstrapping Envoy
TokenPreflightCheck(context.Context, *consulapi.ACLToken) error
}
type consulClient struct {
// client is the API client to interact with consul
client *consulapi.Client
// partition is the Consul partition for the local agent
partition string
// config is the configuration to connect to consul
config *config.ConsulConfig
logger hclog.Logger
// preflightCheckTimeout/BaseInterval control how long the client will wait
// for Consul ACLs tokens to be fully replicated before giving up on the
// allocation; these are configurable via node metadata
preflightCheckTimeout time.Duration
preflightCheckBaseInterval time.Duration
}
// ConsulClientFunc creates a new Consul client for the specific Consul config
type ConsulClientFunc func(config *config.ConsulConfig, logger hclog.Logger) (Client, error)
// NodeGetter breaks a circular dependency between client/config.Config and this
// package
type NodeGetter interface {
GetNode() *structs.Node
}
// NewConsulClientFactory returns a ConsulClientFunc that closes over the
// partition
func NewConsulClientFactory(nodeGetter NodeGetter) ConsulClientFunc {
return func(config *config.ConsulConfig, logger hclog.Logger) (Client, error) {
if config == nil {
return nil, fmt.Errorf("nil consul config")
}
logger = logger.Named("consul").With("name", config.Name)
node := nodeGetter.GetNode()
partition := node.Attributes["consul.partition"]
preflightCheckTimeout := durationFromMeta(
node, "consul.token_preflight_check.timeout", time.Second*10)
preflightCheckBaseInterval := durationFromMeta(
node, "consul.token_preflight_check.base", time.Millisecond*500)
c := &consulClient{
config: config,
logger: logger,
partition: partition,
preflightCheckTimeout: preflightCheckTimeout,
preflightCheckBaseInterval: preflightCheckBaseInterval,
}
// Get the Consul API configuration
apiConf, err := config.ApiConfig()
if err != nil {
logger.Error("error creating default Consul API config", "error", err)
return nil, err
}
// Create the API client
client, err := consulapi.NewClient(apiConf)
if err != nil {
logger.Error("error creating Consul client", "error", err)
return nil, err
}
useragent.SetHeaders(client)
c.client = client
return c, nil
}
}
func durationFromMeta(node *structs.Node, key string, defaultDur time.Duration) time.Duration {
val := node.Meta[key]
if key == "" {
return defaultDur
}
d, err := time.ParseDuration(val)
if err != nil || d == 0 {
return defaultDur
}
return d
}
// DeriveTokenWithJWT takes a JWT from request and returns a consul token.
func (c *consulClient) DeriveTokenWithJWT(req JWTLoginRequest) (*consulapi.ACLToken, error) {
t, _, err := c.client.ACL().Login(&consulapi.ACLLoginParams{
AuthMethod: req.AuthMethodName,
BearerToken: req.JWT,
Meta: req.Meta,
}, &consulapi.WriteOptions{
Partition: c.partition,
})
return t, err
}
func (c *consulClient) RevokeTokens(tokens []*consulapi.ACLToken) error {
var mErr *multierror.Error
for _, token := range tokens {
_, err := c.client.ACL().Logout(&consulapi.WriteOptions{
Namespace: token.Namespace,
Partition: token.Partition,
Token: token.SecretID,
})
mErr = multierror.Append(mErr, err)
}
return mErr.ErrorOrNil()
}
// TokenPreflightCheck verifies that a token has been replicated before we
// try to use it for registering services or bootstrapping Envoy
func (c *consulClient) TokenPreflightCheck(pctx context.Context, t *consulapi.ACLToken) error {
timer, timerStop := helper.NewStoppedTimer()
defer timerStop()
var retry uint64
var err error
ctx, cancel := context.WithTimeout(pctx, c.preflightCheckTimeout)
defer cancel()
for {
_, _, err = c.client.ACL().TokenReadSelf(&consulapi.QueryOptions{
Namespace: t.Namespace,
Partition: c.partition,
AllowStale: true,
Token: t.SecretID,
})
if err == nil {
return nil
}
retry++
backoff := helper.Backoff(
c.preflightCheckBaseInterval, c.preflightCheckBaseInterval*2, retry)
c.logger.Trace("Consul token not ready", "error", err, "backoff", backoff)
timer.Reset(backoff)
select {
case <-ctx.Done():
return err
case <-timer.C:
continue
}
}
}