mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-02 16:01:27 +01:00
ipn{,/local},cmd/tailscale: add "sync" flag and pref to disable control map poll
For manual (human) testing, this lets the user disable control plane map polls with "tailscale set --sync=false" (which survives restarts) and "tailscale set --sync" to restore. A high severity health warning is shown while this is active. Updates #12639 Updates #17945 Change-Id: I83668fa5de3b5e5e25444df0815ec2a859153a6d Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
165a24744e
commit
f1cddc6ecf
@ -63,6 +63,7 @@ type setArgsT struct {
|
|||||||
reportPosture bool
|
reportPosture bool
|
||||||
snat bool
|
snat bool
|
||||||
statefulFiltering bool
|
statefulFiltering bool
|
||||||
|
sync bool
|
||||||
netfilterMode string
|
netfilterMode string
|
||||||
relayServerPort string
|
relayServerPort string
|
||||||
}
|
}
|
||||||
@ -85,6 +86,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
|||||||
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
|
||||||
setf.BoolVar(&setArgs.reportPosture, "report-posture", false, "allow management plane to gather device posture information")
|
setf.BoolVar(&setArgs.reportPosture, "report-posture", false, "allow management plane to gather device posture information")
|
||||||
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
|
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
|
||||||
|
setf.BoolVar(&setArgs.sync, "sync", false, hidden+"actively sync configuration from the control plane (set to false only for network failure testing)")
|
||||||
setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "UDP port number (0 will pick a random unused port) for the relay server to bind to, on all interfaces, or empty string to disable relay server functionality")
|
setf.StringVar(&setArgs.relayServerPort, "relay-server-port", "", "UDP port number (0 will pick a random unused port) for the relay server to bind to, on all interfaces, or empty string to disable relay server functionality")
|
||||||
|
|
||||||
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
|
||||||
@ -149,6 +151,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
|||||||
OperatorUser: setArgs.opUser,
|
OperatorUser: setArgs.opUser,
|
||||||
NoSNAT: !setArgs.snat,
|
NoSNAT: !setArgs.snat,
|
||||||
ForceDaemon: setArgs.forceDaemon,
|
ForceDaemon: setArgs.forceDaemon,
|
||||||
|
Sync: opt.NewBool(setArgs.sync),
|
||||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||||
Check: setArgs.updateCheck,
|
Check: setArgs.updateCheck,
|
||||||
Apply: opt.NewBool(setArgs.updateApply),
|
Apply: opt.NewBool(setArgs.updateApply),
|
||||||
|
|||||||
@ -890,6 +890,7 @@ func init() {
|
|||||||
addPrefFlagMapping("advertise-connector", "AppConnector")
|
addPrefFlagMapping("advertise-connector", "AppConnector")
|
||||||
addPrefFlagMapping("report-posture", "PostureChecking")
|
addPrefFlagMapping("report-posture", "PostureChecking")
|
||||||
addPrefFlagMapping("relay-server-port", "RelayServerPort")
|
addPrefFlagMapping("relay-server-port", "RelayServerPort")
|
||||||
|
addPrefFlagMapping("sync", "Sync")
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||||
@ -925,7 +926,7 @@ func updateMaskedPrefsFromUpOrSetFlag(mp *ipn.MaskedPrefs, flagName string) {
|
|||||||
if prefs, ok := prefsOfFlag[flagName]; ok {
|
if prefs, ok := prefsOfFlag[flagName]; ok {
|
||||||
for _, pref := range prefs {
|
for _, pref := range prefs {
|
||||||
f := reflect.ValueOf(mp).Elem()
|
f := reflect.ValueOf(mp).Elem()
|
||||||
for _, name := range strings.Split(pref, ".") {
|
for name := range strings.SplitSeq(pref, ".") {
|
||||||
f = f.FieldByName(name + "Set")
|
f = f.FieldByName(name + "Set")
|
||||||
}
|
}
|
||||||
f.SetBool(true)
|
f.SetBool(true)
|
||||||
|
|||||||
@ -90,6 +90,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
|||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
AdvertiseServices []string
|
AdvertiseServices []string
|
||||||
|
Sync opt.Bool
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
NoStatefulFiltering opt.Bool
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
|||||||
@ -363,6 +363,12 @@ func (v PrefsView) AdvertiseServices() views.Slice[string] {
|
|||||||
return views.SliceOf(v.ж.AdvertiseServices)
|
return views.SliceOf(v.ж.AdvertiseServices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync is whether this node should sync its configuration from
|
||||||
|
// the control plane. If unset, this defaults to true.
|
||||||
|
// This exists primarily for testing, to verify that netmap caching
|
||||||
|
// and offline operation work correctly.
|
||||||
|
func (v PrefsView) Sync() opt.Bool { return v.ж.Sync }
|
||||||
|
|
||||||
// NoSNAT specifies whether to source NAT traffic going to
|
// NoSNAT specifies whether to source NAT traffic going to
|
||||||
// destinations in AdvertiseRoutes. The default is to apply source
|
// destinations in AdvertiseRoutes. The default is to apply source
|
||||||
// NAT, which makes the traffic appear to come from the router
|
// NAT, which makes the traffic appear to come from the router
|
||||||
@ -482,6 +488,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
|||||||
Egg bool
|
Egg bool
|
||||||
AdvertiseRoutes []netip.Prefix
|
AdvertiseRoutes []netip.Prefix
|
||||||
AdvertiseServices []string
|
AdvertiseServices []string
|
||||||
|
Sync opt.Bool
|
||||||
NoSNAT bool
|
NoSNAT bool
|
||||||
NoStatefulFiltering opt.Bool
|
NoStatefulFiltering opt.Bool
|
||||||
NetfilterMode preftype.NetfilterMode
|
NetfilterMode preftype.NetfilterMode
|
||||||
|
|||||||
@ -870,6 +870,7 @@ func (b *LocalBackend) initPrefsFromConfig(conf *conffile.Config) error {
|
|||||||
if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
b.updateWarnSync(p.View())
|
||||||
b.setStaticEndpointsFromConfigLocked(conf)
|
b.setStaticEndpointsFromConfigLocked(conf)
|
||||||
b.conf = conf
|
b.conf = conf
|
||||||
return nil
|
return nil
|
||||||
@ -931,7 +932,12 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||||
b.cc.SetPaused((b.state == ipn.Stopped && b.NetMap() != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
|
pauseForNetwork := (b.state == ipn.Stopped && b.NetMap() != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest())
|
||||||
|
|
||||||
|
prefs := b.pm.CurrentPrefs()
|
||||||
|
pauseForSyncPref := prefs.Valid() && prefs.Sync().EqualBool(false)
|
||||||
|
|
||||||
|
b.cc.SetPaused(pauseForNetwork || pauseForSyncPref)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisconnectControl shuts down control client. This can be run before node shutdown to force control to consider this ndoe
|
// DisconnectControl shuts down control client. This can be run before node shutdown to force control to consider this ndoe
|
||||||
@ -2519,6 +2525,7 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error {
|
|||||||
logf("serverMode=%v", inServerMode)
|
logf("serverMode=%v", inServerMode)
|
||||||
}
|
}
|
||||||
b.applyPrefsToHostinfoLocked(hostinfo, prefs)
|
b.applyPrefsToHostinfoLocked(hostinfo, prefs)
|
||||||
|
b.updateWarnSync(prefs)
|
||||||
|
|
||||||
persistv := prefs.Persist().AsStruct()
|
persistv := prefs.Persist().AsStruct()
|
||||||
if persistv == nil {
|
if persistv == nil {
|
||||||
@ -2570,6 +2577,7 @@ func (b *LocalBackend) startLocked(opts ipn.Options) error {
|
|||||||
ControlKnobs: b.sys.ControlKnobs(),
|
ControlKnobs: b.sys.ControlKnobs(),
|
||||||
Shutdown: ccShutdown,
|
Shutdown: ccShutdown,
|
||||||
Bus: b.sys.Bus.Get(),
|
Bus: b.sys.Bus.Get(),
|
||||||
|
StartPaused: prefs.Sync().EqualBool(false),
|
||||||
|
|
||||||
// Don't warn about broken Linux IP forwarding when
|
// Don't warn about broken Linux IP forwarding when
|
||||||
// netstack is being used.
|
// netstack is being used.
|
||||||
@ -4658,6 +4666,9 @@ func (b *LocalBackend) setPrefsLocked(newp *ipn.Prefs) ipn.PrefsView {
|
|||||||
b.resetAlwaysOnOverrideLocked()
|
b.resetAlwaysOnOverrideLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.pauseOrResumeControlClientLocked() // for prefs.Sync changes
|
||||||
|
b.updateWarnSync(prefs)
|
||||||
|
|
||||||
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
|
if oldp.ShieldsUp() != newp.ShieldsUp || hostInfoChanged {
|
||||||
b.doSetHostinfoFilterServicesLocked()
|
b.doSetHostinfoFilterServicesLocked()
|
||||||
}
|
}
|
||||||
@ -6665,6 +6676,13 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
|||||||
return b.sshServer, nil
|
return b.sshServer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var warnSyncDisabled = health.Register(&health.Warnable{
|
||||||
|
Code: "sync-disabled",
|
||||||
|
Title: "Tailscale Sync is Disabled",
|
||||||
|
Severity: health.SeverityHigh,
|
||||||
|
Text: health.StaticMessage("Tailscale control plane syncing is disabled; run `tailscale set --sync` to restore"),
|
||||||
|
})
|
||||||
|
|
||||||
var warnSSHSELinuxWarnable = health.Register(&health.Warnable{
|
var warnSSHSELinuxWarnable = health.Register(&health.Warnable{
|
||||||
Code: "ssh-unavailable-selinux-enabled",
|
Code: "ssh-unavailable-selinux-enabled",
|
||||||
Title: "Tailscale SSH and SELinux",
|
Title: "Tailscale SSH and SELinux",
|
||||||
@ -6680,6 +6698,14 @@ func (b *LocalBackend) updateSELinuxHealthWarning() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) updateWarnSync(prefs ipn.PrefsView) {
|
||||||
|
if prefs.Sync().EqualBool(false) {
|
||||||
|
b.health.SetUnhealthy(warnSyncDisabled, nil)
|
||||||
|
} else {
|
||||||
|
b.health.SetHealthy(warnSyncDisabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) handleSSHConn(c net.Conn) (err error) {
|
func (b *LocalBackend) handleSSHConn(c net.Conn) (err error) {
|
||||||
s, err := b.sshServerOrInit()
|
s, err := b.sshServerOrInit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -1129,10 +1129,12 @@ func TestProfileStateChangeCallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gotChanges := make([]stateChange, 0, len(tt.wantChanges))
|
gotChanges := make([]stateChange, 0, len(tt.wantChanges))
|
||||||
pm.StateChangeHook = func(profile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
|
pm.StateChangeHook = func(profile ipn.LoginProfileView, prefView ipn.PrefsView, sameNode bool) {
|
||||||
|
prefs := prefView.AsStruct()
|
||||||
|
prefs.Sync = prefs.Sync.Normalized()
|
||||||
gotChanges = append(gotChanges, stateChange{
|
gotChanges = append(gotChanges, stateChange{
|
||||||
Profile: profile.AsStruct(),
|
Profile: profile.AsStruct(),
|
||||||
Prefs: prefs.AsStruct(),
|
Prefs: prefs,
|
||||||
SameNode: sameNode,
|
SameNode: sameNode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
20
ipn/prefs.go
20
ipn/prefs.go
@ -207,6 +207,12 @@ type Prefs struct {
|
|||||||
// control server.
|
// control server.
|
||||||
AdvertiseServices []string
|
AdvertiseServices []string
|
||||||
|
|
||||||
|
// Sync is whether this node should sync its configuration from
|
||||||
|
// the control plane. If unset, this defaults to true.
|
||||||
|
// This exists primarily for testing, to verify that netmap caching
|
||||||
|
// and offline operation work correctly.
|
||||||
|
Sync opt.Bool
|
||||||
|
|
||||||
// NoSNAT specifies whether to source NAT traffic going to
|
// NoSNAT specifies whether to source NAT traffic going to
|
||||||
// destinations in AdvertiseRoutes. The default is to apply source
|
// destinations in AdvertiseRoutes. The default is to apply source
|
||||||
// NAT, which makes the traffic appear to come from the router
|
// NAT, which makes the traffic appear to come from the router
|
||||||
@ -364,12 +370,13 @@ type MaskedPrefs struct {
|
|||||||
EggSet bool `json:",omitempty"`
|
EggSet bool `json:",omitempty"`
|
||||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||||
AdvertiseServicesSet bool `json:",omitempty"`
|
AdvertiseServicesSet bool `json:",omitempty"`
|
||||||
|
SyncSet bool `json:",omitzero"`
|
||||||
NoSNATSet bool `json:",omitempty"`
|
NoSNATSet bool `json:",omitempty"`
|
||||||
NoStatefulFilteringSet bool `json:",omitempty"`
|
NoStatefulFilteringSet bool `json:",omitempty"`
|
||||||
NetfilterModeSet bool `json:",omitempty"`
|
NetfilterModeSet bool `json:",omitempty"`
|
||||||
OperatorUserSet bool `json:",omitempty"`
|
OperatorUserSet bool `json:",omitempty"`
|
||||||
ProfileNameSet bool `json:",omitempty"`
|
ProfileNameSet bool `json:",omitempty"`
|
||||||
AutoUpdateSet AutoUpdatePrefsMask `json:",omitempty"`
|
AutoUpdateSet AutoUpdatePrefsMask `json:",omitzero"`
|
||||||
AppConnectorSet bool `json:",omitempty"`
|
AppConnectorSet bool `json:",omitempty"`
|
||||||
PostureCheckingSet bool `json:",omitempty"`
|
PostureCheckingSet bool `json:",omitempty"`
|
||||||
NetfilterKindSet bool `json:",omitempty"`
|
NetfilterKindSet bool `json:",omitempty"`
|
||||||
@ -547,6 +554,9 @@ func (p *Prefs) pretty(goos string) string {
|
|||||||
if p.LoggedOut {
|
if p.LoggedOut {
|
||||||
sb.WriteString("loggedout=true ")
|
sb.WriteString("loggedout=true ")
|
||||||
}
|
}
|
||||||
|
if p.Sync.EqualBool(false) {
|
||||||
|
sb.WriteString("sync=false ")
|
||||||
|
}
|
||||||
if p.ForceDaemon {
|
if p.ForceDaemon {
|
||||||
sb.WriteString("server=true ")
|
sb.WriteString("server=true ")
|
||||||
}
|
}
|
||||||
@ -653,6 +663,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
|||||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||||
p.CorpDNS == p2.CorpDNS &&
|
p.CorpDNS == p2.CorpDNS &&
|
||||||
p.RunSSH == p2.RunSSH &&
|
p.RunSSH == p2.RunSSH &&
|
||||||
|
p.Sync.Normalized() == p2.Sync.Normalized() &&
|
||||||
p.RunWebClient == p2.RunWebClient &&
|
p.RunWebClient == p2.RunWebClient &&
|
||||||
p.WantRunning == p2.WantRunning &&
|
p.WantRunning == p2.WantRunning &&
|
||||||
p.LoggedOut == p2.LoggedOut &&
|
p.LoggedOut == p2.LoggedOut &&
|
||||||
@ -956,10 +967,15 @@ func PrefsFromBytes(b []byte, base *Prefs) error {
|
|||||||
if len(b) == 0 {
|
if len(b) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return json.Unmarshal(b, base)
|
return json.Unmarshal(b, base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Prefs) normalizeOptBools() {
|
||||||
|
if p.Sync == opt.ExplicitlyUnset {
|
||||||
|
p.Sync = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var jsonEscapedZero = []byte(`\u0000`)
|
var jsonEscapedZero = []byte(`\u0000`)
|
||||||
|
|
||||||
// LoadPrefsWindows loads a legacy relaynode config file into Prefs with
|
// LoadPrefsWindows loads a legacy relaynode config file into Prefs with
|
||||||
|
|||||||
@ -57,6 +57,7 @@ func TestPrefsEqual(t *testing.T) {
|
|||||||
"Egg",
|
"Egg",
|
||||||
"AdvertiseRoutes",
|
"AdvertiseRoutes",
|
||||||
"AdvertiseServices",
|
"AdvertiseServices",
|
||||||
|
"Sync",
|
||||||
"NoSNAT",
|
"NoSNAT",
|
||||||
"NoStatefulFiltering",
|
"NoStatefulFiltering",
|
||||||
"NetfilterMode",
|
"NetfilterMode",
|
||||||
@ -404,6 +405,7 @@ func checkPrefs(t *testing.T, p Prefs) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PrefsFromBytes(p2) failed: bytes=%q; err=%v\n", p2.ToBytes(), err)
|
t.Fatalf("PrefsFromBytes(p2) failed: bytes=%q; err=%v\n", p2.ToBytes(), err)
|
||||||
}
|
}
|
||||||
|
p2b.normalizeOptBools()
|
||||||
p2p := p2.Pretty()
|
p2p := p2.Pretty()
|
||||||
p2bp := p2b.Pretty()
|
p2bp := p2b.Pretty()
|
||||||
t.Logf("\np2p: %#v\np2bp: %#v\n", p2p, p2bp)
|
t.Logf("\np2p: %#v\np2bp: %#v\n", p2p, p2bp)
|
||||||
@ -419,6 +421,42 @@ func checkPrefs(t *testing.T, p Prefs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrefsFromBytes documents that it preserves fields unset in the JSON.
|
||||||
|
// This verifies that stays true.
|
||||||
|
func TestPrefsFromBytesPreservesOldValues(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
old Prefs
|
||||||
|
json []byte
|
||||||
|
want Prefs
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "preserve-control-url",
|
||||||
|
old: Prefs{ControlURL: "https://foo"},
|
||||||
|
json: []byte(`{"RouteAll": true}`),
|
||||||
|
want: Prefs{ControlURL: "https://foo", RouteAll: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "opt.Bool", // test that we don't normalize it early
|
||||||
|
old: Prefs{Sync: "unset"},
|
||||||
|
json: []byte(`{}`),
|
||||||
|
want: Prefs{Sync: "unset"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
old := tt.old // shallow
|
||||||
|
err := PrefsFromBytes(tt.json, &old)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PrefsFromBytes failed: %v", err)
|
||||||
|
}
|
||||||
|
if !old.Equals(&tt.want) {
|
||||||
|
t.Fatalf("got %+v; want %+v", old, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBasicPrefs(t *testing.T) {
|
func TestBasicPrefs(t *testing.T) {
|
||||||
tstest.PanicOnLog()
|
tstest.PanicOnLog()
|
||||||
|
|
||||||
@ -591,6 +629,11 @@ func TestPrefsPretty(t *testing.T) {
|
|||||||
"linux",
|
"linux",
|
||||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Prefs{Sync: "false"},
|
||||||
|
"linux",
|
||||||
|
"Prefs{ra=false dns=false want=false sync=false routes=[] nf=off update=off Persist=nil}",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
got := tt.p.pretty(tt.os)
|
got := tt.p.pretty(tt.os)
|
||||||
|
|||||||
@ -83,6 +83,17 @@ func (b *Bool) Scan(src any) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalized returns the normalized form of b, mapping "unset" to ""
|
||||||
|
// and leaving other values unchanged.
|
||||||
|
func (b Bool) Normalized() Bool {
|
||||||
|
switch b {
|
||||||
|
case ExplicitlyUnset:
|
||||||
|
return Empty
|
||||||
|
default:
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EqualBool reports whether b is equal to v.
|
// EqualBool reports whether b is equal to v.
|
||||||
// If b is empty or not a valid bool, it reports false.
|
// If b is empty or not a valid bool, it reports false.
|
||||||
func (b Bool) EqualBool(v bool) bool {
|
func (b Bool) EqualBool(v bool) bool {
|
||||||
|
|||||||
@ -106,6 +106,8 @@ func TestBoolEqualBool(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"", true, false},
|
{"", true, false},
|
||||||
{"", false, false},
|
{"", false, false},
|
||||||
|
{"unset", true, false},
|
||||||
|
{"unset", false, false},
|
||||||
{"sdflk;", true, false},
|
{"sdflk;", true, false},
|
||||||
{"sldkf;", false, false},
|
{"sldkf;", false, false},
|
||||||
{"true", true, true},
|
{"true", true, true},
|
||||||
@ -122,6 +124,24 @@ func TestBoolEqualBool(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBoolNormalized(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
in Bool
|
||||||
|
want Bool
|
||||||
|
}{
|
||||||
|
{"", ""},
|
||||||
|
{"true", "true"},
|
||||||
|
{"false", "false"},
|
||||||
|
{"unset", ""},
|
||||||
|
{"foo", "foo"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := tt.in.Normalized(); got != tt.want {
|
||||||
|
t.Errorf("(%q).Normalized() = %q; want %q", string(tt.in), string(got), string(tt.want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnmarshalAlloc(t *testing.T) {
|
func TestUnmarshalAlloc(t *testing.T) {
|
||||||
b := json.Unmarshaler(new(Bool))
|
b := json.Unmarshaler(new(Bool))
|
||||||
n := testing.AllocsPerRun(10, func() { b.UnmarshalJSON(trueBytes) })
|
n := testing.AllocsPerRun(10, func() { b.UnmarshalJSON(trueBytes) })
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user