util: add syspolicy package (#9550)
Add a more generalized package for getting policies. Updates tailcale/corp#10967 Signed-off-by: Claire Wang <claire@tailscale.com> Co-authored-by: Adrian Dewhurst <adrian@tailscale.com>main
parent
d71184d674
commit
32c0156311
@ -0,0 +1,52 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy |
||||
|
||||
import ( |
||||
"errors" |
||||
"sync/atomic" |
||||
) |
||||
|
||||
var ( |
||||
handlerUsed atomic.Bool |
||||
handler Handler = defaultHandler{} |
||||
) |
||||
|
||||
// Handler reads system policies from OS-specific storage.
|
||||
type Handler interface { |
||||
// ReadString reads the policy settings value string given the key.
|
||||
ReadString(key string) (string, error) |
||||
// ReadUInt64 reads the policy settings uint64 value given the key.
|
||||
ReadUInt64(key string) (uint64, error) |
||||
} |
||||
|
||||
// ErrNoSuchKey is returned when the specified key does not have a value set.
|
||||
var ErrNoSuchKey = errors.New("no such key") |
||||
|
||||
// defaultHandler is the catch all syspolicy type for anything that isn't windows or apple.
|
||||
type defaultHandler struct{} |
||||
|
||||
func (defaultHandler) ReadString(_ string) (string, error) { |
||||
return "", ErrNoSuchKey |
||||
} |
||||
|
||||
func (defaultHandler) ReadUInt64(_ string) (uint64, error) { |
||||
return 0, ErrNoSuchKey |
||||
} |
||||
|
||||
// markHandlerInUse is called before handler methods are called.
|
||||
func markHandlerInUse() { |
||||
handlerUsed.Store(true) |
||||
} |
||||
|
||||
// RegisterHandler initializes the policy handler and ensures registration will happen once.
|
||||
func RegisterHandler(h Handler) { |
||||
// Technically this assignment is not concurrency safe, but in the
|
||||
// event that there was any risk of a data race, we will panic due to
|
||||
// the CompareAndSwap failing.
|
||||
handler = h |
||||
if !handlerUsed.CompareAndSwap(false, true) { |
||||
panic("handler was already used before registration") |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy |
||||
|
||||
import "testing" |
||||
|
||||
func TestDefaultHandlerReadValues(t *testing.T) { |
||||
var h defaultHandler |
||||
|
||||
got, err := h.ReadString(string(AdminConsoleVisibility)) |
||||
if got != "" || err != ErrNoSuchKey { |
||||
t.Fatalf("got %v err %v", got, err) |
||||
} |
||||
result, err := h.ReadUInt64(string(LogSCMInteractions)) |
||||
if result != 0 || err != ErrNoSuchKey { |
||||
t.Fatalf("got %v err %v", result, err) |
||||
} |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy |
||||
|
||||
import ( |
||||
"errors" |
||||
|
||||
"tailscale.com/util/winutil" |
||||
) |
||||
|
||||
type windowsHandler struct{} |
||||
|
||||
func init() { |
||||
RegisterHandler(windowsHandler{}) |
||||
} |
||||
|
||||
func (windowsHandler) ReadString(key string) (string, error) { |
||||
s, err := winutil.GetPolicyString(key) |
||||
if errors.Is(err, winutil.ErrNoValue) { |
||||
err = ErrNoSuchKey |
||||
} |
||||
return s, err |
||||
} |
||||
|
||||
func (windowsHandler) ReadUInt64(key string) (uint64, error) { |
||||
value, err := winutil.GetPolicyInteger(key) |
||||
if errors.Is(err, winutil.ErrNoValue) { |
||||
err = ErrNoSuchKey |
||||
} |
||||
return value, err |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy |
||||
|
||||
type Key string |
||||
|
||||
const ( |
||||
// Keys with a string value
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
|
||||
// Keys with a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated.
|
||||
EnableIncomingConnections Key = "AllowIncomingConnections" |
||||
EnableServerMode Key = "UnattendedMode" |
||||
|
||||
// Keys with a string value that controls visibility: "show", "hide".
|
||||
// The default is "show" unless otherwise stated.
|
||||
AdminConsoleVisibility Key = "AdminConsole" |
||||
NetworkDevicesVisibility Key = "NetworkDevices" |
||||
TestMenuVisibility Key = "TestMenu" |
||||
UpdateMenuVisibility Key = "UpdateMenu" |
||||
RunExitNodeVisibility Key = "RunExitNode" |
||||
PreferencesMenuVisibility Key = "PreferencesMenu" |
||||
|
||||
// Keys with a string value formatted for use with time.ParseDuration().
|
||||
KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
|
||||
|
||||
// Boolean Keys that are only applicable on Windows. Booleans are stored in the registry as
|
||||
// DWORD or QWORD (either is acceptable). 0 means false, and anything else means true.
|
||||
// The default is 0 unless otherwise stated.
|
||||
LogSCMInteractions Key = "LogSCMInteractions" |
||||
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock" |
||||
) |
||||
@ -0,0 +1,172 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package syspolicy provides functions to retrieve system settings of a device.
|
||||
package syspolicy |
||||
|
||||
import ( |
||||
"errors" |
||||
"time" |
||||
) |
||||
|
||||
func GetString(key Key, defaultValue string) (string, error) { |
||||
markHandlerInUse() |
||||
v, err := handler.ReadString(string(key)) |
||||
if errors.Is(err, ErrNoSuchKey) { |
||||
return defaultValue, nil |
||||
} |
||||
return v, err |
||||
} |
||||
|
||||
func GetUint64(key Key, defaultValue uint64) (uint64, error) { |
||||
markHandlerInUse() |
||||
v, err := handler.ReadUInt64(string(key)) |
||||
if errors.Is(err, ErrNoSuchKey) { |
||||
return defaultValue, nil |
||||
} |
||||
return v, err |
||||
} |
||||
|
||||
// PreferenceOption is a policy that governs whether a boolean variable
|
||||
// is forcibly assigned an administrator-defined value, or allowed to receive
|
||||
// a user-defined value.
|
||||
type PreferenceOption int |
||||
|
||||
const ( |
||||
showChoiceByPolicy PreferenceOption = iota |
||||
neverByPolicy |
||||
alwaysByPolicy |
||||
) |
||||
|
||||
// Show returns if the UI option that controls the choice administered by this
|
||||
// policy should be shown. Currently this is true if and only if the policy is
|
||||
// showChoiceByPolicy.
|
||||
func (p PreferenceOption) Show() bool { |
||||
return p == showChoiceByPolicy |
||||
} |
||||
|
||||
// ShouldEnable checks if the choice administered by this policy should be
|
||||
// enabled. If the administrator has chosen a setting, the administrator's
|
||||
// setting is returned, otherwise userChoice is returned.
|
||||
func (p PreferenceOption) ShouldEnable(userChoice bool) bool { |
||||
switch p { |
||||
case neverByPolicy: |
||||
return false |
||||
case alwaysByPolicy: |
||||
return true |
||||
default: |
||||
return userChoice |
||||
} |
||||
} |
||||
|
||||
// GetPreferenceOption loads a policy from the registry that can be
|
||||
// managed by an enterprise policy management system and allows administrative
|
||||
// overrides of users' choices in a way that we do not want tailcontrol to have
|
||||
// the authority to set. It describes user-decides/always/never options, where
|
||||
// "always" and "never" remove the user's ability to make a selection. If not
|
||||
// present or set to a different value, "user-decides" is the default.
|
||||
func GetPreferenceOption(name Key) (PreferenceOption, error) { |
||||
opt, err := GetString(name, "user-decides") |
||||
if err != nil { |
||||
return showChoiceByPolicy, err |
||||
} |
||||
switch opt { |
||||
case "always": |
||||
return alwaysByPolicy, nil |
||||
case "never": |
||||
return neverByPolicy, nil |
||||
default: |
||||
return showChoiceByPolicy, nil |
||||
} |
||||
} |
||||
|
||||
// Visibility is a policy that controls whether or not a particular
|
||||
// component of a user interface is to be shown.
|
||||
type Visibility byte |
||||
|
||||
const ( |
||||
visibleByPolicy Visibility = 'v' |
||||
hiddenByPolicy Visibility = 'h' |
||||
) |
||||
|
||||
// Show reports whether the UI option administered by this policy should be shown.
|
||||
// Currently this is true if and only if the policy is visibleByPolicy.
|
||||
func (p Visibility) Show() bool { |
||||
return p == visibleByPolicy |
||||
} |
||||
|
||||
// GetVisibility loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes show/hide decisions
|
||||
// for UI elements. The registry value should be a string set to "show" (return
|
||||
// true) or "hide" (return true). If not present or set to a different value,
|
||||
// "show" (return false) is the default.
|
||||
func GetVisibility(name Key) (Visibility, error) { |
||||
opt, err := GetString(name, "show") |
||||
if err != nil { |
||||
return visibleByPolicy, err |
||||
} |
||||
switch opt { |
||||
case "hide": |
||||
return hiddenByPolicy, nil |
||||
default: |
||||
return visibleByPolicy, nil |
||||
} |
||||
} |
||||
|
||||
// GetDuration loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes a duration for some
|
||||
// action. The registry value should be a string that time.ParseDuration
|
||||
// understands. If the registry value is "" or can not be processed,
|
||||
// defaultValue is returned instead.
|
||||
func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) { |
||||
opt, err := GetString(name, "") |
||||
if opt == "" || err != nil { |
||||
return defaultValue, err |
||||
} |
||||
v, err := time.ParseDuration(opt) |
||||
if err != nil || v < 0 { |
||||
return defaultValue, nil |
||||
} |
||||
return v, nil |
||||
} |
||||
|
||||
// SelectControlURL returns the ControlURL to use based on a value in
|
||||
// the registry (LoginURL) and the one on disk (in the GUI's
|
||||
// prefs.conf). If both are empty, it returns a default value. (It
|
||||
// always return a non-empty value)
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/2798 for some background.
|
||||
func SelectControlURL(reg, disk string) string { |
||||
const def = "https://controlplane.tailscale.com" |
||||
|
||||
// Prior to Dec 2020's commit 739b02e6, the installer
|
||||
// wrote a LoginURL value of https://login.tailscale.com to the registry.
|
||||
const oldRegDef = "https://login.tailscale.com" |
||||
|
||||
// If they have an explicit value in the registry, use it,
|
||||
// unless it's an old default value from an old installer.
|
||||
// Then we have to see which is better.
|
||||
if reg != "" { |
||||
if reg != oldRegDef { |
||||
// Something explicit in the registry that we didn't
|
||||
// set ourselves by the installer.
|
||||
return reg |
||||
} |
||||
if disk == "" { |
||||
// Something in the registry is better than nothing on disk.
|
||||
return reg |
||||
} |
||||
if disk != def && disk != oldRegDef { |
||||
// The value in the registry is the old
|
||||
// default (login.tailscale.com) but the value
|
||||
// on disk is neither our old nor new default
|
||||
// value, so it must be some custom thing that
|
||||
// the user cares about. Prefer the disk value.
|
||||
return disk |
||||
} |
||||
} |
||||
if disk != "" { |
||||
return disk |
||||
} |
||||
return def |
||||
} |
||||
@ -0,0 +1,375 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy |
||||
|
||||
import ( |
||||
"errors" |
||||
"testing" |
||||
"time" |
||||
) |
||||
|
||||
// testHandler encompasses all data types returned when testing any of the syspolicy
|
||||
// methods that involve getting a policy value.
|
||||
// For keys and the corresponding values, check policy_keys.go.
|
||||
type testHandler struct { |
||||
t *testing.T |
||||
key Key |
||||
s string |
||||
u64 uint64 |
||||
err error |
||||
} |
||||
|
||||
var someOtherError = errors.New("error other than not found") |
||||
|
||||
func setHandlerForTest(tb testing.TB, h Handler) { |
||||
tb.Helper() |
||||
oldHandler := handler |
||||
handler = h |
||||
tb.Cleanup(func() { handler = oldHandler }) |
||||
} |
||||
|
||||
func (th *testHandler) ReadString(key string) (string, error) { |
||||
if key != string(th.key) { |
||||
th.t.Errorf("ReadString(%q) want %q", key, th.key) |
||||
} |
||||
return th.s, th.err |
||||
} |
||||
|
||||
func (th *testHandler) ReadUInt64(key string) (uint64, error) { |
||||
if key != string(th.key) { |
||||
th.t.Errorf("ReadUint64(%q) want %q", key, th.key) |
||||
} |
||||
return th.u64, th.err |
||||
} |
||||
|
||||
func TestGetString(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
key Key |
||||
handlerValue string |
||||
handlerError error |
||||
defaultValue string |
||||
wantValue string |
||||
wantError error |
||||
}{ |
||||
{ |
||||
name: "read existing value", |
||||
key: AdminConsoleVisibility, |
||||
handlerValue: "hide", |
||||
wantValue: "hide", |
||||
}, |
||||
{ |
||||
name: "read non-existing value", |
||||
key: EnableServerMode, |
||||
handlerError: ErrNoSuchKey, |
||||
wantError: nil, |
||||
}, |
||||
{ |
||||
name: "read non-existing value, non-blank default", |
||||
key: EnableServerMode, |
||||
handlerError: ErrNoSuchKey, |
||||
defaultValue: "test", |
||||
wantValue: "test", |
||||
wantError: nil, |
||||
}, |
||||
{ |
||||
name: "reading value returns other error", |
||||
key: NetworkDevicesVisibility, |
||||
handlerError: someOtherError, |
||||
wantError: someOtherError, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
setHandlerForTest(t, &testHandler{ |
||||
t: t, |
||||
key: tt.key, |
||||
s: tt.handlerValue, |
||||
err: tt.handlerError, |
||||
}) |
||||
value, err := GetString(tt.key, tt.defaultValue) |
||||
if err != tt.wantError { |
||||
t.Errorf("err=%q, want %q", err, tt.wantError) |
||||
} |
||||
if value != tt.wantValue { |
||||
t.Errorf("value=%v, want %v", value, tt.wantValue) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGetUint64(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
key Key |
||||
handlerValue uint64 |
||||
handlerError error |
||||
defaultValue uint64 |
||||
wantValue uint64 |
||||
wantError error |
||||
}{ |
||||
{ |
||||
name: "read existing value", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerValue: 1, |
||||
wantValue: 1, |
||||
}, |
||||
{ |
||||
name: "read non-existing value", |
||||
key: LogSCMInteractions, |
||||
handlerValue: 0, |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: 0, |
||||
}, |
||||
{ |
||||
name: "read non-existing value, non-zero default", |
||||
key: LogSCMInteractions, |
||||
defaultValue: 2, |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: 2, |
||||
}, |
||||
{ |
||||
name: "reading value returns other error", |
||||
key: FlushDNSOnSessionUnlock, |
||||
handlerError: someOtherError, |
||||
wantError: someOtherError, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
setHandlerForTest(t, &testHandler{ |
||||
t: t, |
||||
key: tt.key, |
||||
u64: tt.handlerValue, |
||||
err: tt.handlerError, |
||||
}) |
||||
value, err := GetUint64(tt.key, tt.defaultValue) |
||||
if err != tt.wantError { |
||||
t.Errorf("err=%q, want %q", err, tt.wantError) |
||||
} |
||||
if value != tt.wantValue { |
||||
t.Errorf("value=%v, want %v", value, tt.wantValue) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGetPreferenceOption(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
key Key |
||||
handlerValue string |
||||
handlerError error |
||||
wantValue PreferenceOption |
||||
wantError error |
||||
}{ |
||||
{ |
||||
name: "always by policy", |
||||
key: EnableIncomingConnections, |
||||
handlerValue: "always", |
||||
wantValue: alwaysByPolicy, |
||||
}, |
||||
{ |
||||
name: "never by policy", |
||||
key: EnableIncomingConnections, |
||||
handlerValue: "never", |
||||
wantValue: neverByPolicy, |
||||
}, |
||||
{ |
||||
name: "use default", |
||||
key: EnableIncomingConnections, |
||||
handlerValue: "", |
||||
wantValue: showChoiceByPolicy, |
||||
}, |
||||
{ |
||||
name: "read non-existing value", |
||||
key: EnableIncomingConnections, |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: showChoiceByPolicy, |
||||
}, |
||||
{ |
||||
name: "other error is returned", |
||||
key: EnableIncomingConnections, |
||||
handlerError: someOtherError, |
||||
wantValue: showChoiceByPolicy, |
||||
wantError: someOtherError, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
setHandlerForTest(t, &testHandler{ |
||||
t: t, |
||||
key: tt.key, |
||||
s: tt.handlerValue, |
||||
err: tt.handlerError, |
||||
}) |
||||
option, err := GetPreferenceOption(tt.key) |
||||
if err != tt.wantError { |
||||
t.Errorf("err=%q, want %q", err, tt.wantError) |
||||
} |
||||
if option != tt.wantValue { |
||||
t.Errorf("option=%v, want %v", option, tt.wantValue) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGetVisibility(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
key Key |
||||
handlerValue string |
||||
handlerError error |
||||
wantValue Visibility |
||||
wantError error |
||||
}{ |
||||
{ |
||||
name: "hidden by policy", |
||||
key: AdminConsoleVisibility, |
||||
handlerValue: "hide", |
||||
wantValue: hiddenByPolicy, |
||||
}, |
||||
{ |
||||
name: "visibility default", |
||||
key: AdminConsoleVisibility, |
||||
handlerValue: "show", |
||||
wantValue: visibleByPolicy, |
||||
}, |
||||
{ |
||||
name: "read non-existing value", |
||||
key: AdminConsoleVisibility, |
||||
handlerValue: "show", |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: visibleByPolicy, |
||||
}, |
||||
{ |
||||
name: "other error is returned", |
||||
key: AdminConsoleVisibility, |
||||
handlerValue: "show", |
||||
handlerError: someOtherError, |
||||
wantValue: visibleByPolicy, |
||||
wantError: someOtherError, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
setHandlerForTest(t, &testHandler{ |
||||
t: t, |
||||
key: tt.key, |
||||
s: tt.handlerValue, |
||||
err: tt.handlerError, |
||||
}) |
||||
visibility, err := GetVisibility(tt.key) |
||||
if err != tt.wantError { |
||||
t.Errorf("err=%q, want %q", err, tt.wantError) |
||||
} |
||||
if visibility != tt.wantValue { |
||||
t.Errorf("visibility=%v, want %v", visibility, tt.wantValue) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestGetDuration(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
key Key |
||||
handlerValue string |
||||
handlerError error |
||||
defaultValue time.Duration |
||||
wantValue time.Duration |
||||
wantError error |
||||
}{ |
||||
{ |
||||
name: "read existing value", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerValue: "2h", |
||||
wantValue: 2 * time.Hour, |
||||
defaultValue: 24 * time.Hour, |
||||
}, |
||||
{ |
||||
name: "invalid duration value", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerValue: "-20", |
||||
wantValue: 24 * time.Hour, |
||||
defaultValue: 24 * time.Hour, |
||||
}, |
||||
{ |
||||
name: "read non-existing value", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: 24 * time.Hour, |
||||
defaultValue: 24 * time.Hour, |
||||
}, |
||||
{ |
||||
name: "read non-existing value different default", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerError: ErrNoSuchKey, |
||||
wantValue: 0 * time.Second, |
||||
defaultValue: 0 * time.Second, |
||||
}, |
||||
{ |
||||
name: "other error is returned", |
||||
key: KeyExpirationNoticeTime, |
||||
handlerError: someOtherError, |
||||
wantValue: 24 * time.Hour, |
||||
wantError: someOtherError, |
||||
defaultValue: 24 * time.Hour, |
||||
}, |
||||
} |
||||
|
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
setHandlerForTest(t, &testHandler{ |
||||
t: t, |
||||
key: tt.key, |
||||
s: tt.handlerValue, |
||||
err: tt.handlerError, |
||||
}) |
||||
duration, err := GetDuration(tt.key, tt.defaultValue) |
||||
if err != tt.wantError { |
||||
t.Errorf("err=%q, want %q", err, tt.wantError) |
||||
} |
||||
if duration != tt.wantValue { |
||||
t.Errorf("duration=%v, want %v", duration, tt.wantValue) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestSelectControlURL(t *testing.T) { |
||||
tests := []struct { |
||||
reg, disk, want string |
||||
}{ |
||||
// Modern default case.
|
||||
{"", "", "https://controlplane.tailscale.com"}, |
||||
|
||||
// For a user who installed prior to Dec 2020, with
|
||||
// stuff in their registry.
|
||||
{"https://login.tailscale.com", "", "https://login.tailscale.com"}, |
||||
|
||||
// Ignore pre-Dec'20 LoginURL from installer if prefs
|
||||
// prefs overridden manually to an on-prem control
|
||||
// server.
|
||||
{"https://login.tailscale.com", "http://on-prem", "http://on-prem"}, |
||||
|
||||
// Something unknown explicitly set in the registry always wins.
|
||||
{"http://explicit-reg", "", "http://explicit-reg"}, |
||||
{"http://explicit-reg", "http://on-prem", "http://explicit-reg"}, |
||||
{"http://explicit-reg", "https://login.tailscale.com", "http://explicit-reg"}, |
||||
{"http://explicit-reg", "https://controlplane.tailscale.com", "http://explicit-reg"}, |
||||
|
||||
// If nothing in the registry, disk wins.
|
||||
{"", "http://on-prem", "http://on-prem"}, |
||||
} |
||||
for _, tt := range tests { |
||||
if got := SelectControlURL(tt.reg, tt.disk); got != tt.want { |
||||
t.Errorf("(reg %q, disk %q) = %q; want %q", tt.reg, tt.disk, got, tt.want) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue