Merge pull request #3812 from hashicorp/autopilot-config-change

Refactor redundancy_zone/upgrade_version out of client meta
This commit is contained in:
Kyle Havlovitz 2018-01-30 16:14:26 -08:00 committed by GitHub
commit cb2321353c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 364 additions and 331 deletions

View file

@ -2,6 +2,7 @@ package api
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
@ -19,7 +20,7 @@ type AutopilotConfiguration struct {
// LastContactThreshold is the limit on the amount of time a server can go // LastContactThreshold is the limit on the amount of time a server can go
// without leader contact before being considered unhealthy. // without leader contact before being considered unhealthy.
LastContactThreshold *ReadableDuration LastContactThreshold time.Duration
// MaxTrailingLogs is the amount of entries in the Raft Log that a server can // MaxTrailingLogs is the amount of entries in the Raft Log that a server can
// be behind before being considered unhealthy. // be behind before being considered unhealthy.
@ -28,20 +29,19 @@ type AutopilotConfiguration struct {
// ServerStabilizationTime is the minimum amount of time a server must be // ServerStabilizationTime is the minimum amount of time a server must be
// in a stable, healthy state before it can be added to the cluster. Only // in a stable, healthy state before it can be added to the cluster. Only
// applicable with Raft protocol version 3 or higher. // applicable with Raft protocol version 3 or higher.
ServerStabilizationTime *ReadableDuration ServerStabilizationTime time.Duration
// (Enterprise-only) RedundancyZoneTag is the node tag to use for separating // (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones.
// servers into zones for redundancy. If left blank, this feature will be disabled. EnableRedundancyZones bool
RedundancyZoneTag string
// (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration // (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration
// strategy of waiting until enough newer-versioned servers have been added to the // strategy of waiting until enough newer-versioned servers have been added to the
// cluster before promoting them to voters. // cluster before promoting them to voters.
DisableUpgradeMigration bool DisableUpgradeMigration bool
// (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when // (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom
// performing upgrade migrations. If left blank, the Nomad version will be used. // upgrade versions when performing migrations.
UpgradeVersionTag string EnableCustomUpgrades bool
// CreateIndex holds the index corresponding the creation of this configuration. // CreateIndex holds the index corresponding the creation of this configuration.
// This is a read-only field. // This is a read-only field.
@ -54,6 +54,45 @@ type AutopilotConfiguration struct {
ModifyIndex uint64 ModifyIndex uint64
} }
func (u *AutopilotConfiguration) MarshalJSON() ([]byte, error) {
type Alias AutopilotConfiguration
return json.Marshal(&struct {
LastContactThreshold string
ServerStabilizationTime string
*Alias
}{
LastContactThreshold: u.LastContactThreshold.String(),
ServerStabilizationTime: u.ServerStabilizationTime.String(),
Alias: (*Alias)(u),
})
}
func (u *AutopilotConfiguration) UnmarshalJSON(data []byte) error {
type Alias AutopilotConfiguration
aux := &struct {
LastContactThreshold string
ServerStabilizationTime string
*Alias
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
if aux.LastContactThreshold != "" {
if u.LastContactThreshold, err = time.ParseDuration(aux.LastContactThreshold); err != nil {
return err
}
}
if aux.ServerStabilizationTime != "" {
if u.ServerStabilizationTime, err = time.ParseDuration(aux.ServerStabilizationTime); err != nil {
return err
}
}
return nil
}
// ServerHealth is the health (from the leader's point of view) of a server. // ServerHealth is the health (from the leader's point of view) of a server.
type ServerHealth struct { type ServerHealth struct {
// ID is the raft ID of the server. // ID is the raft ID of the server.
@ -75,7 +114,7 @@ type ServerHealth struct {
Leader bool Leader bool
// LastContact is the time since this node's last contact with the leader. // LastContact is the time since this node's last contact with the leader.
LastContact *ReadableDuration LastContact time.Duration
// LastTerm is the highest leader term this server has a record of in its Raft log. // LastTerm is the highest leader term this server has a record of in its Raft log.
LastTerm uint64 LastTerm uint64
@ -94,6 +133,37 @@ type ServerHealth struct {
StableSince time.Time StableSince time.Time
} }
func (u *ServerHealth) MarshalJSON() ([]byte, error) {
type Alias ServerHealth
return json.Marshal(&struct {
LastContact string
*Alias
}{
LastContact: u.LastContact.String(),
Alias: (*Alias)(u),
})
}
func (u *ServerHealth) UnmarshalJSON(data []byte) error {
type Alias ServerHealth
aux := &struct {
LastContact string
*Alias
}{
Alias: (*Alias)(u),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
var err error
if aux.LastContact != "" {
if u.LastContact, err = time.ParseDuration(aux.LastContact); err != nil {
return err
}
}
return nil
}
// OperatorHealthReply is a representation of the overall health of the cluster // OperatorHealthReply is a representation of the overall health of the cluster
type OperatorHealthReply struct { type OperatorHealthReply struct {
// Healthy is true if all the servers in the cluster are healthy. // Healthy is true if all the servers in the cluster are healthy.
@ -107,46 +177,6 @@ type OperatorHealthReply struct {
Servers []ServerHealth Servers []ServerHealth
} }
// ReadableDuration is a duration type that is serialized to JSON in human readable format.
type ReadableDuration time.Duration
func NewReadableDuration(dur time.Duration) *ReadableDuration {
d := ReadableDuration(dur)
return &d
}
func (d *ReadableDuration) String() string {
return d.Duration().String()
}
func (d *ReadableDuration) Duration() time.Duration {
if d == nil {
return time.Duration(0)
}
return time.Duration(*d)
}
func (d *ReadableDuration) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, d.Duration().String())), nil
}
func (d *ReadableDuration) UnmarshalJSON(raw []byte) error {
if d == nil {
return fmt.Errorf("cannot unmarshal to nil pointer")
}
str := string(raw)
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
return fmt.Errorf("must be enclosed with quotes: %s", str)
}
dur, err := time.ParseDuration(str[1 : len(str)-1])
if err != nil {
return err
}
*d = ReadableDuration(dur)
return nil
}
// AutopilotGetConfiguration is used to query the current Autopilot configuration. // AutopilotGetConfiguration is used to query the current Autopilot configuration.
func (op *Operator) AutopilotGetConfiguration(q *QueryOptions) (*AutopilotConfiguration, error) { func (op *Operator) AutopilotGetConfiguration(q *QueryOptions) (*AutopilotConfiguration, error) {
r, err := op.c.newRequest("GET", "/v1/operator/autopilot/configuration") r, err := op.c.newRequest("GET", "/v1/operator/autopilot/configuration")

View file

@ -17,13 +17,17 @@ func TestAPI_OperatorAutopilotGetSetConfiguration(t *testing.T) {
defer s.Stop() defer s.Stop()
operator := c.Operator() operator := c.Operator()
config, err := operator.AutopilotGetConfiguration(nil) var config *AutopilotConfiguration
assert.Nil(err) retry.Run(t, func(r *retry.R) {
var err error
config, err = operator.AutopilotGetConfiguration(nil)
r.Check(err)
})
assert.True(config.CleanupDeadServers) assert.True(config.CleanupDeadServers)
// Change a config setting // Change a config setting
newConf := &AutopilotConfiguration{CleanupDeadServers: false} newConf := &AutopilotConfiguration{CleanupDeadServers: false}
err = operator.AutopilotSetConfiguration(newConf, nil) err := operator.AutopilotSetConfiguration(newConf, nil)
assert.Nil(err) assert.Nil(err)
config, err = operator.AutopilotGetConfiguration(nil) config, err = operator.AutopilotGetConfiguration(nil)

View file

@ -163,6 +163,12 @@ func convertServerConfig(agentConfig *Config, logOutput io.Writer) (*nomad.Confi
if agentConfig.Server.NonVotingServer { if agentConfig.Server.NonVotingServer {
conf.NonVoter = true conf.NonVoter = true
} }
if agentConfig.Server.RedundancyZone != "" {
conf.RedundancyZone = agentConfig.Server.RedundancyZone
}
if agentConfig.Server.UpgradeVersion != "" {
conf.UpgradeVersion = agentConfig.Server.UpgradeVersion
}
if agentConfig.Autopilot != nil { if agentConfig.Autopilot != nil {
if agentConfig.Autopilot.CleanupDeadServers != nil { if agentConfig.Autopilot.CleanupDeadServers != nil {
conf.AutopilotConfig.CleanupDeadServers = *agentConfig.Autopilot.CleanupDeadServers conf.AutopilotConfig.CleanupDeadServers = *agentConfig.Autopilot.CleanupDeadServers
@ -176,14 +182,14 @@ func convertServerConfig(agentConfig *Config, logOutput io.Writer) (*nomad.Confi
if agentConfig.Autopilot.MaxTrailingLogs != 0 { if agentConfig.Autopilot.MaxTrailingLogs != 0 {
conf.AutopilotConfig.MaxTrailingLogs = uint64(agentConfig.Autopilot.MaxTrailingLogs) conf.AutopilotConfig.MaxTrailingLogs = uint64(agentConfig.Autopilot.MaxTrailingLogs)
} }
if agentConfig.Autopilot.RedundancyZoneTag != "" { if agentConfig.Autopilot.EnableRedundancyZones != nil {
conf.AutopilotConfig.RedundancyZoneTag = agentConfig.Autopilot.RedundancyZoneTag conf.AutopilotConfig.EnableRedundancyZones = *agentConfig.Autopilot.EnableRedundancyZones
} }
if agentConfig.Autopilot.DisableUpgradeMigration != nil { if agentConfig.Autopilot.DisableUpgradeMigration != nil {
conf.AutopilotConfig.DisableUpgradeMigration = *agentConfig.Autopilot.DisableUpgradeMigration conf.AutopilotConfig.DisableUpgradeMigration = *agentConfig.Autopilot.DisableUpgradeMigration
} }
if agentConfig.Autopilot.UpgradeVersionTag != "" { if agentConfig.Autopilot.EnableCustomUpgrades != nil {
conf.AutopilotConfig.UpgradeVersionTag = agentConfig.Autopilot.UpgradeVersionTag conf.AutopilotConfig.EnableCustomUpgrades = *agentConfig.Autopilot.EnableCustomUpgrades
} }
} }

View file

@ -83,7 +83,9 @@ server {
retry_interval = "15s" retry_interval = "15s"
rejoin_after_leave = true rejoin_after_leave = true
non_voting_server = true non_voting_server = true
encrypt = "abc" redundancy_zone = "foo"
upgrade_version = "0.8.0"
encrypt = "abc"
} }
acl { acl {
enabled = true enabled = true
@ -166,7 +168,7 @@ autopilot {
disable_upgrade_migration = true disable_upgrade_migration = true
last_contact_threshold = "12705s" last_contact_threshold = "12705s"
max_trailing_logs = 17849 max_trailing_logs = 17849
redundancy_zone_tag = "foo" enable_redundancy_zones = true
server_stabilization_time = "23057s" server_stabilization_time = "23057s"
upgrade_version_tag = "bar" enable_custom_upgrades = true
} }

View file

@ -330,10 +330,17 @@ type ServerConfig struct {
// true, we ignore the leave, and rejoin the cluster on start. // true, we ignore the leave, and rejoin the cluster on start.
RejoinAfterLeave bool `mapstructure:"rejoin_after_leave"` RejoinAfterLeave bool `mapstructure:"rejoin_after_leave"`
// NonVotingServer is whether this server will act as a non-voting member // (Enterprise-only) NonVotingServer is whether this server will act as a
// of the cluster to help provide read scalability. (Enterprise-only) // non-voting member of the cluster to help provide read scalability.
NonVotingServer bool `mapstructure:"non_voting_server"` NonVotingServer bool `mapstructure:"non_voting_server"`
// (Enterprise-only) RedundancyZone is the redundancy zone to use for this server.
RedundancyZone string `mapstructure:"redundancy_zone"`
// (Enterprise-only) UpgradeVersion is the custom upgrade version to use when
// performing upgrade migrations.
UpgradeVersion string `mapstructure:"upgrade_version"`
// Encryption key to use for the Serf communication // Encryption key to use for the Serf communication
EncryptKey string `mapstructure:"encrypt" json:"-"` EncryptKey string `mapstructure:"encrypt" json:"-"`
} }
@ -1034,6 +1041,12 @@ func (a *ServerConfig) Merge(b *ServerConfig) *ServerConfig {
if b.NonVotingServer { if b.NonVotingServer {
result.NonVotingServer = true result.NonVotingServer = true
} }
if b.RedundancyZone != "" {
result.RedundancyZone = b.RedundancyZone
}
if b.UpgradeVersion != "" {
result.UpgradeVersion = b.UpgradeVersion
}
if b.EncryptKey != "" { if b.EncryptKey != "" {
result.EncryptKey = b.EncryptKey result.EncryptKey = b.EncryptKey
} }

View file

@ -9,6 +9,7 @@ import (
"time" "time"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl" "github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper"
@ -536,6 +537,8 @@ func parseServer(result **ServerConfig, list *ast.ObjectList) error {
"encrypt", "encrypt",
"authoritative_region", "authoritative_region",
"non_voting_server", "non_voting_server",
"redundancy_zone",
"upgrade_version",
} }
if err := helper.CheckHCLKeys(listVal, valid); err != nil { if err := helper.CheckHCLKeys(listVal, valid); err != nil {
return err return err
@ -559,6 +562,12 @@ func parseServer(result **ServerConfig, list *ast.ObjectList) error {
return err return err
} }
if config.UpgradeVersion != "" {
if _, err := version.NewVersion(config.UpgradeVersion); err != nil {
return fmt.Errorf("error parsing upgrade_version: %v", err)
}
}
*result = &config *result = &config
return nil return nil
} }
@ -865,9 +874,9 @@ func parseAutopilot(result **config.AutopilotConfig, list *ast.ObjectList) error
"server_stabilization_time", "server_stabilization_time",
"last_contact_threshold", "last_contact_threshold",
"max_trailing_logs", "max_trailing_logs",
"redundancy_zone_tag", "enable_redundancy_zones",
"disable_upgrade_migration", "disable_upgrade_migration",
"upgrade_version_tag", "enable_custom_upgrades",
} }
if err := helper.CheckHCLKeys(listVal, valid); err != nil { if err := helper.CheckHCLKeys(listVal, valid); err != nil {

View file

@ -104,6 +104,8 @@ func TestConfig_Parse(t *testing.T) {
RejoinAfterLeave: true, RejoinAfterLeave: true,
RetryMaxAttempts: 3, RetryMaxAttempts: 3,
NonVotingServer: true, NonVotingServer: true,
RedundancyZone: "foo",
UpgradeVersion: "0.8.0",
EncryptKey: "abc", EncryptKey: "abc",
}, },
ACL: &ACLConfig{ ACL: &ACLConfig{
@ -193,9 +195,9 @@ func TestConfig_Parse(t *testing.T) {
ServerStabilizationTime: 23057 * time.Second, ServerStabilizationTime: 23057 * time.Second,
LastContactThreshold: 12705 * time.Second, LastContactThreshold: 12705 * time.Second,
MaxTrailingLogs: 17849, MaxTrailingLogs: 17849,
RedundancyZoneTag: "foo", EnableRedundancyZones: &trueValue,
DisableUpgradeMigration: &trueValue, DisableUpgradeMigration: &trueValue,
UpgradeVersionTag: "bar", EnableCustomUpgrades: &trueValue,
}, },
}, },
false, false,

View file

@ -107,6 +107,8 @@ func TestConfig_Merge(t *testing.T) {
HeartbeatGrace: 30 * time.Second, HeartbeatGrace: 30 * time.Second,
MinHeartbeatTTL: 30 * time.Second, MinHeartbeatTTL: 30 * time.Second,
MaxHeartbeatsPerSecond: 30.0, MaxHeartbeatsPerSecond: 30.0,
RedundancyZone: "foo",
UpgradeVersion: "foo",
}, },
ACL: &ACLConfig{ ACL: &ACLConfig{
Enabled: true, Enabled: true,
@ -165,9 +167,9 @@ func TestConfig_Merge(t *testing.T) {
ServerStabilizationTime: 1 * time.Second, ServerStabilizationTime: 1 * time.Second,
LastContactThreshold: 1 * time.Second, LastContactThreshold: 1 * time.Second,
MaxTrailingLogs: 1, MaxTrailingLogs: 1,
RedundancyZoneTag: "1", EnableRedundancyZones: &falseValue,
DisableUpgradeMigration: &falseValue, DisableUpgradeMigration: &falseValue,
UpgradeVersionTag: "1", EnableCustomUpgrades: &falseValue,
}, },
} }
@ -260,6 +262,8 @@ func TestConfig_Merge(t *testing.T) {
RetryInterval: "10s", RetryInterval: "10s",
retryInterval: time.Second * 10, retryInterval: time.Second * 10,
NonVotingServer: true, NonVotingServer: true,
RedundancyZone: "bar",
UpgradeVersion: "bar",
}, },
ACL: &ACLConfig{ ACL: &ACLConfig{
Enabled: true, Enabled: true,
@ -328,9 +332,9 @@ func TestConfig_Merge(t *testing.T) {
ServerStabilizationTime: 2 * time.Second, ServerStabilizationTime: 2 * time.Second,
LastContactThreshold: 2 * time.Second, LastContactThreshold: 2 * time.Second,
MaxTrailingLogs: 2, MaxTrailingLogs: 2,
RedundancyZoneTag: "2", EnableRedundancyZones: &trueValue,
DisableUpgradeMigration: &trueValue, DisableUpgradeMigration: &trueValue,
UpgradeVersionTag: "2", EnableCustomUpgrades: &trueValue,
}, },
} }

View file

@ -18,7 +18,6 @@ import (
assetfs "github.com/elazarl/go-bindata-assetfs" assetfs "github.com/elazarl/go-bindata-assetfs"
"github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
"github.com/mitchellh/mapstructure"
"github.com/rs/cors" "github.com/rs/cors"
"github.com/ugorji/go/codec" "github.com/ugorji/go/codec"
) )
@ -346,24 +345,6 @@ func decodeBody(req *http.Request, out interface{}) error {
return dec.Decode(&out) return dec.Decode(&out)
} }
// decodeBodyFunc is used to decode a JSON request body invoking
// a given callback function
func decodeBodyFunc(req *http.Request, out interface{}, cb func(interface{}) error) error {
var raw interface{}
dec := json.NewDecoder(req.Body)
if err := dec.Decode(&raw); err != nil {
return err
}
// Invoke the callback prior to decode
if cb != nil {
if err := cb(raw); err != nil {
return err
}
}
return mapstructure.Decode(raw, out)
}
// setIndex is used to set the index response header // setIndex is used to set the index response header
func setIndex(resp http.ResponseWriter, index uint64) { func setIndex(resp http.ResponseWriter, index uint64) {
resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10)) resp.Header().Set("X-Nomad-Index", strconv.FormatUint(index, 10))

View file

@ -104,19 +104,19 @@ func (s *HTTPServer) OperatorAutopilotConfiguration(resp http.ResponseWriter, re
return nil, nil return nil, nil
} }
var reply autopilot.Config var reply structs.AutopilotConfig
if err := s.agent.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { if err := s.agent.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
return nil, err return nil, err
} }
out := api.AutopilotConfiguration{ out := api.AutopilotConfiguration{
CleanupDeadServers: reply.CleanupDeadServers, CleanupDeadServers: reply.CleanupDeadServers,
LastContactThreshold: api.NewReadableDuration(reply.LastContactThreshold), LastContactThreshold: reply.LastContactThreshold,
MaxTrailingLogs: reply.MaxTrailingLogs, MaxTrailingLogs: reply.MaxTrailingLogs,
ServerStabilizationTime: api.NewReadableDuration(reply.ServerStabilizationTime), ServerStabilizationTime: reply.ServerStabilizationTime,
RedundancyZoneTag: reply.RedundancyZoneTag, EnableRedundancyZones: reply.EnableRedundancyZones,
DisableUpgradeMigration: reply.DisableUpgradeMigration, DisableUpgradeMigration: reply.DisableUpgradeMigration,
UpgradeVersionTag: reply.UpgradeVersionTag, EnableCustomUpgrades: reply.EnableCustomUpgrades,
CreateIndex: reply.CreateIndex, CreateIndex: reply.CreateIndex,
ModifyIndex: reply.ModifyIndex, ModifyIndex: reply.ModifyIndex,
} }
@ -129,21 +129,20 @@ func (s *HTTPServer) OperatorAutopilotConfiguration(resp http.ResponseWriter, re
s.parseToken(req, &args.AuthToken) s.parseToken(req, &args.AuthToken)
var conf api.AutopilotConfiguration var conf api.AutopilotConfiguration
durations := NewDurationFixer("lastcontactthreshold", "serverstabilizationtime") if err := decodeBody(req, &conf); err != nil {
if err := decodeBodyFunc(req, &conf, durations.FixupDurations); err != nil {
resp.WriteHeader(http.StatusBadRequest) resp.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(resp, "Error parsing autopilot config: %v", err) fmt.Fprintf(resp, "Error parsing autopilot config: %v", err)
return nil, nil return nil, nil
} }
args.Config = autopilot.Config{ args.Config = structs.AutopilotConfig{
CleanupDeadServers: conf.CleanupDeadServers, CleanupDeadServers: conf.CleanupDeadServers,
LastContactThreshold: conf.LastContactThreshold.Duration(), LastContactThreshold: conf.LastContactThreshold,
MaxTrailingLogs: conf.MaxTrailingLogs, MaxTrailingLogs: conf.MaxTrailingLogs,
ServerStabilizationTime: conf.ServerStabilizationTime.Duration(), ServerStabilizationTime: conf.ServerStabilizationTime,
RedundancyZoneTag: conf.RedundancyZoneTag, EnableRedundancyZones: conf.EnableRedundancyZones,
DisableUpgradeMigration: conf.DisableUpgradeMigration, DisableUpgradeMigration: conf.DisableUpgradeMigration,
UpgradeVersionTag: conf.UpgradeVersionTag, EnableCustomUpgrades: conf.EnableCustomUpgrades,
} }
// Check for cas value // Check for cas value
@ -210,7 +209,7 @@ func (s *HTTPServer) OperatorServerHealth(resp http.ResponseWriter, req *http.Re
Version: server.Version, Version: server.Version,
Leader: server.Leader, Leader: server.Leader,
SerfStatus: server.SerfStatus.String(), SerfStatus: server.SerfStatus.String(),
LastContact: api.NewReadableDuration(server.LastContact), LastContact: server.LastContact,
LastTerm: server.LastTerm, LastTerm: server.LastTerm,
LastIndex: server.LastIndex, LastIndex: server.LastIndex,
Healthy: server.Healthy, Healthy: server.Healthy,
@ -221,56 +220,3 @@ func (s *HTTPServer) OperatorServerHealth(resp http.ResponseWriter, req *http.Re
return out, nil return out, nil
} }
type durationFixer map[string]bool
func NewDurationFixer(fields ...string) durationFixer {
d := make(map[string]bool)
for _, field := range fields {
d[field] = true
}
return d
}
// FixupDurations is used to handle parsing any field names in the map to time.Durations
func (d durationFixer) FixupDurations(raw interface{}) error {
rawMap, ok := raw.(map[string]interface{})
if !ok {
return nil
}
for key, val := range rawMap {
switch val.(type) {
case map[string]interface{}:
if err := d.FixupDurations(val); err != nil {
return err
}
case []interface{}:
for _, v := range val.([]interface{}) {
if err := d.FixupDurations(v); err != nil {
return err
}
}
case []map[string]interface{}:
for _, v := range val.([]map[string]interface{}) {
if err := d.FixupDurations(v); err != nil {
return err
}
}
default:
if d[strings.ToLower(key)] {
// Convert a string value into an integer
if vStr, ok := val.(string); ok {
dur, err := time.ParseDuration(vStr)
if err != nil {
return err
}
rawMap[key] = dur
}
}
}
}
return nil
}

View file

@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/consul/testutil/retry"
"github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/api"
"github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/nomad/structs"
@ -112,7 +111,7 @@ func TestOperator_AutopilotSetConfiguration(t *testing.T) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if resp.Code != 200 { if resp.Code != 200 {
t.Fatalf("bad code: %d", resp.Code) t.Fatalf("bad code: %d, %q", resp.Code, resp.Body.String())
} }
args := structs.GenericRequest{ args := structs.GenericRequest{
@ -121,7 +120,7 @@ func TestOperator_AutopilotSetConfiguration(t *testing.T) {
}, },
} }
var reply autopilot.Config var reply structs.AutopilotConfig
if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -150,7 +149,7 @@ func TestOperator_AutopilotCASConfiguration(t *testing.T) {
}, },
} }
var reply autopilot.Config var reply structs.AutopilotConfig
if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil { if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -200,7 +199,6 @@ func TestOperator_AutopilotCASConfiguration(t *testing.T) {
} }
func TestOperator_ServerHealth(t *testing.T) { func TestOperator_ServerHealth(t *testing.T) {
t.Parallel()
httpTest(t, func(c *Config) { httpTest(t, func(c *Config) {
c.Server.RaftProtocol = 3 c.Server.RaftProtocol = 3
}, func(s *TestAgent) { }, func(s *TestAgent) {
@ -259,47 +257,3 @@ func TestOperator_ServerHealth_Unhealthy(t *testing.T) {
}) })
}) })
} }
func TestDurationFixer(t *testing.T) {
assert := assert.New(t)
obj := map[string]interface{}{
"key1": []map[string]interface{}{
{
"subkey1": "10s",
},
{
"subkey2": "5d",
},
},
"key2": map[string]interface{}{
"subkey3": "30s",
"subkey4": "20m",
},
"key3": "11s",
"key4": "49h",
}
expected := map[string]interface{}{
"key1": []map[string]interface{}{
{
"subkey1": 10 * time.Second,
},
{
"subkey2": "5d",
},
},
"key2": map[string]interface{}{
"subkey3": "30s",
"subkey4": 20 * time.Minute,
},
"key3": "11s",
"key4": 49 * time.Hour,
}
fixer := NewDurationFixer("key4", "subkey1", "subkey4")
if err := fixer.FixupDurations(obj); err != nil {
t.Fatal(err)
}
// Ensure we only processed the intended fieldnames
assert.Equal(obj, expected)
}

View file

@ -45,9 +45,9 @@ func (c *OperatorAutopilotGetCommand) Run(args []string) int {
c.Ui.Output(fmt.Sprintf("LastContactThreshold = %v", config.LastContactThreshold.String())) c.Ui.Output(fmt.Sprintf("LastContactThreshold = %v", config.LastContactThreshold.String()))
c.Ui.Output(fmt.Sprintf("MaxTrailingLogs = %v", config.MaxTrailingLogs)) c.Ui.Output(fmt.Sprintf("MaxTrailingLogs = %v", config.MaxTrailingLogs))
c.Ui.Output(fmt.Sprintf("ServerStabilizationTime = %v", config.ServerStabilizationTime.String())) c.Ui.Output(fmt.Sprintf("ServerStabilizationTime = %v", config.ServerStabilizationTime.String()))
c.Ui.Output(fmt.Sprintf("RedundancyZoneTag = %q", config.RedundancyZoneTag)) c.Ui.Output(fmt.Sprintf("EnableRedundancyZones = %v", config.EnableRedundancyZones))
c.Ui.Output(fmt.Sprintf("DisableUpgradeMigration = %v", config.DisableUpgradeMigration)) c.Ui.Output(fmt.Sprintf("DisableUpgradeMigration = %v", config.DisableUpgradeMigration))
c.Ui.Output(fmt.Sprintf("UpgradeVersionTag = %q", config.UpgradeVersionTag)) c.Ui.Output(fmt.Sprintf("EnableCustomUpgrades = %v", config.EnableCustomUpgrades))
return 0 return 0
} }

View file

@ -3,10 +3,8 @@ package command
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/nomad/api"
"github.com/posener/complete" "github.com/posener/complete"
) )
@ -21,9 +19,9 @@ func (c *OperatorAutopilotSetCommand) AutocompleteFlags() complete.Flags {
"-max-trailing-logs": complete.PredictAnything, "-max-trailing-logs": complete.PredictAnything,
"-last-contact-threshold": complete.PredictAnything, "-last-contact-threshold": complete.PredictAnything,
"-server-stabilization-time": complete.PredictAnything, "-server-stabilization-time": complete.PredictAnything,
"-redundancy-zone-tag": complete.PredictAnything, "-enable-redundancy-zones": complete.PredictNothing,
"-disable-upgrade-migration": complete.PredictAnything, "-disable-upgrade-migration": complete.PredictNothing,
"-upgrade-version-tag": complete.PredictAnything, "-enable-custom-upgrades": complete.PredictNothing,
}) })
} }
@ -36,9 +34,9 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int {
var maxTrailingLogs flags.UintValue var maxTrailingLogs flags.UintValue
var lastContactThreshold flags.DurationValue var lastContactThreshold flags.DurationValue
var serverStabilizationTime flags.DurationValue var serverStabilizationTime flags.DurationValue
var redundancyZoneTag flags.StringValue var enableRedundancyZones flags.BoolValue
var disableUpgradeMigration flags.BoolValue var disableUpgradeMigration flags.BoolValue
var upgradeVersionTag flags.StringValue var enableCustomUpgrades flags.BoolValue
f := c.Meta.FlagSet("autopilot", FlagSetClient) f := c.Meta.FlagSet("autopilot", FlagSetClient)
f.Usage = func() { c.Ui.Output(c.Help()) } f.Usage = func() { c.Ui.Output(c.Help()) }
@ -47,9 +45,9 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int {
f.Var(&maxTrailingLogs, "max-trailing-logs", "") f.Var(&maxTrailingLogs, "max-trailing-logs", "")
f.Var(&lastContactThreshold, "last-contact-threshold", "") f.Var(&lastContactThreshold, "last-contact-threshold", "")
f.Var(&serverStabilizationTime, "server-stabilization-time", "") f.Var(&serverStabilizationTime, "server-stabilization-time", "")
f.Var(&redundancyZoneTag, "redundancy-zone-tag", "") f.Var(&enableRedundancyZones, "enable-redundancy-zones", "")
f.Var(&disableUpgradeMigration, "disable-upgrade-migration", "") f.Var(&disableUpgradeMigration, "disable-upgrade-migration", "")
f.Var(&upgradeVersionTag, "upgrade-version-tag", "") f.Var(&enableCustomUpgrades, "enable-custom-upgrades", "")
if err := f.Parse(args); err != nil { if err := f.Parse(args); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse args: %v", err)) c.Ui.Error(fmt.Sprintf("Failed to parse args: %v", err))
@ -73,21 +71,15 @@ func (c *OperatorAutopilotSetCommand) Run(args []string) int {
// Update the config values based on the set flags. // Update the config values based on the set flags.
cleanupDeadServers.Merge(&conf.CleanupDeadServers) cleanupDeadServers.Merge(&conf.CleanupDeadServers)
redundancyZoneTag.Merge(&conf.RedundancyZoneTag) enableRedundancyZones.Merge(&conf.EnableRedundancyZones)
disableUpgradeMigration.Merge(&conf.DisableUpgradeMigration) disableUpgradeMigration.Merge(&conf.DisableUpgradeMigration)
upgradeVersionTag.Merge(&conf.UpgradeVersionTag) enableRedundancyZones.Merge(&conf.EnableCustomUpgrades)
trailing := uint(conf.MaxTrailingLogs) trailing := uint(conf.MaxTrailingLogs)
maxTrailingLogs.Merge(&trailing) maxTrailingLogs.Merge(&trailing)
conf.MaxTrailingLogs = uint64(trailing) conf.MaxTrailingLogs = uint64(trailing)
lastContactThreshold.Merge(&conf.LastContactThreshold)
last := time.Duration(*conf.LastContactThreshold) serverStabilizationTime.Merge(&conf.ServerStabilizationTime)
lastContactThreshold.Merge(&last)
conf.LastContactThreshold = api.NewReadableDuration(last)
stablization := time.Duration(*conf.ServerStabilizationTime)
serverStabilizationTime.Merge(&stablization)
conf.ServerStabilizationTime = api.NewReadableDuration(stablization)
// Check-and-set the new configuration. // Check-and-set the new configuration.
result, err := operator.AutopilotCASConfiguration(conf, nil) result, err := operator.AutopilotCASConfiguration(conf, nil)

View file

@ -53,10 +53,10 @@ func TestOperatorAutopilotSetConfigCommmand(t *testing.T) {
if conf.MaxTrailingLogs != 99 { if conf.MaxTrailingLogs != 99 {
t.Fatalf("bad: %#v", conf) t.Fatalf("bad: %#v", conf)
} }
if conf.LastContactThreshold.Duration() != 123*time.Millisecond { if conf.LastContactThreshold != 123*time.Millisecond {
t.Fatalf("bad: %#v", conf) t.Fatalf("bad: %#v", conf)
} }
if conf.ServerStabilizationTime.Duration() != 123*time.Millisecond { if conf.ServerStabilizationTime != 123*time.Millisecond {
t.Fatalf("bad: %#v", conf) t.Fatalf("bad: %#v", conf)
} }
} }

View file

@ -10,13 +10,45 @@ import (
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
) )
const (
// AutopilotRZTag is the Serf tag to use for the redundancy zone value
// when passing the server metadata to Autopilot.
AutopilotRZTag = "ap_zone"
// AutopilotRZTag is the Serf tag to use for the custom version value
// when passing the server metadata to Autopilot.
AutopilotVersionTag = "ap_version"
)
// AutopilotDelegate is a Nomad delegate for autopilot operations. // AutopilotDelegate is a Nomad delegate for autopilot operations.
type AutopilotDelegate struct { type AutopilotDelegate struct {
server *Server server *Server
} }
func (d *AutopilotDelegate) AutopilotConfig() *autopilot.Config { func (d *AutopilotDelegate) AutopilotConfig() *autopilot.Config {
return d.server.getOrCreateAutopilotConfig() c := d.server.getOrCreateAutopilotConfig()
if c == nil {
return nil
}
conf := &autopilot.Config{
CleanupDeadServers: c.CleanupDeadServers,
LastContactThreshold: c.LastContactThreshold,
MaxTrailingLogs: c.MaxTrailingLogs,
ServerStabilizationTime: c.ServerStabilizationTime,
DisableUpgradeMigration: c.DisableUpgradeMigration,
ModifyIndex: c.ModifyIndex,
CreateIndex: c.CreateIndex,
}
if c.EnableRedundancyZones {
conf.RedundancyZoneTag = AutopilotRZTag
}
if c.EnableCustomUpgrades {
conf.UpgradeVersionTag = AutopilotVersionTag
}
return conf
} }
func (d *AutopilotDelegate) FetchStats(ctx context.Context, servers []serf.Member) map[string]*autopilot.ServerStats { func (d *AutopilotDelegate) FetchStats(ctx context.Context, servers []serf.Member) map[string]*autopilot.ServerStats {

View file

@ -270,8 +270,11 @@ func TestAutopilot_CleanupStaleRaftServer(t *testing.T) {
testutil.WaitForLeader(t, s1.RPC) testutil.WaitForLeader(t, s1.RPC)
// Add s4 to peers directly // Add s4 to peers directly
addr := fmt.Sprintf("127.0.0.1:%d", s4.config.SerfConfig.MemberlistConfig.BindPort) addr := fmt.Sprintf("127.0.0.1:%d", s4.config.RPCAddr.Port)
s1.raft.AddVoter(raft.ServerID(s4.config.NodeID), raft.ServerAddress(addr), 0, 0) future := s1.raft.AddVoter(raft.ServerID(s4.config.NodeID), raft.ServerAddress(addr), 0, 0)
if err := future.Error(); err != nil {
t.Fatal(err)
}
// Verify we have 4 peers // Verify we have 4 peers
peers, err := s1.numPeers() peers, err := s1.numPeers()

View file

@ -8,7 +8,6 @@ import (
"runtime" "runtime"
"time" "time"
"github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/memberlist" "github.com/hashicorp/memberlist"
"github.com/hashicorp/nomad/helper/tlsutil" "github.com/hashicorp/nomad/helper/tlsutil"
"github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/helper/uuid"
@ -98,6 +97,13 @@ type Config struct {
// as a voting member of the Raft cluster. // as a voting member of the Raft cluster.
NonVoter bool NonVoter bool
// (Enterprise-only) RedundancyZone is the redundancy zone to use for this server.
RedundancyZone string
// (Enterprise-only) UpgradeVersion is the custom upgrade version to use when
// performing upgrade migrations.
UpgradeVersion string
// SerfConfig is the configuration for the serf cluster // SerfConfig is the configuration for the serf cluster
SerfConfig *serf.Config SerfConfig *serf.Config
@ -269,7 +275,7 @@ type Config struct {
// AutopilotConfig is used to apply the initial autopilot config when // AutopilotConfig is used to apply the initial autopilot config when
// bootstrapping. // bootstrapping.
AutopilotConfig *autopilot.Config AutopilotConfig *structs.AutopilotConfig
// ServerHealthInterval is the frequency with which the health of the // ServerHealthInterval is the frequency with which the health of the
// servers in the cluster will be updated. // servers in the cluster will be updated.
@ -339,7 +345,7 @@ func DefaultConfig() *Config {
TLSConfig: &config.TLSConfig{}, TLSConfig: &config.TLSConfig{},
ReplicationBackoff: 30 * time.Second, ReplicationBackoff: 30 * time.Second,
SentinelGCInterval: 30 * time.Second, SentinelGCInterval: 30 * time.Second,
AutopilotConfig: &autopilot.Config{ AutopilotConfig: &structs.AutopilotConfig{
CleanupDeadServers: true, CleanupDeadServers: true,
LastContactThreshold: 200 * time.Millisecond, LastContactThreshold: 200 * time.Millisecond,
MaxTrailingLogs: 250, MaxTrailingLogs: 250,

View file

@ -10,7 +10,6 @@ import (
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/hashicorp/consul/agent/consul/autopilot"
memdb "github.com/hashicorp/go-memdb" memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/helper" "github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/mock"
@ -2319,7 +2318,7 @@ func TestFSM_Autopilot(t *testing.T) {
// Set the autopilot config using a request. // Set the autopilot config using a request.
req := structs.AutopilotSetConfigRequest{ req := structs.AutopilotSetConfigRequest{
Datacenter: "dc1", Datacenter: "dc1",
Config: autopilot.Config{ Config: structs.AutopilotConfig{
CleanupDeadServers: true, CleanupDeadServers: true,
LastContactThreshold: 10 * time.Second, LastContactThreshold: 10 * time.Second,
MaxTrailingLogs: 300, MaxTrailingLogs: 300,

View file

@ -13,7 +13,6 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
"github.com/armon/go-metrics" "github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/consul/autopilot"
memdb "github.com/hashicorp/go-memdb" memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
"github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/helper/uuid"
@ -1174,7 +1173,7 @@ func diffACLTokens(state *state.StateStore, minIndex uint64, remoteList []*struc
} }
// getOrCreateAutopilotConfig is used to get the autopilot config, initializing it if necessary // getOrCreateAutopilotConfig is used to get the autopilot config, initializing it if necessary
func (s *Server) getOrCreateAutopilotConfig() *autopilot.Config { func (s *Server) getOrCreateAutopilotConfig() *structs.AutopilotConfig {
state := s.fsm.State() state := s.fsm.State()
_, config, err := state.AutopilotConfig() _, config, err := state.AutopilotConfig()
if err != nil { if err != nil {

View file

@ -192,7 +192,7 @@ REMOVE:
} }
// AutopilotGetConfiguration is used to retrieve the current Autopilot configuration. // AutopilotGetConfiguration is used to retrieve the current Autopilot configuration.
func (op *Operator) AutopilotGetConfiguration(args *structs.GenericRequest, reply *autopilot.Config) error { func (op *Operator) AutopilotGetConfiguration(args *structs.GenericRequest, reply *structs.AutopilotConfig) error {
if done, err := op.srv.forward("Operator.AutopilotGetConfiguration", args, args, reply); done { if done, err := op.srv.forward("Operator.AutopilotGetConfiguration", args, args, reply); done {
return err return err
} }

View file

@ -1105,6 +1105,12 @@ func (s *Server) setupSerf(conf *serf.Config, ch chan serf.Event, path string) (
if s.config.NonVoter { if s.config.NonVoter {
conf.Tags["nonvoter"] = "1" conf.Tags["nonvoter"] = "1"
} }
if s.config.RedundancyZone != "" {
conf.Tags[AutopilotRZTag] = s.config.RedundancyZone
}
if s.config.UpgradeVersion != "" {
conf.Tags[AutopilotVersionTag] = s.config.UpgradeVersion
}
conf.MemberlistConfig.LogOutput = s.config.LogOutput conf.MemberlistConfig.LogOutput = s.config.LogOutput
conf.LogOutput = s.config.LogOutput conf.LogOutput = s.config.LogOutput
conf.EventCh = ch conf.EventCh = ch

View file

@ -3,8 +3,8 @@ package state
import ( import (
"fmt" "fmt"
"github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/nomad/structs"
) )
// autopilotConfigTableSchema returns a new table schema used for storing // autopilotConfigTableSchema returns a new table schema used for storing
@ -26,7 +26,7 @@ func autopilotConfigTableSchema() *memdb.TableSchema {
} }
// AutopilotConfig is used to get the current Autopilot configuration. // AutopilotConfig is used to get the current Autopilot configuration.
func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) { func (s *StateStore) AutopilotConfig() (uint64, *structs.AutopilotConfig, error) {
tx := s.db.Txn(false) tx := s.db.Txn(false)
defer tx.Abort() defer tx.Abort()
@ -36,7 +36,7 @@ func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) {
return 0, nil, fmt.Errorf("failed autopilot config lookup: %s", err) return 0, nil, fmt.Errorf("failed autopilot config lookup: %s", err)
} }
config, ok := c.(*autopilot.Config) config, ok := c.(*structs.AutopilotConfig)
if !ok { if !ok {
return 0, nil, nil return 0, nil, nil
} }
@ -45,7 +45,7 @@ func (s *StateStore) AutopilotConfig() (uint64, *autopilot.Config, error) {
} }
// AutopilotSetConfig is used to set the current Autopilot configuration. // AutopilotSetConfig is used to set the current Autopilot configuration.
func (s *StateStore) AutopilotSetConfig(idx uint64, config *autopilot.Config) error { func (s *StateStore) AutopilotSetConfig(idx uint64, config *structs.AutopilotConfig) error {
tx := s.db.Txn(true) tx := s.db.Txn(true)
defer tx.Abort() defer tx.Abort()
@ -58,7 +58,7 @@ func (s *StateStore) AutopilotSetConfig(idx uint64, config *autopilot.Config) er
// AutopilotCASConfig is used to try updating the Autopilot configuration with a // AutopilotCASConfig is used to try updating the Autopilot configuration with a
// given Raft index. If the CAS index specified is not equal to the last observed index // given Raft index. If the CAS index specified is not equal to the last observed index
// for the config, then the call is a noop, // for the config, then the call is a noop,
func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Config) (bool, error) { func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *structs.AutopilotConfig) (bool, error) {
tx := s.db.Txn(true) tx := s.db.Txn(true)
defer tx.Abort() defer tx.Abort()
@ -71,7 +71,7 @@ func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Conf
// If the existing index does not match the provided CAS // If the existing index does not match the provided CAS
// index arg, then we shouldn't update anything and can safely // index arg, then we shouldn't update anything and can safely
// return early here. // return early here.
e, ok := existing.(*autopilot.Config) e, ok := existing.(*structs.AutopilotConfig)
if !ok || e.ModifyIndex != cidx { if !ok || e.ModifyIndex != cidx {
return false, nil return false, nil
} }
@ -82,7 +82,7 @@ func (s *StateStore) AutopilotCASConfig(idx, cidx uint64, config *autopilot.Conf
return true, nil return true, nil
} }
func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *autopilot.Config) error { func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *structs.AutopilotConfig) error {
// Check for an existing config // Check for an existing config
existing, err := tx.First("autopilot-config", "id") existing, err := tx.First("autopilot-config", "id")
if err != nil { if err != nil {
@ -91,7 +91,7 @@ func (s *StateStore) autopilotSetConfigTxn(idx uint64, tx *memdb.Txn, config *au
// Set the indexes. // Set the indexes.
if existing != nil { if existing != nil {
config.CreateIndex = existing.(*autopilot.Config).CreateIndex config.CreateIndex = existing.(*structs.AutopilotConfig).CreateIndex
} else { } else {
config.CreateIndex = idx config.CreateIndex = idx
} }

View file

@ -5,20 +5,20 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/nomad/nomad/structs"
) )
func TestStateStore_Autopilot(t *testing.T) { func TestStateStore_Autopilot(t *testing.T) {
s := testStateStore(t) s := testStateStore(t)
expected := &autopilot.Config{ expected := &structs.AutopilotConfig{
CleanupDeadServers: true, CleanupDeadServers: true,
LastContactThreshold: 5 * time.Second, LastContactThreshold: 5 * time.Second,
MaxTrailingLogs: 500, MaxTrailingLogs: 500,
ServerStabilizationTime: 100 * time.Second, ServerStabilizationTime: 100 * time.Second,
RedundancyZoneTag: "az", EnableRedundancyZones: true,
DisableUpgradeMigration: true, DisableUpgradeMigration: true,
UpgradeVersionTag: "build", EnableCustomUpgrades: true,
} }
if err := s.AutopilotSetConfig(0, expected); err != nil { if err := s.AutopilotSetConfig(0, expected); err != nil {
@ -40,7 +40,7 @@ func TestStateStore_Autopilot(t *testing.T) {
func TestStateStore_AutopilotCAS(t *testing.T) { func TestStateStore_AutopilotCAS(t *testing.T) {
s := testStateStore(t) s := testStateStore(t)
expected := &autopilot.Config{ expected := &structs.AutopilotConfig{
CleanupDeadServers: true, CleanupDeadServers: true,
} }
@ -52,7 +52,7 @@ func TestStateStore_AutopilotCAS(t *testing.T) {
} }
// Do a CAS with an index lower than the entry // Do a CAS with an index lower than the entry
ok, err := s.AutopilotCASConfig(2, 0, &autopilot.Config{ ok, err := s.AutopilotCASConfig(2, 0, &structs.AutopilotConfig{
CleanupDeadServers: false, CleanupDeadServers: false,
}) })
if ok || err != nil { if ok || err != nil {
@ -73,7 +73,7 @@ func TestStateStore_AutopilotCAS(t *testing.T) {
} }
// Do another CAS, this time with the correct index // Do another CAS, this time with the correct index
ok, err = s.AutopilotCASConfig(2, 1, &autopilot.Config{ ok, err = s.AutopilotCASConfig(2, 1, &structs.AutopilotConfig{
CleanupDeadServers: false, CleanupDeadServers: false,
}) })
if !ok || err != nil { if !ok || err != nil {

View file

@ -24,25 +24,23 @@ type AutopilotConfig struct {
// be behind before being considered unhealthy. // be behind before being considered unhealthy.
MaxTrailingLogs int `mapstructure:"max_trailing_logs"` MaxTrailingLogs int `mapstructure:"max_trailing_logs"`
// (Enterprise-only) RedundancyZoneTag is the node tag to use for separating // (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones.
// servers into zones for redundancy. If left blank, this feature will be disabled. EnableRedundancyZones *bool `mapstructure:"enable_redundancy_zones"`
RedundancyZoneTag string `mapstructure:"redundancy_zone_tag"`
// (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration // (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration
// strategy of waiting until enough newer-versioned servers have been added to the // strategy of waiting until enough newer-versioned servers have been added to the
// cluster before promoting them to voters. // cluster before promoting them to voters.
DisableUpgradeMigration *bool `mapstructure:"disable_upgrade_migration"` DisableUpgradeMigration *bool `mapstructure:"disable_upgrade_migration"`
// (Enterprise-only) UpgradeVersionTag is the node tag to use for version info when // (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom
// performing upgrade migrations. If left blank, the Nomad version will be used. // upgrade versions when performing migrations.
UpgradeVersionTag string `mapstructure:"upgrade_version_tag"` EnableCustomUpgrades *bool `mapstructure:"enable_custom_upgrades"`
} }
// DefaultAutopilotConfig() returns the canonical defaults for the Nomad // DefaultAutopilotConfig() returns the canonical defaults for the Nomad
// `autopilot` configuration. // `autopilot` configuration.
func DefaultAutopilotConfig() *AutopilotConfig { func DefaultAutopilotConfig() *AutopilotConfig {
return &AutopilotConfig{ return &AutopilotConfig{
CleanupDeadServers: helper.BoolToPtr(true),
LastContactThreshold: 200 * time.Millisecond, LastContactThreshold: 200 * time.Millisecond,
MaxTrailingLogs: 250, MaxTrailingLogs: 250,
ServerStabilizationTime: 10 * time.Second, ServerStabilizationTime: 10 * time.Second,
@ -64,14 +62,14 @@ func (a *AutopilotConfig) Merge(b *AutopilotConfig) *AutopilotConfig {
if b.MaxTrailingLogs != 0 { if b.MaxTrailingLogs != 0 {
result.MaxTrailingLogs = b.MaxTrailingLogs result.MaxTrailingLogs = b.MaxTrailingLogs
} }
if b.RedundancyZoneTag != "" { if b.EnableRedundancyZones != nil {
result.RedundancyZoneTag = b.RedundancyZoneTag result.EnableRedundancyZones = b.EnableRedundancyZones
} }
if b.DisableUpgradeMigration != nil { if b.DisableUpgradeMigration != nil {
result.DisableUpgradeMigration = helper.BoolToPtr(*b.DisableUpgradeMigration) result.DisableUpgradeMigration = helper.BoolToPtr(*b.DisableUpgradeMigration)
} }
if b.UpgradeVersionTag != "" { if b.EnableCustomUpgrades != nil {
result.UpgradeVersionTag = b.UpgradeVersionTag result.EnableCustomUpgrades = b.EnableCustomUpgrades
} }
return result return result
@ -90,9 +88,15 @@ func (a *AutopilotConfig) Copy() *AutopilotConfig {
if a.CleanupDeadServers != nil { if a.CleanupDeadServers != nil {
nc.CleanupDeadServers = helper.BoolToPtr(*a.CleanupDeadServers) nc.CleanupDeadServers = helper.BoolToPtr(*a.CleanupDeadServers)
} }
if a.EnableRedundancyZones != nil {
nc.EnableRedundancyZones = helper.BoolToPtr(*a.EnableRedundancyZones)
}
if a.DisableUpgradeMigration != nil { if a.DisableUpgradeMigration != nil {
nc.DisableUpgradeMigration = helper.BoolToPtr(*a.DisableUpgradeMigration) nc.DisableUpgradeMigration = helper.BoolToPtr(*a.DisableUpgradeMigration)
} }
if a.EnableCustomUpgrades != nil {
nc.EnableCustomUpgrades = helper.BoolToPtr(*a.EnableCustomUpgrades)
}
return nc return nc
} }

View file

@ -14,9 +14,9 @@ func TestAutopilotConfig_Merge(t *testing.T) {
ServerStabilizationTime: 1 * time.Second, ServerStabilizationTime: 1 * time.Second,
LastContactThreshold: 1 * time.Second, LastContactThreshold: 1 * time.Second,
MaxTrailingLogs: 1, MaxTrailingLogs: 1,
RedundancyZoneTag: "1", EnableRedundancyZones: &trueValue,
DisableUpgradeMigration: &falseValue, DisableUpgradeMigration: &falseValue,
UpgradeVersionTag: "1", EnableCustomUpgrades: &trueValue,
} }
c2 := &AutopilotConfig{ c2 := &AutopilotConfig{
@ -24,9 +24,9 @@ func TestAutopilotConfig_Merge(t *testing.T) {
ServerStabilizationTime: 2 * time.Second, ServerStabilizationTime: 2 * time.Second,
LastContactThreshold: 2 * time.Second, LastContactThreshold: 2 * time.Second,
MaxTrailingLogs: 2, MaxTrailingLogs: 2,
RedundancyZoneTag: "2", EnableRedundancyZones: nil,
DisableUpgradeMigration: nil, DisableUpgradeMigration: nil,
UpgradeVersionTag: "2", EnableCustomUpgrades: nil,
} }
e := &AutopilotConfig{ e := &AutopilotConfig{
@ -34,9 +34,9 @@ func TestAutopilotConfig_Merge(t *testing.T) {
ServerStabilizationTime: 2 * time.Second, ServerStabilizationTime: 2 * time.Second,
LastContactThreshold: 2 * time.Second, LastContactThreshold: 2 * time.Second,
MaxTrailingLogs: 2, MaxTrailingLogs: 2,
RedundancyZoneTag: "2", EnableRedundancyZones: &trueValue,
DisableUpgradeMigration: &falseValue, DisableUpgradeMigration: &falseValue,
UpgradeVersionTag: "2", EnableCustomUpgrades: &trueValue,
} }
result := c1.Merge(c2) result := c1.Merge(c2)

View file

@ -1,7 +1,8 @@
package structs package structs
import ( import (
"github.com/hashicorp/consul/agent/consul/autopilot" "time"
"github.com/hashicorp/raft" "github.com/hashicorp/raft"
) )
@ -69,7 +70,7 @@ type AutopilotSetConfigRequest struct {
Datacenter string Datacenter string
// Config is the new Autopilot configuration to use. // Config is the new Autopilot configuration to use.
Config autopilot.Config Config AutopilotConfig
// CAS controls whether to use check-and-set semantics for this request. // CAS controls whether to use check-and-set semantics for this request.
CAS bool CAS bool
@ -82,3 +83,39 @@ type AutopilotSetConfigRequest struct {
func (op *AutopilotSetConfigRequest) RequestDatacenter() string { func (op *AutopilotSetConfigRequest) RequestDatacenter() string {
return op.Datacenter return op.Datacenter
} }
// AutopilotConfig is the internal config for the Autopilot mechanism.
type AutopilotConfig struct {
// CleanupDeadServers controls whether to remove dead servers when a new
// server is added to the Raft peers.
CleanupDeadServers bool
// ServerStabilizationTime is the minimum amount of time a server must be
// in a stable, healthy state before it can be added to the cluster. Only
// applicable with Raft protocol version 3 or higher.
ServerStabilizationTime time.Duration
// LastContactThreshold is the limit on the amount of time a server can go
// without leader contact before being considered unhealthy.
LastContactThreshold time.Duration
// MaxTrailingLogs is the amount of entries in the Raft Log that a server can
// be behind before being considered unhealthy.
MaxTrailingLogs uint64
// (Enterprise-only) EnableRedundancyZones specifies whether to enable redundancy zones.
EnableRedundancyZones bool
// (Enterprise-only) DisableUpgradeMigration will disable Autopilot's upgrade migration
// strategy of waiting until enough newer-versioned servers have been added to the
// cluster before promoting them to voters.
DisableUpgradeMigration bool
// (Enterprise-only) EnableCustomUpgrades specifies whether to enable using custom
// upgrade versions when performing migrations.
EnableCustomUpgrades bool
// CreateIndex/ModifyIndex store the create/modify indexes of this configuration.
CreateIndex uint64
ModifyIndex uint64
}

View file

@ -46,7 +46,6 @@ type serverParts struct {
MinorVersion int MinorVersion int
Build version.Version Build version.Version
RaftVersion int RaftVersion int
NonVoter bool
Addr net.Addr Addr net.Addr
RPCAddr net.Addr RPCAddr net.Addr
Status serf.MemberStatus Status serf.MemberStatus
@ -71,7 +70,6 @@ func isNomadServer(m serf.Member) (bool, *serverParts) {
region := m.Tags["region"] region := m.Tags["region"]
datacenter := m.Tags["dc"] datacenter := m.Tags["dc"]
_, bootstrap := m.Tags["bootstrap"] _, bootstrap := m.Tags["bootstrap"]
_, nonVoter := m.Tags["nonvoter"]
expect := 0 expect := 0
expectStr, ok := m.Tags["expect"] expectStr, ok := m.Tags["expect"]
@ -140,7 +138,6 @@ func isNomadServer(m serf.Member) (bool, *serverParts) {
MinorVersion: minorVersion, MinorVersion: minorVersion,
Build: *buildVersion, Build: *buildVersion,
RaftVersion: raftVsn, RaftVersion: raftVsn,
NonVoter: nonVoter,
Status: m.Status, Status: m.Status,
} }
return true, parts return true, parts

View file

@ -24,7 +24,6 @@ func TestIsNomadServer(t *testing.T) {
"port": "10000", "port": "10000",
"vsn": "1", "vsn": "1",
"raft_vsn": "2", "raft_vsn": "2",
"nonvoter": "1",
"build": "0.7.0+ent", "build": "0.7.0+ent",
}, },
} }
@ -51,9 +50,6 @@ func TestIsNomadServer(t *testing.T) {
if parts.RPCAddr.String() != "1.1.1.1:10000" { if parts.RPCAddr.String() != "1.1.1.1:10000" {
t.Fatalf("bad: %v", parts.RPCAddr.String()) t.Fatalf("bad: %v", parts.RPCAddr.String())
} }
if !parts.NonVoter {
t.Fatalf("bad: %v", parts.NonVoter)
}
if seg := parts.Build.Segments(); len(seg) != 3 { if seg := parts.Build.Segments(); len(seg) != 3 {
t.Fatalf("bad: %v", parts.Build) t.Fatalf("bad: %v", parts.Build)
} else if seg[0] != 0 && seg[1] != 7 && seg[2] != 0 { } else if seg[0] != 0 && seg[1] != 7 && seg[2] != 0 {

View file

@ -168,9 +168,9 @@ $ curl \
"LastContactThreshold": "200ms", "LastContactThreshold": "200ms",
"MaxTrailingLogs": 250, "MaxTrailingLogs": 250,
"ServerStabilizationTime": "10s", "ServerStabilizationTime": "10s",
"RedundancyZoneTag": "", "EnableRedundancyZones": false,
"DisableUpgradeMigration": false, "DisableUpgradeMigration": false,
"UpgradeVersionTag": "", "EnableCustomUpgrades": false,
"CreateIndex": 4, "CreateIndex": 4,
"ModifyIndex": 4 "ModifyIndex": 4
} }
@ -221,19 +221,16 @@ The table below shows this endpoint's support for
cluster. Only takes effect if all servers are running Raft protocol version 3 cluster. Only takes effect if all servers are running Raft protocol version 3
or higher. Must be a duration value such as `30s`. or higher. Must be a duration value such as `30s`.
- `RedundancyZoneTag` `(string: "")` - Controls the node-meta key to use when - `EnableRedundancyZones` `(bool: false)` - (Enterprise-only) Specifies whether
Autopilot is separating servers into zones for redundancy. Only one server in to enable redundancy zones.
each zone can be a voting member at one time. If left blank, this feature will
be disabled.
- `DisableUpgradeMigration` `(bool: false)` - Disables Autopilot's upgrade - `DisableUpgradeMigration` `(bool: false)` - (Enterprise-only) Disables Autopilot's
migration strategy in Nomad Enterprise of waiting until enough upgrade migration strategy in Nomad Enterprise of waiting until enough
newer-versioned servers have been added to the cluster before promoting any of newer-versioned servers have been added to the cluster before promoting any of
them to voters. them to voters.
- `UpgradeVersionTag` `(string: "")` - Controls the node-meta key to use for - `EnableCustomUpgrades` `(bool: false)` - (Enterprise-only) Specifies whether to
version info when performing upgrade migrations. If left blank, the Nomad enable using custom upgrade versions when performing migrations.
version will be used.
### Sample Payload ### Sample Payload
@ -243,9 +240,9 @@ The table below shows this endpoint's support for
"LastContactThreshold": "200ms", "LastContactThreshold": "200ms",
"MaxTrailingLogs": 250, "MaxTrailingLogs": 250,
"ServerStabilizationTime": "10s", "ServerStabilizationTime": "10s",
"RedundancyZoneTag": "", "EnableRedundancyZones": false,
"DisableUpgradeMigration": false, "DisableUpgradeMigration": false,
"UpgradeVersionTag": "", "EnableCustomUpgrades": false,
"CreateIndex": 4, "CreateIndex": 4,
"ModifyIndex": 4 "ModifyIndex": 4
} }

View file

@ -18,6 +18,7 @@ description: |-
</table> </table>
The `autopilot` stanza configures the Nomad agent to configure Autopilot behavior. The `autopilot` stanza configures the Nomad agent to configure Autopilot behavior.
For more information about Autopilot, see the [Autopilot Guide](/guides/cluster/autopilot.html).
```hcl ```hcl
autopilot { autopilot {
@ -25,9 +26,9 @@ autopilot {
last_contact_threshold = "200ms" last_contact_threshold = "200ms"
max_trailing_logs = 250 max_trailing_logs = 250
server_stabilization_time = "10s" server_stabilization_time = "10s"
redundancy_zone_tag = "" enable_redundancy_zones = false
disable_upgrade_migration = true disable_upgrade_migration = false
upgrade_version_tag = "" enable_custom_upgrades = false
} }
``` ```
@ -48,17 +49,17 @@ autopilot {
cluster. Only takes effect if all servers are running Raft protocol version 3 cluster. Only takes effect if all servers are running Raft protocol version 3
or higher. Must be a duration value such as `30s`. or higher. Must be a duration value such as `30s`.
- `redundancy_zone_tag` `(string: "")` - Controls the node-meta key to use when - `enable_redundancy_zones` `(bool: false)` - (Enterprise-only) Controls whether
Autopilot is separating servers into zones for redundancy. Only one server in Autopilot separates servers into zones for redundancy, in conjunction with the
each zone can be a voting member at one time. If left blank, this feature will [redundancy_zone](/docs/agent/configuration/server.html#redundancy_zone) parameter.
be disabled. Only one server in each zone can be a voting member at one time.
- `disable_upgrade_migration` `(bool: false)` - Disables Autopilot's upgrade - `disable_upgrade_migration` `(bool: false)` - (Enterprise-only) Disables Autopilot's
migration strategy in Nomad Enterprise of waiting until enough upgrade migration strategy in Nomad Enterprise of waiting until enough
newer-versioned servers have been added to the cluster before promoting any of newer-versioned servers have been added to the cluster before promoting any of
them to voters. them to voters.
- `upgrade_version_tag` `(string: "")` - Controls the node-meta key to use for - `enable_custom_upgrades` `(bool: false)` - (Enterprise-only) Specifies whether to
version info when performing upgrade migrations. If left blank, the Nomad enable using custom upgrade versions when performing migrations, in conjunction with
version will be used. the [upgrade_version](/docs/agent/configuration/server.html#upgrade_version) parameter.

View file

@ -102,8 +102,9 @@ server {
second is a tradeoff as it lowers failure detection time of nodes at the second is a tradeoff as it lowers failure detection time of nodes at the
tradeoff of false positives and increased load on the leader. tradeoff of false positives and increased load on the leader.
- `non_voting_server` `(bool: false)` - is whether this server will act as - `non_voting_server` `(bool: false)` - (Enterprise-only) Specifies whether
a non-voting member of the cluster to help provide read scalability. (Enterprise-only) this server will act as a non-voting member of the cluster to help provide
read scalability.
- `num_schedulers` `(int: [num-cores])` - Specifies the number of parallel - `num_schedulers` `(int: [num-cores])` - Specifies the number of parallel
scheduler threads to run. This can be as many as one per core, or `0` to scheduler threads to run. This can be as many as one per core, or `0` to
@ -120,6 +121,10 @@ server {
features and is typically not required as the agent internally knows the features and is typically not required as the agent internally knows the
latest version, but may be useful in some upgrade scenarios. latest version, but may be useful in some upgrade scenarios.
- `redundancy_zone` `(string: "")` - (Enterprise-only) Specifies the redundancy
zone that this server will be a part of for Autopilot management. For more
information, see the [Autopilot Guide](/guides/cluster/autopilot.html).
- `rejoin_after_leave` `(bool: false)` - Specifies if Nomad will ignore a - `rejoin_after_leave` `(bool: false)` - Specifies if Nomad will ignore a
previous leave and attempt to rejoin the cluster when starting. By default, previous leave and attempt to rejoin the cluster when starting. By default,
Nomad treats leave as a permanent intent and does not attempt to join the Nomad treats leave as a permanent intent and does not attempt to join the
@ -149,6 +154,10 @@ server {
[server address format](#server-address-format) section for more information [server address format](#server-address-format) section for more information
on the format of the string. on the format of the string.
- `upgrade_version` `(string: "")` - A custom version of the format X.Y.Z to use
in place of the Nomad version when custom upgrades are enabled in Autopilot.
For more information, see the [Autopilot Guide](/guides/cluster/autopilot.html).
### Server Address Format ### Server Address Format
This section describes the acceptable syntax and format for describing the This section describes the acceptable syntax and format for describing the

View file

@ -32,9 +32,9 @@ autopilot {
last_contact_threshold = 200ms last_contact_threshold = 200ms
max_trailing_logs = 250 max_trailing_logs = 250
server_stabilization_time = "10s" server_stabilization_time = "10s"
redundancy_zone_tag = "az" enable_redundancy_zones = false
disable_upgrade_migration = false disable_upgrade_migration = false
upgrade_version_tag = "" enable_custom_upgrades = false
} }
``` ```
@ -49,21 +49,21 @@ CleanupDeadServers = true
LastContactThreshold = 200ms LastContactThreshold = 200ms
MaxTrailingLogs = 250 MaxTrailingLogs = 250
ServerStabilizationTime = 10s ServerStabilizationTime = 10s
RedundancyZoneTag = "" EnableRedundancyZones = false
DisableUpgradeMigration = false DisableUpgradeMigration = false
UpgradeVersionTag = "" EnableCustomUpgrades = false
$ Nomad operator autopilot set-config -cleanup-dead-servers=false $ nomad operator autopilot set-config -cleanup-dead-servers=false
Configuration updated! Configuration updated!
$ Nomad operator autopilot get-config $ nomad operator autopilot get-config
CleanupDeadServers = false CleanupDeadServers = false
LastContactThreshold = 200ms LastContactThreshold = 200ms
MaxTrailingLogs = 250 MaxTrailingLogs = 250
ServerStabilizationTime = 10s ServerStabilizationTime = 10s
RedundancyZoneTag = "" EnableRedundancyZones = false
DisableUpgradeMigration = false DisableUpgradeMigration = false
UpgradeVersionTag = "" EnableCustomUpgrades = false
``` ```
## Dead Server Cleanup ## Dead Server Cleanup
@ -164,15 +164,21 @@ isolated failure domains such as AWS Availability Zones; users would be forced t
have an overly-large quorum (2-3 nodes per AZ) or give up redundancy within an AZ by have an overly-large quorum (2-3 nodes per AZ) or give up redundancy within an AZ by
deploying just one server in each. deploying just one server in each.
If the `RedundancyZoneTag` setting is set, Nomad will use its value to look for a If the `EnableRedundancyZones` setting is set, Nomad will use its value to look for a
zone in each server's specified [`-meta`](/docs/agent/configuration/client.html#meta) zone in each server's specified [`redundancy_zone`]
tag. For example, if `RedundancyZoneTag` is set to `zone`, and `-meta zone=east1a` (/docs/agent/configuration/server.html#redundancy_zone) field.
is used when starting a server, that server's redundancy zone will be `east1a`.
Here's an example showing how to configure this: Here's an example showing how to configure this:
```hcl
/* config.hcl */
server {
redundancy_zone = "west-1"
}
``` ```
$ nomad operator autopilot set-config -redundancy-zone-tag=zone
```
$ nomad operator autopilot set-config -enable-redundancy-zones=true
Configuration updated! Configuration updated!
``` ```
@ -193,27 +199,25 @@ to voters and demoting the old servers. After this is finished, the old servers
safely removed from the cluster. safely removed from the cluster.
To check the Nomad version of the servers, either the [autopilot health] To check the Nomad version of the servers, either the [autopilot health]
(/api/operator.html#read-health) endpoint or the `Nomad members` (/api/operator.html#read-health) endpoint or the `nomad members`
command can be used: command can be used:
``` ```
$ Nomad members $ nomad server-members
Node Address Status Type Build Protocol DC Name Address Port Status Leader Protocol Build Datacenter Region
node1 127.0.0.1:8301 alive server 0.7.5 2 dc1 node1 127.0.0.1 4648 alive true 3 0.7.1 dc1 global
node2 127.0.0.1:8703 alive server 0.7.5 2 dc1 node2 127.0.0.1 4748 alive false 3 0.7.1 dc1 global
node3 127.0.0.1:8803 alive server 0.7.5 2 dc1 node3 127.0.0.1 4848 alive false 3 0.7.1 dc1 global
node4 127.0.0.1:8203 alive server 0.8.0 2 dc1 node4 127.0.0.1 4948 alive false 3 0.8.0 dc1 global
``` ```
### Migrations Without a Nomad Version Change ### Migrations Without a Nomad Version Change
The `UpgradeVersionTag` can be used to override the version information used during The `EnableCustomUpgrades` field can be used to override the version information used during
a migration, so that the migration logic can be used for updating the cluster when a migration, so that the migration logic can be used for updating the cluster when
changing configuration. changing configuration.
If the `UpgradeVersionTag` setting is set, Nomad will use its value to look for a If the `EnableCustomUpgrades` setting is set to `true`, Nomad will use its value to look for a
version in each server's specified [`-meta`](/docs/agent/configuration/client.html#meta) version in each server's specified [`upgrade_version`](/docs/agent/configuration/server.html#upgrade_version)
tag. For example, if `UpgradeVersionTag` is set to `build`, and `-meta build:0.0.2` tag. The upgrade logic will follow semantic versioning and the `upgrade_version`
is used when starting a server, that server's version will be `0.0.2` when considered in
a migration. The upgrade logic will follow semantic versioning and the version string
must be in the form of either `X`, `X.Y`, or `X.Y.Z`. must be in the form of either `X`, `X.Y`, or `X.Y.Z`.