mirror of
https://github.com/kemko/nomad.git
synced 2026-01-03 17:05:43 +03:00
203 lines
5.7 KiB
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
|
|
}
|
|
}
|
|
}
|