Merge pull request #9926 from hashicorp/f-up-consul-api

consul: upgrade consul/api to 1.8.1
This commit is contained in:
Seth Hoenig 2021-02-01 08:27:52 -06:00 committed by GitHub
commit aa3f6f97a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 868 additions and 113 deletions

6
go.mod
View file

@ -52,8 +52,8 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.2.1-0.20200228141219-3ce3d519df39 github.com/grpc-ecosystem/go-grpc-middleware v1.2.1-0.20200228141219-3ce3d519df39
github.com/hashicorp/consul v1.7.8 github.com/hashicorp/consul v1.7.8
github.com/hashicorp/consul-template v0.25.1 github.com/hashicorp/consul-template v0.25.1
github.com/hashicorp/consul/api v1.6.0 github.com/hashicorp/consul/api v1.8.1
github.com/hashicorp/consul/sdk v0.6.0 github.com/hashicorp/consul/sdk v0.7.0
github.com/hashicorp/cronexpr v1.1.1 github.com/hashicorp/cronexpr v1.1.1
github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de github.com/hashicorp/go-checkpoint v0.0.0-20171009173528-1545e56e46de
github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cleanhttp v0.5.1
@ -81,7 +81,7 @@ require (
github.com/hashicorp/nomad/api v0.0.0-20200529203653-c4416b26d3eb github.com/hashicorp/nomad/api v0.0.0-20200529203653-c4416b26d3eb
github.com/hashicorp/raft v1.1.3-0.20200211192230-365023de17e6 github.com/hashicorp/raft v1.1.3-0.20200211192230-365023de17e6
github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea
github.com/hashicorp/serf v0.9.3 github.com/hashicorp/serf v0.9.5
github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519 github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519
github.com/hashicorp/vault/sdk v0.1.14-0.20190730042320-0dc007d98cc8 github.com/hashicorp/vault/sdk v0.1.14-0.20190730042320-0dc007d98cc8
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d

10
go.sum
View file

@ -329,11 +329,11 @@ github.com/hashicorp/consul v1.7.8/go.mod h1:urbfGaVZDmnXC6geg0LYPh/SRUk1E8nfmDH
github.com/hashicorp/consul-template v0.25.1 h1:+D2s8eyRqWyX7GPNxeUi8tsyh8pRn3J6k8giEchPfKQ= github.com/hashicorp/consul-template v0.25.1 h1:+D2s8eyRqWyX7GPNxeUi8tsyh8pRn3J6k8giEchPfKQ=
github.com/hashicorp/consul-template v0.25.1/go.mod h1:/vUsrJvDuuQHcxEw0zik+YXTS7ZKWZjQeaQhshBmfH0= github.com/hashicorp/consul-template v0.25.1/go.mod h1:/vUsrJvDuuQHcxEw0zik+YXTS7ZKWZjQeaQhshBmfH0=
github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU= github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU=
github.com/hashicorp/consul/api v1.6.0 h1:SZB2hQW8AcTOpfDmiVblQbijxzsRuiyy0JpHfabvHio= github.com/hashicorp/consul/api v1.8.1 h1:BOEQaMWoGMhmQ29fC26bi0qb7/rId9JzZP2V0Xmx7m8=
github.com/hashicorp/consul/api v1.6.0/go.mod h1:1NSuaUUkFaJzMasbfq/11wKYWSR67Xn6r2DXKhuDNFg= github.com/hashicorp/consul/api v1.8.1/go.mod h1:sDjTOq0yUyv5G4h+BqSea7Fn6BU+XbolEz1952UB+mk=
github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/consul/sdk v0.4.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
github.com/hashicorp/consul/sdk v0.6.0 h1:FfhMEkwvQl57CildXJyGHnwGGM4HMODGyfjGwNM1Vdw= github.com/hashicorp/consul/sdk v0.7.0 h1:H6R9d008jDcHPQPAqPNuydAshJ4v5/8URdFnUvK/+sc=
github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM= github.com/hashicorp/consul/sdk v0.7.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
github.com/hashicorp/cronexpr v1.1.0/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/cronexpr v1.1.0/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c= github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c=
github.com/hashicorp/cronexpr v1.1.1/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/cronexpr v1.1.1/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4=
@ -435,6 +435,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k= github.com/hashicorp/serf v0.8.3/go.mod h1:UpNcs7fFbpKIyZaUuSW6EPiH+eZC7OuyFD+wc1oal+k=
github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM= github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM=
github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q=
github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519 h1:2qdbEUXjHohC+OYHtVU5lujvPAHPKYR4IMs9rsiUTk8= github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519 h1:2qdbEUXjHohC+OYHtVU5lujvPAHPKYR4IMs9rsiUTk8=
github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519/go.mod h1:i9PKqwFko/s/aihU1uuHGh/FaQS+Xcgvd9dvnfAvQb0= github.com/hashicorp/vault/api v1.0.5-0.20190730042357-746c0b111519/go.mod h1:i9PKqwFko/s/aihU1uuHGh/FaQS+Xcgvd9dvnfAvQb0=

View file

@ -93,6 +93,8 @@ type AgentService struct {
// to include the Namespace in the hash. When we do, then we are in for lots of fun with tests. // to include the Namespace in the hash. When we do, then we are in for lots of fun with tests.
// For now though, ignoring it works well enough. // For now though, ignoring it works well enough.
Namespace string `json:",omitempty" bexpr:"-" hash:"ignore"` Namespace string `json:",omitempty" bexpr:"-" hash:"ignore"`
// Datacenter is only ever returned and is ignored if presented.
Datacenter string `json:",omitempty" bexpr:"-" hash:"ignore"`
} }
// AgentServiceChecksInfo returns information about a Service and its checks // AgentServiceChecksInfo returns information about a Service and its checks
@ -121,12 +123,84 @@ type AgentServiceConnectProxyConfig struct {
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
} }
const (
// MemberTagKeyACLMode is the key used to indicate what ACL mode the agent is
// operating in. The values of this key will be one of the MemberACLMode constants
// with the key not being present indicating ACLModeUnknown.
MemberTagKeyACLMode = "acls"
// MemberTagRole is the key used to indicate that the member is a server or not.
MemberTagKeyRole = "role"
// MemberTagValueRoleServer is the value of the MemberTagKeyRole used to indicate
// that the member represents a Consul server.
MemberTagValueRoleServer = "consul"
// MemberTagKeySegment is the key name of the tag used to indicate which network
// segment this member is in.
// Network Segments are a Consul Enterprise feature.
MemberTagKeySegment = "segment"
// MemberTagKeyBootstrap is the key name of the tag used to indicate whether this
// agent was started with the "bootstrap" configuration enabled
MemberTagKeyBootstrap = "bootstrap"
// MemberTagValueBootstrap is the value of the MemberTagKeyBootstrap key when the
// agent was started with the "bootstrap" configuration enabled.
MemberTagValueBootstrap = "1"
// MemberTagKeyBootstrapExpect is the key name of the tag used to indicate whether
// this agent was started with the "bootstrap_expect" configuration set to a non-zero
// value. The value of this key will be the string for of that configuration value.
MemberTagKeyBootstrapExpect = "expect"
// MemberTagKeyUseTLS is the key name of the tag used to indicate whther this agent
// was configured to use TLS.
MemberTagKeyUseTLS = "use_tls"
// MemberTagValueUseTLS is the value of the MemberTagKeyUseTLS when the agent was
// configured to use TLS. Any other value indicates that it was not setup in
// that manner.
MemberTagValueUseTLS = "1"
// MemberTagKeyReadReplica is the key used to indicate that the member is a read
// replica server (will remain a Raft non-voter).
// Read Replicas are a Consul Enterprise feature.
MemberTagKeyReadReplica = "read_replica"
// MemberTagValueReadReplica is the value of the MemberTagKeyReadReplica key when
// the member is in fact a read-replica. Any other value indicates that it is not.
// Read Replicas are a Consul Enterprise feature.
MemberTagValueReadReplica = "1"
)
type MemberACLMode string
const (
// ACLModeDisables indicates that ACLs are disabled for this agent
ACLModeDisabled MemberACLMode = "0"
// ACLModeEnabled indicates that ACLs are enabled and operating in new ACL
// mode (v1.4.0+ ACLs)
ACLModeEnabled MemberACLMode = "1"
// ACLModeLegacy indicates that ACLs are enabled and operating in legacy mode.
ACLModeLegacy MemberACLMode = "2"
// ACLModeUnkown is used to indicate that the AgentMember.Tags didn't advertise
// an ACL mode at all. This is the case for Consul versions before v1.4.0 and
// should be treated similarly to ACLModeLegacy.
ACLModeUnknown MemberACLMode = "3"
)
// AgentMember represents a cluster member known to the agent // AgentMember represents a cluster member known to the agent
type AgentMember struct { type AgentMember struct {
Name string Name string
Addr string Addr string
Port uint16 Port uint16
Tags map[string]string Tags map[string]string
// Status of the Member which corresponds to github.com/hashicorp/serf/serf.MemberStatus
// Value is one of:
//
// AgentMemberNone = 0
// AgentMemberAlive = 1
// AgentMemberLeaving = 2
// AgentMemberLeft = 3
// AgentMemberFailed = 4
Status int Status int
ProtocolMin uint8 ProtocolMin uint8
ProtocolMax uint8 ProtocolMax uint8
@ -136,6 +210,30 @@ type AgentMember struct {
DelegateCur uint8 DelegateCur uint8
} }
// ACLMode returns the ACL mode this agent is operating in.
func (m *AgentMember) ACLMode() MemberACLMode {
mode := m.Tags[MemberTagKeyACLMode]
// the key may not have existed but then an
// empty string will be returned and we will
// handle that in the default case of the switch
switch MemberACLMode(mode) {
case ACLModeDisabled:
return ACLModeDisabled
case ACLModeEnabled:
return ACLModeEnabled
case ACLModeLegacy:
return ACLModeLegacy
default:
return ACLModeUnknown
}
}
// IsConsulServer returns true when this member is a Consul server.
func (m *AgentMember) IsConsulServer() bool {
return m.Tags[MemberTagKeyRole] == MemberTagValueRoleServer
}
// AllSegments is used to select for all segments in MembersOpts. // AllSegments is used to select for all segments in MembersOpts.
const AllSegments = "_all" const AllSegments = "_all"

View file

@ -254,6 +254,11 @@ type QueryMeta struct {
// CacheAge is set if request was ?cached and indicates how stale the cached // CacheAge is set if request was ?cached and indicates how stale the cached
// response is. // response is.
CacheAge time.Duration CacheAge time.Duration
// DefaultACLPolicy is used to control the ACL interaction when there is no
// defined policy. This can be "allow" which means ACLs are used to
// deny-list, or "deny" which means ACLs are allow-lists.
DefaultACLPolicy string
} }
// WriteMeta is used to return meta data about a write // WriteMeta is used to return meta data about a write
@ -305,7 +310,7 @@ type Config struct {
TokenFile string TokenFile string
// Namespace is the name of the namespace to send along for the request // Namespace is the name of the namespace to send along for the request
// when no other Namespace ispresent in the QueryOptions // when no other Namespace is present in the QueryOptions
Namespace string Namespace string
TLSConfig TLSConfig TLSConfig TLSConfig
@ -607,9 +612,11 @@ func NewClient(config *Config) (*Client, error) {
trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", parts[1]) return net.Dial("unix", parts[1])
} }
config.HttpClient = &http.Client{ httpClient, err := NewHttpClient(trans, config.TLSConfig)
Transport: trans, if err != nil {
return nil, err
} }
config.HttpClient = httpClient
default: default:
return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
} }
@ -960,6 +967,12 @@ func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
q.AddressTranslationEnabled = false q.AddressTranslationEnabled = false
} }
// Parse X-Consul-Default-ACL-Policy
switch v := header.Get("X-Consul-Default-ACL-Policy"); v {
case "allow", "deny":
q.DefaultACLPolicy = v
}
// Parse Cache info // Parse Cache info
if cacheStr := header.Get("X-Cache"); cacheStr != "" { if cacheStr := header.Get("X-Cache"); cacheStr != "" {
q.CacheHit = strings.EqualFold(cacheStr, "HIT") q.CacheHit = strings.EqualFold(cacheStr, "HIT")

View file

@ -7,6 +7,7 @@ import (
"io" "io"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -19,6 +20,7 @@ const (
ServiceResolver string = "service-resolver" ServiceResolver string = "service-resolver"
IngressGateway string = "ingress-gateway" IngressGateway string = "ingress-gateway"
TerminatingGateway string = "terminating-gateway" TerminatingGateway string = "terminating-gateway"
ServiceIntentions string = "service-intentions"
ProxyConfigGlobal string = "global" ProxyConfigGlobal string = "global"
) )
@ -26,6 +28,8 @@ const (
type ConfigEntry interface { type ConfigEntry interface {
GetKind() string GetKind() string
GetName() string GetName() string
GetNamespace() string
GetMeta() map[string]string
GetCreateIndex() uint64 GetCreateIndex() uint64
GetModifyIndex() uint64 GetModifyIndex() uint64
} }
@ -95,6 +99,7 @@ type ServiceConfigEntry struct {
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"` ExternalSNI string `json:",omitempty" alias:"external_sni"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -107,6 +112,14 @@ func (s *ServiceConfigEntry) GetName() string {
return s.Name return s.Name
} }
func (s *ServiceConfigEntry) GetNamespace() string {
return s.Namespace
}
func (s *ServiceConfigEntry) GetMeta() map[string]string {
return s.Meta
}
func (s *ServiceConfigEntry) GetCreateIndex() uint64 { func (s *ServiceConfigEntry) GetCreateIndex() uint64 {
return s.CreateIndex return s.CreateIndex
} }
@ -122,6 +135,7 @@ type ProxyConfigEntry struct {
Config map[string]interface{} `json:",omitempty"` Config map[string]interface{} `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -134,6 +148,14 @@ func (p *ProxyConfigEntry) GetName() string {
return p.Name return p.Name
} }
func (p *ProxyConfigEntry) GetNamespace() string {
return p.Namespace
}
func (p *ProxyConfigEntry) GetMeta() map[string]string {
return p.Meta
}
func (p *ProxyConfigEntry) GetCreateIndex() uint64 { func (p *ProxyConfigEntry) GetCreateIndex() uint64 {
return p.CreateIndex return p.CreateIndex
} }
@ -158,6 +180,8 @@ func makeConfigEntry(kind, name string) (ConfigEntry, error) {
return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil return &IngressGatewayConfigEntry{Kind: kind, Name: name}, nil
case TerminatingGateway: case TerminatingGateway:
return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
case ServiceIntentions:
return &ServiceIntentionsConfigEntry{Kind: kind, Name: name}, nil
default: default:
return nil, fmt.Errorf("invalid config entry kind: %s", kind) return nil, fmt.Errorf("invalid config entry kind: %s", kind)
} }
@ -200,7 +224,10 @@ func DecodeConfigEntry(raw map[string]interface{}) (ConfigEntry, error) {
} }
decodeConf := &mapstructure.DecoderConfig{ decodeConf := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(), DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
),
Result: &entry, Result: &entry,
WeaklyTypedInput: true, WeaklyTypedInput: true,
} }

View file

@ -12,14 +12,17 @@ type ServiceRouterConfigEntry struct {
Routes []ServiceRoute `json:",omitempty"` Routes []ServiceRoute `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
func (e *ServiceRouterConfigEntry) GetKind() string { return e.Kind } func (e *ServiceRouterConfigEntry) GetKind() string { return e.Kind }
func (e *ServiceRouterConfigEntry) GetName() string { return e.Name } func (e *ServiceRouterConfigEntry) GetName() string { return e.Name }
func (e *ServiceRouterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex } func (e *ServiceRouterConfigEntry) GetNamespace() string { return e.Namespace }
func (e *ServiceRouterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex } func (e *ServiceRouterConfigEntry) GetMeta() map[string]string { return e.Meta }
func (e *ServiceRouterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
func (e *ServiceRouterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
type ServiceRoute struct { type ServiceRoute struct {
Match *ServiceRouteMatch `json:",omitempty"` Match *ServiceRouteMatch `json:",omitempty"`
@ -111,14 +114,17 @@ type ServiceSplitterConfigEntry struct {
Splits []ServiceSplit `json:",omitempty"` Splits []ServiceSplit `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
func (e *ServiceSplitterConfigEntry) GetKind() string { return e.Kind } func (e *ServiceSplitterConfigEntry) GetKind() string { return e.Kind }
func (e *ServiceSplitterConfigEntry) GetName() string { return e.Name } func (e *ServiceSplitterConfigEntry) GetName() string { return e.Name }
func (e *ServiceSplitterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex } func (e *ServiceSplitterConfigEntry) GetNamespace() string { return e.Namespace }
func (e *ServiceSplitterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex } func (e *ServiceSplitterConfigEntry) GetMeta() map[string]string { return e.Meta }
func (e *ServiceSplitterConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
func (e *ServiceSplitterConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
type ServiceSplit struct { type ServiceSplit struct {
Weight float32 Weight float32
@ -138,6 +144,11 @@ type ServiceResolverConfigEntry struct {
Failover map[string]ServiceResolverFailover `json:",omitempty"` Failover map[string]ServiceResolverFailover `json:",omitempty"`
ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"` ConnectTimeout time.Duration `json:",omitempty" alias:"connect_timeout"`
// LoadBalancer determines the load balancing policy and configuration for services
// issuing requests to this upstream service.
LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -178,10 +189,12 @@ func (e *ServiceResolverConfigEntry) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (e *ServiceResolverConfigEntry) GetKind() string { return e.Kind } func (e *ServiceResolverConfigEntry) GetKind() string { return e.Kind }
func (e *ServiceResolverConfigEntry) GetName() string { return e.Name } func (e *ServiceResolverConfigEntry) GetName() string { return e.Name }
func (e *ServiceResolverConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex } func (e *ServiceResolverConfigEntry) GetNamespace() string { return e.Namespace }
func (e *ServiceResolverConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex } func (e *ServiceResolverConfigEntry) GetMeta() map[string]string { return e.Meta }
func (e *ServiceResolverConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
func (e *ServiceResolverConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
type ServiceResolverSubset struct { type ServiceResolverSubset struct {
Filter string `json:",omitempty"` Filter string `json:",omitempty"`
@ -201,3 +214,76 @@ type ServiceResolverFailover struct {
Namespace string `json:",omitempty"` Namespace string `json:",omitempty"`
Datacenters []string `json:",omitempty"` Datacenters []string `json:",omitempty"`
} }
// LoadBalancer determines the load balancing policy and configuration for services
// issuing requests to this upstream service.
type LoadBalancer struct {
// Policy is the load balancing policy used to select a host
Policy string `json:",omitempty"`
// RingHashConfig contains configuration for the "ring_hash" policy type
RingHashConfig *RingHashConfig `json:",omitempty" alias:"ring_hash_config"`
// LeastRequestConfig contains configuration for the "least_request" policy type
LeastRequestConfig *LeastRequestConfig `json:",omitempty" alias:"least_request_config"`
// HashPolicies is a list of hash policies to use for hashing load balancing algorithms.
// Hash policies are evaluated individually and combined such that identical lists
// result in the same hash.
// If no hash policies are present, or none are successfully evaluated,
// then a random backend host will be selected.
HashPolicies []HashPolicy `json:",omitempty" alias:"hash_policies"`
}
// RingHashConfig contains configuration for the "ring_hash" policy type
type RingHashConfig struct {
// MinimumRingSize determines the minimum number of entries in the hash ring
MinimumRingSize uint64 `json:",omitempty" alias:"minimum_ring_size"`
// MaximumRingSize determines the maximum number of entries in the hash ring
MaximumRingSize uint64 `json:",omitempty" alias:"maximum_ring_size"`
}
// LeastRequestConfig contains configuration for the "least_request" policy type
type LeastRequestConfig struct {
// ChoiceCount determines the number of random healthy hosts from which to select the one with the least requests.
ChoiceCount uint32 `json:",omitempty" alias:"choice_count"`
}
// HashPolicy defines which attributes will be hashed by hash-based LB algorithms
type HashPolicy struct {
// Field is the attribute type to hash on.
// Must be one of "header","cookie", or "query_parameter".
// Cannot be specified along with SourceIP.
Field string `json:",omitempty"`
// FieldValue is the value to hash.
// ie. header name, cookie name, URL query parameter name
// Cannot be specified along with SourceIP.
FieldValue string `json:",omitempty" alias:"field_value"`
// CookieConfig contains configuration for the "cookie" hash policy type.
CookieConfig *CookieConfig `json:",omitempty" alias:"cookie_config"`
// SourceIP determines whether the hash should be of the source IP rather than of a field and field value.
// Cannot be specified along with Field or FieldValue.
SourceIP bool `json:",omitempty" alias:"source_ip"`
// Terminal will short circuit the computation of the hash when multiple hash policies are present.
// If a hash is computed when a Terminal policy is evaluated,
// then that hash will be used and subsequent hash policies will be ignored.
Terminal bool `json:",omitempty"`
}
// CookieConfig contains configuration for the "cookie" hash policy type.
// This is specified to have Envoy generate a cookie for a client on its first request.
type CookieConfig struct {
// Generates a session cookie with no expiration.
Session bool `json:",omitempty"`
// TTL for generated cookies. Cannot be specified for session cookies.
TTL time.Duration `json:",omitempty"`
// The path to set for the cookie
Path string `json:",omitempty"`
}

View file

@ -21,6 +21,8 @@ type IngressGatewayConfigEntry struct {
// what services to associated to those ports. // what services to associated to those ports.
Listeners []IngressListener Listeners []IngressListener
Meta map[string]string `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a // CreateIndex is the Raft index this entry was created at. This is a
// read-only field. // read-only field.
CreateIndex uint64 CreateIndex uint64
@ -44,7 +46,7 @@ type IngressListener struct {
// Protocol declares what type of traffic this listener is expected to // Protocol declares what type of traffic this listener is expected to
// receive. Depending on the protocol, a listener might support multiplexing // receive. Depending on the protocol, a listener might support multiplexing
// services over a single port, or additional discovery chain features. The // services over a single port, or additional discovery chain features. The
// current supported values are: (tcp | http). // current supported values are: (tcp | http | http2 | grpc).
Protocol string Protocol string
// Services declares the set of services to which the listener forwards // Services declares the set of services to which the listener forwards
@ -94,6 +96,14 @@ func (i *IngressGatewayConfigEntry) GetName() string {
return i.Name return i.Name
} }
func (i *IngressGatewayConfigEntry) GetNamespace() string {
return i.Namespace
}
func (i *IngressGatewayConfigEntry) GetMeta() map[string]string {
return i.Meta
}
func (i *IngressGatewayConfigEntry) GetCreateIndex() uint64 { func (i *IngressGatewayConfigEntry) GetCreateIndex() uint64 {
return i.CreateIndex return i.CreateIndex
} }
@ -115,6 +125,8 @@ type TerminatingGatewayConfigEntry struct {
// Services is a list of service names represented by the terminating gateway. // Services is a list of service names represented by the terminating gateway.
Services []LinkedService `json:",omitempty"` Services []LinkedService `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a // CreateIndex is the Raft index this entry was created at. This is a
// read-only field. // read-only field.
CreateIndex uint64 CreateIndex uint64
@ -161,6 +173,14 @@ func (g *TerminatingGatewayConfigEntry) GetName() string {
return g.Name return g.Name
} }
func (g *TerminatingGatewayConfigEntry) GetNamespace() string {
return g.Namespace
}
func (g *TerminatingGatewayConfigEntry) GetMeta() map[string]string {
return g.Meta
}
func (g *TerminatingGatewayConfigEntry) GetCreateIndex() uint64 { func (g *TerminatingGatewayConfigEntry) GetCreateIndex() uint64 {
return g.CreateIndex return g.CreateIndex
} }

View file

@ -0,0 +1,80 @@
package api
import "time"
type ServiceIntentionsConfigEntry struct {
Kind string
Name string
Namespace string `json:",omitempty"`
Sources []*SourceIntention
Meta map[string]string `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
}
type SourceIntention struct {
Name string
Namespace string `json:",omitempty"`
Action IntentionAction `json:",omitempty"`
Permissions []*IntentionPermission `json:",omitempty"`
Precedence int
Type IntentionSourceType
Description string `json:",omitempty"`
LegacyID string `json:",omitempty" alias:"legacy_id"`
LegacyMeta map[string]string `json:",omitempty" alias:"legacy_meta"`
LegacyCreateTime *time.Time `json:",omitempty" alias:"legacy_create_time"`
LegacyUpdateTime *time.Time `json:",omitempty" alias:"legacy_update_time"`
}
func (e *ServiceIntentionsConfigEntry) GetKind() string {
return e.Kind
}
func (e *ServiceIntentionsConfigEntry) GetName() string {
return e.Name
}
func (e *ServiceIntentionsConfigEntry) GetNamespace() string {
return e.Namespace
}
func (e *ServiceIntentionsConfigEntry) GetMeta() map[string]string {
return e.Meta
}
func (e *ServiceIntentionsConfigEntry) GetCreateIndex() uint64 {
return e.CreateIndex
}
func (e *ServiceIntentionsConfigEntry) GetModifyIndex() uint64 {
return e.ModifyIndex
}
type IntentionPermission struct {
Action IntentionAction
HTTP *IntentionHTTPPermission `json:",omitempty"`
}
type IntentionHTTPPermission struct {
PathExact string `json:",omitempty" alias:"path_exact"`
PathPrefix string `json:",omitempty" alias:"path_prefix"`
PathRegex string `json:",omitempty" alias:"path_regex"`
Header []IntentionHTTPHeaderPermission `json:",omitempty"`
Methods []string `json:",omitempty"`
}
type IntentionHTTPHeaderPermission struct {
Name string
Present bool `json:",omitempty"`
Exact string `json:",omitempty"`
Prefix string `json:",omitempty"`
Suffix string `json:",omitempty"`
Regex string `json:",omitempty"`
Invert bool `json:",omitempty"`
}

View file

@ -12,12 +12,12 @@ import (
// Connect. // Connect.
type Intention struct { type Intention struct {
// ID is the UUID-based ID for the intention, always generated by Consul. // ID is the UUID-based ID for the intention, always generated by Consul.
ID string ID string `json:",omitempty"`
// Description is a human-friendly description of this intention. // Description is a human-friendly description of this intention.
// It is opaque to Consul and is only stored and transferred in API // It is opaque to Consul and is only stored and transferred in API
// requests. // requests.
Description string Description string `json:",omitempty"`
// SourceNS, SourceName are the namespace and name, respectively, of // SourceNS, SourceName are the namespace and name, respectively, of
// the source service. Either of these may be the wildcard "*", but only // the source service. Either of these may be the wildcard "*", but only
@ -34,16 +34,25 @@ type Intention struct {
SourceType IntentionSourceType SourceType IntentionSourceType
// Action is whether this is an allowlist or denylist intention. // Action is whether this is an allowlist or denylist intention.
Action IntentionAction Action IntentionAction `json:",omitempty"`
// DefaultAddr, DefaultPort of the local listening proxy (if any) to // Permissions is the list of additional L7 attributes that extend the
// make this connection. // intention definition.
DefaultAddr string //
DefaultPort int // NOTE: This field is not editable unless editing the underlying
// service-intentions config entry directly.
Permissions []*IntentionPermission `json:",omitempty"`
// DefaultAddr is not used.
// Deprecated: DefaultAddr is not used and may be removed in a future version.
DefaultAddr string `json:",omitempty"`
// DefaultPort is not used.
// Deprecated: DefaultPort is not used and may be removed in a future version.
DefaultPort int `json:",omitempty"`
// Meta is arbitrary metadata associated with the intention. This is // Meta is arbitrary metadata associated with the intention. This is
// opaque to Consul but is served in API responses. // opaque to Consul but is served in API responses.
Meta map[string]string Meta map[string]string `json:",omitempty"`
// Precedence is the order that the intention will be applied, with // Precedence is the order that the intention will be applied, with
// larger numbers being applied first. This is a read-only field, on // larger numbers being applied first. This is a read-only field, on
@ -59,7 +68,7 @@ type Intention struct {
// This is needed mainly for replication purposes. When replicating from // This is needed mainly for replication purposes. When replicating from
// one DC to another keeping the content Hash will allow us to detect // one DC to another keeping the content Hash will allow us to detect
// content changes more efficiently than checking every single field // content changes more efficiently than checking every single field
Hash []byte Hash []byte `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
@ -67,10 +76,20 @@ type Intention struct {
// String returns human-friendly output describing ths intention. // String returns human-friendly output describing ths intention.
func (i *Intention) String() string { func (i *Intention) String() string {
var detail string
switch n := len(i.Permissions); n {
case 0:
detail = string(i.Action)
case 1:
detail = "1 permission"
default:
detail = fmt.Sprintf("%d permissions", len(i.Permissions))
}
return fmt.Sprintf("%s => %s (%s)", return fmt.Sprintf("%s => %s (%s)",
i.SourceString(), i.SourceString(),
i.DestinationString(), i.DestinationString(),
i.Action) detail)
} }
// SourceString returns the namespace/name format for the source, or // SourceString returns the namespace/name format for the source, or
@ -164,7 +183,42 @@ func (h *Connect) Intentions(q *QueryOptions) ([]*Intention, *QueryMeta, error)
return out, qm, nil return out, qm, nil
} }
// IntentionGetExact retrieves a single intention by its unique name instead of
// its ID.
func (h *Connect) IntentionGetExact(source, destination string, q *QueryOptions) (*Intention, *QueryMeta, error) {
r := h.c.newRequest("GET", "/v1/connect/intentions/exact")
r.setQueryOptions(q)
r.params.Set("source", source)
r.params.Set("destination", destination)
rtt, resp, err := h.c.doRequest(r)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if resp.StatusCode == 404 {
return nil, qm, nil
} else if resp.StatusCode != 200 {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
return nil, nil, fmt.Errorf(
"Unexpected response %d: %s", resp.StatusCode, buf.String())
}
var out Intention
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// IntentionGet retrieves a single intention. // IntentionGet retrieves a single intention.
//
// Deprecated: use IntentionGetExact instead
func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) { func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMeta, error) {
r := h.c.newRequest("GET", "/v1/connect/intentions/"+id) r := h.c.newRequest("GET", "/v1/connect/intentions/"+id)
r.setQueryOptions(q) r.setQueryOptions(q)
@ -194,7 +248,28 @@ func (h *Connect) IntentionGet(id string, q *QueryOptions) (*Intention, *QueryMe
return &out, qm, nil return &out, qm, nil
} }
// IntentionDeleteExact deletes a single intention by its unique name instead of its ID.
func (h *Connect) IntentionDeleteExact(source, destination string, q *WriteOptions) (*WriteMeta, error) {
r := h.c.newRequest("DELETE", "/v1/connect/intentions/exact")
r.setWriteOptions(q)
r.params.Set("source", source)
r.params.Set("destination", destination)
rtt, resp, err := requireOK(h.c.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
qm := &WriteMeta{}
qm.RequestTime = rtt
return qm, nil
}
// IntentionDelete deletes a single intention. // IntentionDelete deletes a single intention.
//
// Deprecated: use IntentionDeleteExact instead
func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) { func (h *Connect) IntentionDelete(id string, q *WriteOptions) (*WriteMeta, error) {
r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id) r := h.c.newRequest("DELETE", "/v1/connect/intentions/"+id)
r.setWriteOptions(q) r.setWriteOptions(q)
@ -268,9 +343,37 @@ func (h *Connect) IntentionCheck(args *IntentionCheck, q *QueryOptions) (bool, *
return out.Allowed, qm, nil return out.Allowed, qm, nil
} }
// IntentionUpsert will update an existing intention. The Source & Destination parameters
// in the structure must be non-empty. The ID must be empty.
func (c *Connect) IntentionUpsert(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
r := c.c.newRequest("PUT", "/v1/connect/intentions/exact")
r.setWriteOptions(q)
r.params.Set("source", maybePrefixNamespace(ixn.SourceNS, ixn.SourceName))
r.params.Set("destination", maybePrefixNamespace(ixn.DestinationNS, ixn.DestinationName))
r.obj = ixn
rtt, resp, err := requireOK(c.c.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{}
wm.RequestTime = rtt
return wm, nil
}
func maybePrefixNamespace(ns, name string) string {
if ns == "" {
return name
}
return ns + "/" + name
}
// IntentionCreate will create a new intention. The ID in the given // IntentionCreate will create a new intention. The ID in the given
// structure must be empty and a generate ID will be returned on // structure must be empty and a generate ID will be returned on
// success. // success.
//
// Deprecated: use IntentionUpsert instead
func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) { func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *WriteMeta, error) {
r := c.c.newRequest("POST", "/v1/connect/intentions") r := c.c.newRequest("POST", "/v1/connect/intentions")
r.setWriteOptions(q) r.setWriteOptions(q)
@ -293,6 +396,8 @@ func (c *Connect) IntentionCreate(ixn *Intention, q *WriteOptions) (string, *Wri
// IntentionUpdate will update an existing intention. The ID in the given // IntentionUpdate will update an existing intention. The ID in the given
// structure must be non-empty. // structure must be non-empty.
//
// Deprecated: use IntentionUpsert instead
func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) { func (c *Connect) IntentionUpdate(ixn *Intention, q *WriteOptions) (*WriteMeta, error) {
r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID) r := c.c.newRequest("PUT", "/v1/connect/intentions/"+ixn.ID)
r.setWriteOptions(q) r.setWriteOptions(q)

View file

@ -147,6 +147,9 @@ type DiscoveryGraphNode struct {
// fields for Type==resolver // fields for Type==resolver
Resolver *DiscoveryResolver Resolver *DiscoveryResolver
// shared by Type==resolver || Type==splitter
LoadBalancer *LoadBalancer `json:",omitempty"`
} }
// compiled form of ServiceRoute // compiled form of ServiceRoute

View file

@ -5,12 +5,12 @@ go 1.12
replace github.com/hashicorp/consul/sdk => ../sdk replace github.com/hashicorp/consul/sdk => ../sdk
require ( require (
github.com/hashicorp/consul/sdk v0.6.0 github.com/hashicorp/consul/sdk v0.7.0
github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-hclog v0.12.0 github.com/hashicorp/go-hclog v0.12.0
github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-rootcerts v1.0.2
github.com/hashicorp/go-uuid v1.0.1 github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/serf v0.9.3 github.com/hashicorp/serf v0.9.5
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.1.2
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
) )

View file

@ -13,8 +13,6 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/hashicorp/consul/sdk v0.6.0 h1:FfhMEkwvQl57CildXJyGHnwGGM4HMODGyfjGwNM1Vdw=
github.com/hashicorp/consul/sdk v0.6.0/go.mod h1:fY08Y9z5SvJqevyZNy6WWPXiG3KwBPAvlcdx16zZ0fM=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
@ -43,8 +41,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.3 h1:AVF6JDQQens6nMHT9OGERBvK0f8rPrAGILnsKLr6lzM= github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM=
github.com/hashicorp/serf v0.9.3/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

View file

@ -79,6 +79,7 @@ type LockOptions struct {
MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime
LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime
LockTryOnce bool // Optional, defaults to false which means try forever LockTryOnce bool // Optional, defaults to false which means try forever
LockDelay time.Duration // Optional, defaults to 15s
Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace
} }
@ -351,8 +352,9 @@ func (l *Lock) createSession() (string, error) {
se := l.opts.SessionOpts se := l.opts.SessionOpts
if se == nil { if se == nil {
se = &SessionEntry{ se = &SessionEntry{
Name: l.opts.SessionName, Name: l.opts.SessionName,
TTL: l.opts.SessionTTL, TTL: l.opts.SessionTTL,
LockDelay: l.opts.LockDelay,
} }
} }
w := WriteOptions{Namespace: l.opts.Namespace} w := WriteOptions{Namespace: l.opts.Namespace}

View file

@ -26,7 +26,7 @@ type Namespace struct {
// DeletedAt is the time when the Namespace was marked for deletion // DeletedAt is the time when the Namespace was marked for deletion
// This is nullable so that we can omit if empty when encoding in JSON // This is nullable so that we can omit if empty when encoding in JSON
DeletedAt *time.Time `json:"DeletedAt,omitempty"` DeletedAt *time.Time `json:"DeletedAt,omitempty" alias:"deleted_at"`
// CreateIndex is the Raft index at which the Namespace was created // CreateIndex is the Raft index at which the Namespace was created
CreateIndex uint64 `json:"CreateIndex,omitempty"` CreateIndex uint64 `json:"CreateIndex,omitempty"`
@ -39,10 +39,10 @@ type Namespace struct {
type NamespaceACLConfig struct { type NamespaceACLConfig struct {
// PolicyDefaults is the list of policies that should be used for the parent authorizer // PolicyDefaults is the list of policies that should be used for the parent authorizer
// of all tokens in the associated namespace. // of all tokens in the associated namespace.
PolicyDefaults []ACLLink `json:"PolicyDefaults"` PolicyDefaults []ACLLink `json:"PolicyDefaults" alias:"policy_defaults"`
// RoleDefaults is the list of roles that should be used for the parent authorizer // RoleDefaults is the list of roles that should be used for the parent authorizer
// of all tokens in the associated namespace. // of all tokens in the associated namespace.
RoleDefaults []ACLLink `json:"RoleDefaults"` RoleDefaults []ACLLink `json:"RoleDefaults" alias:"role_defaults"`
} }
// Namespaces can be used to manage Namespaces in Consul Enterprise.. // Namespaces can be used to manage Namespaces in Consul Enterprise..

View file

@ -111,6 +111,122 @@ type OperatorHealthReply struct {
Servers []ServerHealth Servers []ServerHealth
} }
type AutopilotState struct {
Healthy bool
FailureTolerance int
OptimisticFailureTolerance int
Servers map[string]AutopilotServer
Leader string
Voters []string
ReadReplicas []string `json:",omitempty"`
RedundancyZones map[string]AutopilotZone `json:",omitempty"`
Upgrade *AutopilotUpgrade `json:",omitempty"`
}
type AutopilotServer struct {
ID string
Name string
Address string
NodeStatus string
Version string
LastContact *ReadableDuration
LastTerm uint64
LastIndex uint64
Healthy bool
StableSince time.Time
RedundancyZone string `json:",omitempty"`
UpgradeVersion string `json:",omitempty"`
ReadReplica bool
Status AutopilotServerStatus
Meta map[string]string
NodeType AutopilotServerType
}
type AutopilotServerStatus string
const (
AutopilotServerNone AutopilotServerStatus = "none"
AutopilotServerLeader AutopilotServerStatus = "leader"
AutopilotServerVoter AutopilotServerStatus = "voter"
AutopilotServerNonVoter AutopilotServerStatus = "non-voter"
AutopilotServerStaging AutopilotServerStatus = "staging"
)
type AutopilotServerType string
const (
AutopilotTypeVoter AutopilotServerType = "voter"
AutopilotTypeReadReplica AutopilotServerType = "read-replica"
AutopilotTypeZoneVoter AutopilotServerType = "zone-voter"
AutopilotTypeZoneExtraVoter AutopilotServerType = "zone-extra-voter"
AutopilotTypeZoneStandby AutopilotServerType = "zone-standby"
)
type AutopilotZone struct {
Servers []string
Voters []string
FailureTolerance int
}
type AutopilotZoneUpgradeVersions struct {
TargetVersionVoters []string `json:",omitempty"`
TargetVersionNonVoters []string `json:",omitempty"`
OtherVersionVoters []string `json:",omitempty"`
OtherVersionNonVoters []string `json:",omitempty"`
}
type AutopilotUpgrade struct {
Status AutopilotUpgradeStatus
TargetVersion string `json:",omitempty"`
TargetVersionVoters []string `json:",omitempty"`
TargetVersionNonVoters []string `json:",omitempty"`
TargetVersionReadReplicas []string `json:",omitempty"`
OtherVersionVoters []string `json:",omitempty"`
OtherVersionNonVoters []string `json:",omitempty"`
OtherVersionReadReplicas []string `json:",omitempty"`
RedundancyZones map[string]AutopilotZoneUpgradeVersions `json:",omitempty"`
}
type AutopilotUpgradeStatus string
const (
// AutopilotUpgradeIdle is the status when no upgrade is in progress.
AutopilotUpgradeIdle AutopilotUpgradeStatus = "idle"
// AutopilotUpgradeAwaitNewVoters is the status when more servers of
// the target version must be added in order to start the promotion
// phase of the upgrade
AutopilotUpgradeAwaitNewVoters AutopilotUpgradeStatus = "await-new-voters"
// AutopilotUpgradePromoting is the status when autopilot is promoting
// servers of the target version.
AutopilotUpgradePromoting AutopilotUpgradeStatus = "promoting"
// AutopilotUpgradeDemoting is the status when autopilot is demoting
// servers not on the target version
AutopilotUpgradeDemoting AutopilotUpgradeStatus = "demoting"
// AutopilotUpgradeLeaderTransfer is the status when autopilot is transferring
// leadership from a server running an older version to a server
// using the target version.
AutopilotUpgradeLeaderTransfer AutopilotUpgradeStatus = "leader-transfer"
// AutopilotUpgradeAwaitNewServers is the status when autpilot has finished
// transferring leadership and has demoted all the other versioned
// servers but wants to indicate that more target version servers
// are needed to replace all the existing other version servers.
AutopilotUpgradeAwaitNewServers AutopilotUpgradeStatus = "await-new-servers"
// AutopilotUpgradeAwaitServerRemoval is the status when autopilot is waiting
// for the servers on non-target versions to be removed
AutopilotUpgradeAwaitServerRemoval AutopilotUpgradeStatus = "await-server-removal"
// AutopilotUpgradeDisabled is the status when automated ugprades are
// disabled in the autopilot configuration
AutopilotUpgradeDisabled AutopilotUpgradeStatus = "disabled"
)
// ReadableDuration is a duration type that is serialized to JSON in human readable format. // ReadableDuration is a duration type that is serialized to JSON in human readable format.
type ReadableDuration time.Duration type ReadableDuration time.Duration
@ -230,3 +346,20 @@ func (op *Operator) AutopilotServerHealth(q *QueryOptions) (*OperatorHealthReply
} }
return &out, nil return &out, nil
} }
func (op *Operator) AutopilotState(q *QueryOptions) (*AutopilotState, error) {
r := op.c.newRequest("GET", "/v1/operator/autopilot/state")
r.setQueryOptions(q)
_, resp, err := requireOK(op.c.doRequest(r))
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out AutopilotState
if err := decodeBody(resp, &out); err != nil {
return nil, err
}
return &out, nil
}

View file

@ -22,6 +22,9 @@ type KeyringResponse struct {
// A map of the encryption keys to the number of nodes they're installed on // A map of the encryption keys to the number of nodes they're installed on
Keys map[string]int Keys map[string]int
// A map of the encryption primary keys to the number of nodes they're installed on
PrimaryKeys map[string]int
// The total number of nodes in this ring // The total number of nodes in this ring
NumNodes int NumNodes int
} }

View file

@ -11,8 +11,13 @@ func (c *Client) Status() *Status {
} }
// Leader is used to query for a known leader // Leader is used to query for a known leader
func (s *Status) Leader() (string, error) { func (s *Status) LeaderWithQueryOptions(q *QueryOptions) (string, error) {
r := s.c.newRequest("GET", "/v1/status/leader") r := s.c.newRequest("GET", "/v1/status/leader")
if q != nil {
r.setQueryOptions(q)
}
_, resp, err := requireOK(s.c.doRequest(r)) _, resp, err := requireOK(s.c.doRequest(r))
if err != nil { if err != nil {
return "", err return "", err
@ -26,9 +31,18 @@ func (s *Status) Leader() (string, error) {
return leader, nil return leader, nil
} }
func (s *Status) Leader() (string, error) {
return s.LeaderWithQueryOptions(nil)
}
// Peers is used to query for a known raft peers // Peers is used to query for a known raft peers
func (s *Status) Peers() ([]string, error) { func (s *Status) PeersWithQueryOptions(q *QueryOptions) ([]string, error) {
r := s.c.newRequest("GET", "/v1/status/peers") r := s.c.newRequest("GET", "/v1/status/peers")
if q != nil {
r.setQueryOptions(q)
}
_, resp, err := requireOK(s.c.doRequest(r)) _, resp, err := requireOK(s.c.doRequest(r))
if err != nil { if err != nil {
return nil, err return nil, err
@ -41,3 +55,7 @@ func (s *Status) Peers() ([]string, error) {
} }
return peers, nil return peers, nil
} }
func (s *Status) Peers() ([]string, error) {
return s.PeersWithQueryOptions(nil)
}

View file

@ -29,40 +29,54 @@ func init() {
} }
} }
// TempDir creates a temporary directory within tmpdir var noCleanup = strings.ToLower(os.Getenv("TEST_NOCLEANUP")) == "true"
// with the name 'testname-name'. If the directory cannot
// be created t.Fatal is called. // TempDir creates a temporary directory within tmpdir with the name 'testname-name'.
// If the directory cannot be created t.Fatal is called.
// The directory will be removed when the test ends. Set TEST_NOCLEANUP env var
// to prevent the directory from being removed.
func TempDir(t *testing.T, name string) string { func TempDir(t *testing.T, name string) string {
if t != nil && t.Name() != "" { if t == nil {
name = t.Name() + "-" + name panic("argument t must be non-nil")
} }
name = t.Name() + "-" + name
name = strings.Replace(name, "/", "_", -1) name = strings.Replace(name, "/", "_", -1)
d, err := ioutil.TempDir(tmpdir, name) d, err := ioutil.TempDir(tmpdir, name)
if err != nil { if err != nil {
if t == nil {
panic(err)
}
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
t.Cleanup(func() {
if noCleanup {
t.Logf("skipping cleanup because TEST_NOCLEANUP was enabled")
return
}
os.RemoveAll(d)
})
return d return d
} }
// TempFile creates a temporary file within tmpdir // TempFile creates a temporary file within tmpdir with the name 'testname-name'.
// with the name 'testname-name'. If the file cannot // If the file cannot be created t.Fatal is called. If a temporary directory
// be created t.Fatal is called. If a temporary directory // has been created before consider storing the file inside this directory to
// has been created before consider storing the file // avoid double cleanup.
// inside this directory to avoid double cleanup. // The file will be removed when the test ends. Set TEST_NOCLEANUP env var
// to prevent the file from being removed.
func TempFile(t *testing.T, name string) *os.File { func TempFile(t *testing.T, name string) *os.File {
if t != nil && t.Name() != "" { if t == nil {
name = t.Name() + "-" + name panic("argument t must be non-nil")
} }
name = t.Name() + "-" + name
name = strings.Replace(name, "/", "_", -1) name = strings.Replace(name, "/", "_", -1)
f, err := ioutil.TempFile(tmpdir, name) f, err := ioutil.TempFile(tmpdir, name)
if err != nil { if err != nil {
if t == nil {
panic(err)
}
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
t.Cleanup(func() {
if noCleanup {
t.Logf("skipping cleanup because TEST_NOCLEANUP was enabled")
return
}
os.Remove(f.Name())
})
return f return f
} }

View file

@ -23,6 +23,8 @@ import (
// Failer is an interface compatible with testing.T. // Failer is an interface compatible with testing.T.
type Failer interface { type Failer interface {
Helper()
// Log is called for the final test output // Log is called for the final test output
Log(args ...interface{}) Log(args ...interface{})
@ -116,6 +118,7 @@ func dedup(a []string) string {
func run(r Retryer, t Failer, f func(r *R)) { func run(r Retryer, t Failer, f func(r *R)) {
rr := &R{} rr := &R{}
fail := func() { fail := func() {
t.Helper()
out := dedup(rr.output) out := dedup(rr.output)
if out != "" { if out != "" {
t.Log(out) t.Log(out)

View file

@ -25,6 +25,7 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@ -133,7 +134,7 @@ type ServerConfigCallback func(c *TestServerConfig)
// defaultServerConfig returns a new TestServerConfig struct // defaultServerConfig returns a new TestServerConfig struct
// with all of the listen ports incremented by one. // with all of the listen ports incremented by one.
func defaultServerConfig(t CleanupT) *TestServerConfig { func defaultServerConfig(t TestingTB) *TestServerConfig {
nodeID, err := uuid.GenerateUUID() nodeID, err := uuid.GenerateUUID()
if err != nil { if err != nil {
panic(err) panic(err)
@ -215,11 +216,11 @@ type TestServer struct {
tmpdir string tmpdir string
} }
// NewTestServerConfig creates a new TestServer, and makes a call to an optional // NewTestServerConfigT creates a new TestServer, and makes a call to an optional
// callback function to modify the configuration. If there is an error // callback function to modify the configuration. If there is an error
// configuring or starting the server, the server will NOT be running when the // configuring or starting the server, the server will NOT be running when the
// function returns (thus you do not need to stop it). // function returns (thus you do not need to stop it).
func NewTestServerConfigT(t testing.TB, cb ServerConfigCallback) (*TestServer, error) { func NewTestServerConfigT(t TestingTB, cb ServerConfigCallback) (*TestServer, error) {
path, err := exec.LookPath("consul") path, err := exec.LookPath("consul")
if err != nil || path == "" { if err != nil || path == "" {
return nil, fmt.Errorf("consul not found on $PATH - download and install " + return nil, fmt.Errorf("consul not found on $PATH - download and install " +
@ -328,9 +329,22 @@ func (s *TestServer) Stop() error {
} }
} }
waitDone := make(chan error)
go func() {
waitDone <- s.cmd.Wait()
close(waitDone)
}()
// wait for the process to exit to be sure that the data dir can be // wait for the process to exit to be sure that the data dir can be
// deleted on all platforms. // deleted on all platforms.
return s.cmd.Wait() select {
case err := <-waitDone:
return err
case <-time.After(10 * time.Second):
s.cmd.Process.Signal(syscall.SIGABRT)
s.cmd.Wait()
return fmt.Errorf("timeout waiting for server to stop gracefully")
}
} }
// waitForAPI waits for the /status/leader HTTP endpoint to start // waitForAPI waits for the /status/leader HTTP endpoint to start
@ -351,11 +365,12 @@ func (s *TestServer) waitForAPI() error {
time.Sleep(timer.Wait) time.Sleep(timer.Wait)
url := s.url("/v1/status/leader") url := s.url("/v1/status/leader")
_, err := s.masterGet(url) resp, err := s.masterGet(url)
if err != nil { if err != nil {
failed = true failed = true
continue continue
} }
resp.Body.Close()
failed = false failed = false
} }
@ -378,7 +393,7 @@ func (s *TestServer) WaitForLeader(t *testing.T) {
} }
defer resp.Body.Close() defer resp.Body.Close()
if err := s.requireOK(resp); err != nil { if err := s.requireOK(resp); err != nil {
r.Fatal("failed OK response", err) r.Fatalf("failed OK response: %v", err)
} }
// Ensure we have a leader and a node registration. // Ensure we have a leader and a node registration.
@ -387,7 +402,7 @@ func (s *TestServer) WaitForLeader(t *testing.T) {
} }
index, err := strconv.ParseInt(resp.Header.Get("X-Consul-Index"), 10, 64) index, err := strconv.ParseInt(resp.Header.Get("X-Consul-Index"), 10, 64)
if err != nil { if err != nil {
r.Fatal("bad consul index", err) r.Fatalf("bad consul index: %v", err)
} }
if index < 2 { if index < 2 {
r.Fatal("consul index should be at least 2") r.Fatal("consul index should be at least 2")
@ -418,7 +433,7 @@ func (s *TestServer) WaitForActiveCARoot(t *testing.T) {
// since this is used in both `api` and consul test or duplication. The 200 // since this is used in both `api` and consul test or duplication. The 200
// is all we really need to wait for. // is all we really need to wait for.
if err := s.requireOK(resp); err != nil { if err := s.requireOK(resp); err != nil {
r.Fatal("failed OK response", err) r.Fatalf("failed OK response: %v", err)
} }
var roots rootsResponse var roots rootsResponse
@ -434,6 +449,27 @@ func (s *TestServer) WaitForActiveCARoot(t *testing.T) {
}) })
} }
// WaitForServiceIntentions waits until the server can accept config entry
// kinds of service-intentions meaning any migration bootstrapping from pre-1.9
// intentions has completed.
func (s *TestServer) WaitForServiceIntentions(t *testing.T) {
const fakeConfigName = "Sa4ohw5raith4si0Ohwuqu3lowiethoh"
retry.Run(t, func(r *retry.R) {
// Try to delete a non-existent service-intentions config entry. The
// preflightCheck call in agent/consul/config_endpoint.go will fail if
// we aren't ready yet, vs just doing no work instead.
url := s.url("/v1/config/service-intentions/" + fakeConfigName)
resp, err := s.masterDelete(url)
if err != nil {
r.Fatalf("failed http get '%s': %v", url, err)
}
defer resp.Body.Close()
if err := s.requireOK(resp); err != nil {
r.Fatalf("failed OK response: %v", err)
}
})
}
// WaitForSerfCheck ensures we have a node with serfHealth check registered // WaitForSerfCheck ensures we have a node with serfHealth check registered
// Behavior mirrors testrpc.WaitForTestAgent but avoids the dependency cycle in api pkg // Behavior mirrors testrpc.WaitForTestAgent but avoids the dependency cycle in api pkg
func (s *TestServer) WaitForSerfCheck(t *testing.T) { func (s *TestServer) WaitForSerfCheck(t *testing.T) {
@ -442,11 +478,11 @@ func (s *TestServer) WaitForSerfCheck(t *testing.T) {
url := s.url("/v1/catalog/nodes?index=0") url := s.url("/v1/catalog/nodes?index=0")
resp, err := s.masterGet(url) resp, err := s.masterGet(url)
if err != nil { if err != nil {
r.Fatal("failed http get", err) r.Fatalf("failed http get: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if err := s.requireOK(resp); err != nil { if err := s.requireOK(resp); err != nil {
r.Fatal("failed OK response", err) r.Fatalf("failed OK response: %v", err)
} }
// Watch for the anti-entropy sync to finish. // Watch for the anti-entropy sync to finish.
@ -463,11 +499,11 @@ func (s *TestServer) WaitForSerfCheck(t *testing.T) {
url = s.url(fmt.Sprintf("/v1/health/node/%s", payload[0]["Node"])) url = s.url(fmt.Sprintf("/v1/health/node/%s", payload[0]["Node"]))
resp, err = s.masterGet(url) resp, err = s.masterGet(url)
if err != nil { if err != nil {
r.Fatal("failed http get", err) r.Fatalf("failed http get: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if err := s.requireOK(resp); err != nil { if err := s.requireOK(resp); err != nil {
r.Fatal("failed OK response", err) r.Fatalf("failed OK response: %v", err)
} }
dec = json.NewDecoder(resp.Body) dec = json.NewDecoder(resp.Body)
if err = dec.Decode(&payload); err != nil { if err = dec.Decode(&payload); err != nil {
@ -497,3 +533,14 @@ func (s *TestServer) masterGet(url string) (*http.Response, error) {
} }
return s.HTTPClient.Do(req) return s.HTTPClient.Do(req)
} }
func (s *TestServer) masterDelete(url string) (*http.Response, error) {
req, err := http.NewRequest("DELETE", url, nil)
if err != nil {
return nil, err
}
if s.Config.ACL.Tokens.Master != "" {
req.Header.Set("x-consul-token", s.Config.ACL.Tokens.Master)
}
return s.HTTPClient.Do(req)
}

View file

@ -10,11 +10,11 @@ import (
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
) )
func Logger(t testing.TB) hclog.InterceptLogger { func Logger(t TestingTB) hclog.InterceptLogger {
return LoggerWithOutput(t, NewLogBuffer(t)) return LoggerWithOutput(t, NewLogBuffer(t))
} }
func LoggerWithOutput(t testing.TB, output io.Writer) hclog.InterceptLogger { func LoggerWithOutput(t TestingTB, output io.Writer) hclog.InterceptLogger {
return hclog.NewInterceptLogger(&hclog.LoggerOptions{ return hclog.NewInterceptLogger(&hclog.LoggerOptions{
Name: t.Name(), Name: t.Name(),
Level: hclog.Trace, Level: hclog.Trace,
@ -25,18 +25,18 @@ func LoggerWithOutput(t testing.TB, output io.Writer) hclog.InterceptLogger {
var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1" var sendTestLogsToStdout = os.Getenv("NOLOGBUFFER") == "1"
// NewLogBuffer returns an io.Writer which buffers all writes. When the test // NewLogBuffer returns an io.Writer which buffers all writes. When the test
// ends, t.Failed is checked. If the test has failed all log output is printed // ends, t.Failed is checked. If the test has failed or has been run in verbose
// to stdout. // mode all log output is printed to stdout.
// //
// Set the env var NOLOGBUFFER=1 to disable buffering, resulting in all log // Set the env var NOLOGBUFFER=1 to disable buffering, resulting in all log
// output being written immediately to stdout. // output being written immediately to stdout.
func NewLogBuffer(t CleanupT) io.Writer { func NewLogBuffer(t TestingTB) io.Writer {
if sendTestLogsToStdout { if sendTestLogsToStdout {
return os.Stdout return os.Stdout
} }
buf := &logBuffer{buf: new(bytes.Buffer)} buf := &logBuffer{buf: new(bytes.Buffer)}
t.Cleanup(func() { t.Cleanup(func() {
if t.Failed() { if t.Failed() || testing.Verbose() {
buf.Lock() buf.Lock()
defer buf.Unlock() defer buf.Unlock()
buf.buf.WriteTo(os.Stdout) buf.buf.WriteTo(os.Stdout)
@ -45,11 +45,6 @@ func NewLogBuffer(t CleanupT) io.Writer {
return buf return buf
} }
type CleanupT interface {
Cleanup(f func())
Failed() bool
}
type logBuffer struct { type logBuffer struct {
buf *bytes.Buffer buf *bytes.Buffer
sync.Mutex sync.Mutex

View file

@ -0,0 +1,11 @@
package testutil
// TestingTB is an interface that describes the implementation of the testing object.
// Using an interface that describes testing.TB instead of the actual implementation
// makes testutil usable in a wider variety of contexts (e.g. use with ginkgo : https://godoc.org/github.com/onsi/ginkgo#GinkgoT)
type TestingTB interface {
Cleanup(func())
Failed() bool
Logf(format string, args ...interface{})
Name() string
}

View file

@ -11,7 +11,7 @@ import (
// given config. // given config.
func GenerateClients(nodes int, config *Config) ([]*Client, error) { func GenerateClients(nodes int, config *Config) ([]*Client, error) {
clients := make([]*Client, nodes) clients := make([]*Client, nodes)
for i, _ := range clients { for i := range clients {
client, err := NewClient(config) client, err := NewClient(config)
if err != nil { if err != nil {
return nil, err return nil, err
@ -146,7 +146,7 @@ func Simulate(clients []*Client, truth [][]time.Duration, cycles int) {
nodes := len(clients) nodes := len(clients)
for cycle := 0; cycle < cycles; cycle++ { for cycle := 0; cycle < cycles; cycle++ {
for i, _ := range clients { for i := range clients {
if j := rand.Intn(nodes); j != i { if j := rand.Intn(nodes); j != i {
c := clients[j].GetCoordinate() c := clients[j].GetCoordinate()
rtt := truth[i][j] rtt := truth[i][j]

View file

@ -253,6 +253,15 @@ type Config struct {
// //
// WARNING: this should ONLY be used in tests // WARNING: this should ONLY be used in tests
messageDropper func(typ messageType) bool messageDropper func(typ messageType) bool
// ReconnectTimeoutOverride is an optional interface which when present allows
// the application to cause reaping of a node to happen when it otherwise wouldn't
ReconnectTimeoutOverride ReconnectTimeoutOverrider
// ValidateNodeNames controls whether nodenames only
// contain alphanumeric, dashes and '.'characters
// and sets maximum length to 128 characters
ValidateNodeNames bool
} }
// Init allocates the subdata structures // Init allocates the subdata structures
@ -298,6 +307,7 @@ func DefaultConfig() *Config {
QuerySizeLimit: 1024, QuerySizeLimit: 1024,
EnableNameConflictResolution: true, EnableNameConflictResolution: true,
DisableCoordinates: false, DisableCoordinates: false,
ValidateNodeNames: false,
UserEventSizeLimit: 512, UserEventSizeLimit: 512,
} }
} }

View file

@ -64,6 +64,9 @@ type nodeKeyResponse struct {
// Keys is used in listing queries to relay a list of installed keys // Keys is used in listing queries to relay a list of installed keys
Keys []string Keys []string
// PrimaryKey is used in listing queries to relay the primary key
PrimaryKey string
} }
// newSerfQueries is used to create a new serfQueries. We return an event // newSerfQueries is used to create a new serfQueries. We return an event
@ -346,7 +349,7 @@ SEND:
func (s *serfQueries) handleListKeys(q *Query) { func (s *serfQueries) handleListKeys(q *Query) {
response := nodeKeyResponse{Result: false} response := nodeKeyResponse{Result: false}
keyring := s.serf.config.MemberlistConfig.Keyring keyring := s.serf.config.MemberlistConfig.Keyring
var primaryKeyBytes []byte
if !s.serf.EncryptionEnabled() { if !s.serf.EncryptionEnabled() {
response.Message = "Keyring is empty (encryption not enabled)" response.Message = "Keyring is empty (encryption not enabled)"
s.logger.Printf("[ERR] serf: Keyring is empty (encryption not enabled)") s.logger.Printf("[ERR] serf: Keyring is empty (encryption not enabled)")
@ -360,6 +363,9 @@ func (s *serfQueries) handleListKeys(q *Query) {
key := base64.StdEncoding.EncodeToString(keyBytes) key := base64.StdEncoding.EncodeToString(keyBytes)
response.Keys = append(response.Keys, key) response.Keys = append(response.Keys, key)
} }
primaryKeyBytes = keyring.GetPrimaryKey()
response.PrimaryKey = base64.StdEncoding.EncodeToString(primaryKeyBytes)
response.Result = true response.Result = true
SEND: SEND:

View file

@ -31,6 +31,10 @@ type KeyResponse struct {
// Keys is a mapping of the base64-encoded value of the key bytes to the // Keys is a mapping of the base64-encoded value of the key bytes to the
// number of nodes that have the key installed. // number of nodes that have the key installed.
Keys map[string]int Keys map[string]int
// PrimaryKeys is a mapping of the base64-encoded value of the primary
// key bytes to the number of nodes that have the key installed.
PrimaryKeys map[string]int
} }
// KeyRequestOptions is used to contain optional parameters for a keyring operation // KeyRequestOptions is used to contain optional parameters for a keyring operation
@ -76,13 +80,11 @@ func (k *KeyManager) streamKeyResp(resp *KeyResponse, ch <-chan NodeResponse) {
// Currently only used for key list queries, this adds keys to a counter // Currently only used for key list queries, this adds keys to a counter
// and increments them for each node response which contains them. // and increments them for each node response which contains them.
for _, key := range nodeResponse.Keys { for _, key := range nodeResponse.Keys {
if _, ok := resp.Keys[key]; !ok { resp.Keys[key]++
resp.Keys[key] = 1
} else {
resp.Keys[key]++
}
} }
resp.PrimaryKeys[nodeResponse.PrimaryKey]++
NEXT: NEXT:
// Return early if all nodes have responded. This allows us to avoid // Return early if all nodes have responded. This allows us to avoid
// waiting for the full timeout when there is nothing left to do. // waiting for the full timeout when there is nothing left to do.
@ -97,8 +99,9 @@ func (k *KeyManager) streamKeyResp(resp *KeyResponse, ch <-chan NodeResponse) {
// KeyResponse for uniform response handling. // KeyResponse for uniform response handling.
func (k *KeyManager) handleKeyRequest(key, query string, opts *KeyRequestOptions) (*KeyResponse, error) { func (k *KeyManager) handleKeyRequest(key, query string, opts *KeyRequestOptions) (*KeyResponse, error) {
resp := &KeyResponse{ resp := &KeyResponse{
Messages: make(map[string]string), Messages: make(map[string]string),
Keys: make(map[string]int), Keys: make(map[string]int),
PrimaryKeys: make(map[string]int),
} }
qName := internalQueryName(query) qName := internalQueryName(query)

View file

@ -1,6 +1,7 @@
package serf package serf
import ( import (
"fmt"
"net" "net"
"github.com/hashicorp/memberlist" "github.com/hashicorp/memberlist"
@ -17,22 +18,31 @@ type mergeDelegate struct {
func (m *mergeDelegate) NotifyMerge(nodes []*memberlist.Node) error { func (m *mergeDelegate) NotifyMerge(nodes []*memberlist.Node) error {
members := make([]*Member, len(nodes)) members := make([]*Member, len(nodes))
for idx, n := range nodes { for idx, n := range nodes {
members[idx] = m.nodeToMember(n) var err error
members[idx], err = m.nodeToMember(n)
if err != nil {
return err
}
} }
return m.serf.config.Merge.NotifyMerge(members) return m.serf.config.Merge.NotifyMerge(members)
} }
func (m *mergeDelegate) NotifyAlive(peer *memberlist.Node) error { func (m *mergeDelegate) NotifyAlive(peer *memberlist.Node) error {
member := m.nodeToMember(peer) member, err := m.nodeToMember(peer)
if err != nil {
return err
}
return m.serf.config.Merge.NotifyMerge([]*Member{member}) return m.serf.config.Merge.NotifyMerge([]*Member{member})
} }
func (m *mergeDelegate) nodeToMember(n *memberlist.Node) *Member { func (m *mergeDelegate) nodeToMember(n *memberlist.Node) (*Member, error) {
status := StatusNone status := StatusNone
if n.State == memberlist.StateLeft { if n.State == memberlist.StateLeft {
status = StatusLeft status = StatusLeft
} }
if err := m.validateMemberInfo(n); err != nil {
return nil, err
}
return &Member{ return &Member{
Name: n.Name, Name: n.Name,
Addr: net.IP(n.Addr), Addr: net.IP(n.Addr),
@ -45,5 +55,22 @@ func (m *mergeDelegate) nodeToMember(n *memberlist.Node) *Member {
DelegateMin: n.DMin, DelegateMin: n.DMin,
DelegateMax: n.DMax, DelegateMax: n.DMax,
DelegateCur: n.DCur, DelegateCur: n.DCur,
} }, nil
}
// validateMemberInfo checks that the data we are sending is valid
func (m *mergeDelegate) validateMemberInfo(n *memberlist.Node) error {
if err := m.serf.validateNodeName(n.Name); err != nil {
return err
}
if len(n.Addr) != 4 && len(n.Addr) != 16 {
return fmt.Errorf("IP byte length is invalid: %d bytes is not either 4 or 16", len(n.Addr))
}
if len(n.Meta) > memberlist.MetaMaxSize {
return fmt.Errorf("Encoded length of tags exceeds limit of %d bytes",
memberlist.MetaMaxSize)
}
return nil
} }

View file

@ -11,6 +11,7 @@ import (
"math/rand" "math/rand"
"net" "net"
"os" "os"
"regexp"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -36,6 +37,8 @@ const (
tagMagicByte uint8 = 255 tagMagicByte uint8 = 255
) )
const MaxNodeNameLength int = 128
var ( var (
// FeatureNotSupported is returned if a feature cannot be used // FeatureNotSupported is returned if a feature cannot be used
// due to an older protocol version being used. // due to an older protocol version being used.
@ -47,6 +50,12 @@ func init() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
} }
// ReconnectTimeoutOverrider is an interface that can be implemented to allow overriding
// the reconnect timeout for individual members.
type ReconnectTimeoutOverrider interface {
ReconnectTimeout(member *Member, timeout time.Duration) time.Duration
}
// Serf is a single node that is part of a single cluster that gets // Serf is a single node that is part of a single cluster that gets
// events about joins/leaves/failures/etc. It is created with the Create // events about joins/leaves/failures/etc. It is created with the Create
// method. // method.
@ -269,6 +278,9 @@ func Create(conf *Config) (*Serf, error) {
if len(serf.encodeTags(conf.Tags)) > memberlist.MetaMaxSize { if len(serf.encodeTags(conf.Tags)) > memberlist.MetaMaxSize {
return nil, fmt.Errorf("Encoded length of tags exceeds limit of %d bytes", memberlist.MetaMaxSize) return nil, fmt.Errorf("Encoded length of tags exceeds limit of %d bytes", memberlist.MetaMaxSize)
} }
if err := serf.ValidateNodeNames(); err != nil {
return nil, err
}
// Check if serf member event coalescing is enabled // Check if serf member event coalescing is enabled
if conf.CoalescePeriod > 0 && conf.QuiescentPeriod > 0 && conf.EventCh != nil { if conf.CoalescePeriod > 0 && conf.QuiescentPeriod > 0 && conf.EventCh != nil {
@ -1102,8 +1114,21 @@ func (s *Serf) handleNodeLeaveIntent(leaveMsg *messageLeave) bool {
return false return false
} }
// Always set the lamport time so that if we retransmit below it won't echo // Always update the lamport time even when the status does not change
// around forever! // (despite the variable naming implying otherwise).
//
// By updating this statusLTime here we ensure that the earlier conditional
// on "leaveMsg.LTime <= member.statusLTime" will prevent an infinite
// rebroadcast when seeing two successive leave message for the same
// member. Without this fix a leave message that arrives after a member is
// already marked as leaving/left will cause it to be rebroadcast without
// marking it locally as witnessed. If more than one serf instance in the
// cluster experiences this series of events then they will rebroadcast
// each other's messages about the affected node indefinitely.
//
// This eventually leads to overflowing serf intent queues
// - https://github.com/hashicorp/consul/issues/8179
// - https://github.com/hashicorp/consul/issues/7960
member.statusLTime = leaveMsg.LTime member.statusLTime = leaveMsg.LTime
// State transition depends on current state // State transition depends on current state
@ -1558,8 +1583,13 @@ func (s *Serf) reap(old []*memberState, now time.Time, timeout time.Duration) []
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
m := old[i] m := old[i]
memberTimeout := timeout
if s.config.ReconnectTimeoutOverride != nil {
memberTimeout = s.config.ReconnectTimeoutOverride.ReconnectTimeout(&m.Member, memberTimeout)
}
// Skip if the timeout is not yet reached // Skip if the timeout is not yet reached
if now.Sub(m.leaveTime) <= timeout { if now.Sub(m.leaveTime) <= memberTimeout {
continue continue
} }
@ -1871,3 +1901,24 @@ func (s *Serf) NumNodes() (numNodes int) {
return numNodes return numNodes
} }
// ValidateNodeNames verifies the NodeName contains
// only alphanumeric, -, or . and is under 128 chracters
func (s *Serf) ValidateNodeNames() error {
return s.validateNodeName(s.config.NodeName)
}
func (s *Serf) validateNodeName(name string) error {
if s.config.ValidateNodeNames {
var InvalidNameRe = regexp.MustCompile(`[^A-Za-z0-9\-\.]+`)
if InvalidNameRe.MatchString(name) {
return fmt.Errorf("Node name contains invalid characters %v , Valid characters include "+
"all alpha-numerics and dashes and '.' ", name)
}
if len(name) > MaxNodeNameLength {
return fmt.Errorf("Node name is %v characters. "+
"Valid length is between 1 and 128 characters", len(name))
}
}
return nil
}

6
vendor/modules.txt vendored
View file

@ -364,10 +364,10 @@ github.com/hashicorp/consul-template/signals
github.com/hashicorp/consul-template/template github.com/hashicorp/consul-template/template
github.com/hashicorp/consul-template/version github.com/hashicorp/consul-template/version
github.com/hashicorp/consul-template/watch github.com/hashicorp/consul-template/watch
# github.com/hashicorp/consul/api v1.6.0 # github.com/hashicorp/consul/api v1.8.1
## explicit ## explicit
github.com/hashicorp/consul/api github.com/hashicorp/consul/api
# github.com/hashicorp/consul/sdk v0.6.0 # github.com/hashicorp/consul/sdk v0.7.0
## explicit ## explicit
github.com/hashicorp/consul/sdk/freeport github.com/hashicorp/consul/sdk/freeport
github.com/hashicorp/consul/sdk/testutil github.com/hashicorp/consul/sdk/testutil
@ -503,7 +503,7 @@ github.com/hashicorp/raft
# github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea # github.com/hashicorp/raft-boltdb v0.0.0-20171010151810-6e5ba93211ea
## explicit ## explicit
github.com/hashicorp/raft-boltdb github.com/hashicorp/raft-boltdb
# github.com/hashicorp/serf v0.9.3 # github.com/hashicorp/serf v0.9.5
## explicit ## explicit
github.com/hashicorp/serf/coordinate github.com/hashicorp/serf/coordinate
github.com/hashicorp/serf/serf github.com/hashicorp/serf/serf