mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-26 11:11:01 +00:00
Merge 6f679d5bc07debf38832dcdd8af990114bff02ff into b3455fa99a5e8d07133d5140017ec7c49f032a07
This commit is contained in:
commit
f3368667bf
@ -80,7 +80,7 @@ func CleanUpArgs(args []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
var localClient = local.Client{
|
||||
var localClient = &local.Client{
|
||||
Socket: paths.DefaultTailscaledSocket(),
|
||||
}
|
||||
|
||||
@ -188,6 +188,7 @@ change in the future.
|
||||
upCmd,
|
||||
downCmd,
|
||||
setCmd,
|
||||
getCmd,
|
||||
loginCmd,
|
||||
logoutCmd,
|
||||
switchCmd,
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var funnelCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
se := &serveEnv{lc: localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
// change is limited to make a revert easier and full cleanup to come after the release.
|
||||
// TODO(tylersmalley): cleanup and removal of newFunnelCommand as of 2023-10-16
|
||||
|
202
cmd/tailscale/cli/get.go
Normal file
202
cmd/tailscale/cli/get.go
Normal file
@ -0,0 +1,202 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
var getCmd = &ffcli.Command{
|
||||
Name: "get",
|
||||
ShortUsage: "tailscale get setting",
|
||||
ShortHelp: "Print specified settings",
|
||||
LongHelp: `"tailscale get" prints a specific setting.
|
||||
|
||||
Only one setting will be printed.
|
||||
|
||||
SETTINGS
|
||||
` + getSettings.Settings(),
|
||||
FlagSet: newFlagSet("get"),
|
||||
Exec: runGet,
|
||||
UsageFunc: usageFuncNoDefaultValues,
|
||||
}
|
||||
|
||||
type getSettingsT map[string]string
|
||||
|
||||
// makeGetSettingsT returns a [getSettingsT] with all of the settings controlled
|
||||
// by the given flagsets. Each setting gets its help text from its flag's Usage.
|
||||
func makeGetSettingsT(flagsets ...*flag.FlagSet) getSettingsT {
|
||||
settings := make(getSettingsT)
|
||||
for _, fs := range flagsets {
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
if _, ok := settings[f.Name]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
settings[f.Name] = f.Usage
|
||||
})
|
||||
}
|
||||
return settings
|
||||
}
|
||||
|
||||
// Settings returns a string of all the settings known to the get command.
|
||||
// The result is formatted for use in help text.
|
||||
func (s getSettingsT) Settings() string {
|
||||
var b strings.Builder
|
||||
names := slices.Sorted(maps.Keys(s))
|
||||
for _, name := range names {
|
||||
usage := s.Usage(name)
|
||||
if strings.HasPrefix(usage, hidden) {
|
||||
continue
|
||||
}
|
||||
b.WriteString(" ")
|
||||
b.WriteString(name)
|
||||
b.WriteString("\n ")
|
||||
b.WriteString(usage)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func lookupPrefOfFlag(p *ipn.Prefs, name string) (reflect.Value, error) {
|
||||
prefs, ok := prefsOfFlag[name]
|
||||
if !ok {
|
||||
return reflect.Value{}, fmt.Errorf("missing pref flag mapping for %s", name)
|
||||
}
|
||||
if len(prefs) != 1 {
|
||||
return reflect.Value{}, fmt.Errorf("expected only one pref flag mapping for %s, not %q", name, prefs)
|
||||
}
|
||||
v := reflect.ValueOf(p).Elem()
|
||||
for _, n := range strings.Split(prefs[0], ".") {
|
||||
v = v.FieldByName(n)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// Lookup returns a function that can be used to look up the associated
|
||||
// preference for a given flag name.
|
||||
func (s getSettingsT) Lookup(name string) func(*ipn.Prefs, *ipnstate.Status) string {
|
||||
if _, ok := s[name]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "advertise-connector":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
value, err := lookupPrefOfFlag(p, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("%v", value.FieldByName("Advertise"))
|
||||
}
|
||||
case "advertise-exit-node":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
return strconv.FormatBool(p.AdvertisesExitNode())
|
||||
}
|
||||
case "advertise-tags":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
value, err := lookupPrefOfFlag(p, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v := value.Interface().([]string)
|
||||
return strings.Join(v, ",")
|
||||
}
|
||||
case "advertise-routes":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
var b strings.Builder
|
||||
for i, r := range p.AdvertiseRoutes {
|
||||
if i > 0 {
|
||||
b.WriteRune(',')
|
||||
}
|
||||
b.WriteString(r.String())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
case "exit-node":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
ip := exitNodeIP(p, st)
|
||||
if ip.IsValid() {
|
||||
return ip.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
case "snat-subnet-routes":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
value, err := lookupPrefOfFlag(p, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("%t", !value.Bool())
|
||||
}
|
||||
case "stateful-filtering":
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
value, err := lookupPrefOfFlag(p, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
v := value.Interface().(opt.Bool)
|
||||
return v.Not().String()
|
||||
}
|
||||
default:
|
||||
return func(p *ipn.Prefs, st *ipnstate.Status) string {
|
||||
value, err := lookupPrefOfFlag(p, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf("%v", value) // fmt prints the concrete value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage returns the usage string for a given flag name.
|
||||
func (s getSettingsT) Usage(name string) string {
|
||||
usage, ok := s[name]
|
||||
if !ok {
|
||||
panic("unknown setting: " + name)
|
||||
}
|
||||
return usage
|
||||
}
|
||||
|
||||
var getSettings = makeGetSettingsT(setFlagSet, upFlagSet)
|
||||
|
||||
func runGet(ctx context.Context, args []string) (retErr error) {
|
||||
if len(args) != 1 {
|
||||
fatalf("must provide only one non-flag argument: %q", args)
|
||||
}
|
||||
|
||||
setting := args[0]
|
||||
lookup := getSettings.Lookup(setting)
|
||||
if lookup == nil {
|
||||
fatalf("unknown setting: %s", setting)
|
||||
}
|
||||
|
||||
prefs, err := localClient.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outln(lookup(prefs, status))
|
||||
return nil
|
||||
}
|
236
cmd/tailscale/cli/get_test.go
Normal file
236
cmd/tailscale/cli/get_test.go
Normal file
@ -0,0 +1,236 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/client/local"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
func TestGetSettingsArePairedWithPrefFlags(t *testing.T) {
|
||||
// Every get setting should have a corresponding prefsOfFlag.
|
||||
// Some prefsOfFlag might not be in getSettings because it is either
|
||||
// a prefless flag or it doesn't apply to this operating system.
|
||||
for name, _ := range getSettings {
|
||||
if _, ok := prefsOfFlag[name]; !ok {
|
||||
t.Errorf("mismatched getter: %s", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSettingsArePairedWithSetFlags(t *testing.T) {
|
||||
// Every set flag should have a corresponding get setting,
|
||||
// except for prefless flags, which don't have get settings.
|
||||
setFlagSet.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
if _, ok := getSettings[f.Name]; !ok {
|
||||
t.Errorf("missing set flag: %s", f.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSettingsArePairedWithUpFlags(t *testing.T) {
|
||||
// Every up flag should have a corresponding get setting,
|
||||
// except for prefless flags, which don't have get settings.
|
||||
upFlagSet.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
if _, ok := getSettings[f.Name]; !ok {
|
||||
t.Errorf("missing up flag: %s", f.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSettingsWithFakeServer(t *testing.T) {
|
||||
for _, tt := range []struct{ flag, value string }{
|
||||
// --nickname is at the top-level in .ProfileName
|
||||
{"nickname", "home"},
|
||||
{"nickname", "work"},
|
||||
// --update-check is nested in .AutoUpdate.Check
|
||||
{"update-check", "false"},
|
||||
{"update-check", "true"},
|
||||
} {
|
||||
name := tt.flag + "=" + tt.value
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Capture outln calls
|
||||
var stdout bytes.Buffer
|
||||
tstest.Replace[io.Writer](t, &Stdout, &stdout)
|
||||
|
||||
// Use a fake localClient that processes settings updates
|
||||
lc := newLocalClient(t)
|
||||
tstest.Replace(t, &localClient, lc)
|
||||
|
||||
// setCmd.FlagSet must be reset to parse arguments
|
||||
cmd := *setCmd
|
||||
cmd.FlagSet = newSetFlagSet(effectiveGOOS(), &setArgs)
|
||||
tstest.Replace(t, &setCmd, &cmd)
|
||||
tstest.Replace(t, &setFlagSet, cmd.FlagSet)
|
||||
|
||||
// Capture errors from setCmd
|
||||
cmd.FlagSet.Init(cmd.FlagSet.Name(), flag.PanicOnError)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatal(r)
|
||||
}
|
||||
}()
|
||||
|
||||
// Capture errors from getCmd
|
||||
tstest.Replace(t, &Fatalf, t.Fatalf)
|
||||
|
||||
arg := "--" + tt.flag + "=" + tt.value
|
||||
t.Logf("tailscale set %s", arg)
|
||||
if err := setCmd.ParseAndRun(t.Context(), []string{arg}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
arg = tt.flag
|
||||
t.Logf("tailscale get %s", arg)
|
||||
if err := runGet(t.Context(), []string{arg}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := stdout.String()
|
||||
want := tt.value + "\n"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultSettings(t *testing.T) {
|
||||
// Fetch the default settings from all of the flags
|
||||
for _, fs := range []*flag.FlagSet{setFlagSet, upFlagSet} {
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run(f.Name, func(t *testing.T) {
|
||||
// Capture outln calls
|
||||
var stdout bytes.Buffer
|
||||
tstest.Replace[io.Writer](t, &Stdout, &stdout)
|
||||
|
||||
// Use a fake localClient that processes settings updates
|
||||
lc := newLocalClient(t)
|
||||
tstest.Replace(t, &localClient, lc)
|
||||
|
||||
if err := runGet(t.Context(), []string{f.Name}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := f.DefValue
|
||||
switch f.Name {
|
||||
case "auto-update":
|
||||
// Unset by tailscale up.
|
||||
want = "unset"
|
||||
case "login-server":
|
||||
// The default settings is empty,
|
||||
// but tailscale up sets it on start.
|
||||
want = ""
|
||||
}
|
||||
want += "\n"
|
||||
|
||||
got := stdout.String()
|
||||
if got != want {
|
||||
t.Errorf("tailscale get %s: got %q, want %q", f.Name, got, want)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
setFlagSet.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
if _, ok := getSettings[f.Name]; !ok {
|
||||
t.Errorf("missing set flag: %s", f.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func newLocalListener(t testing.TB) net.Listener {
|
||||
sock := filepath.Join(t.TempDir(), "sock")
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func newLocalBackend(t testing.TB, logID logid.PublicID) *ipnlocal.LocalBackend {
|
||||
var logf logger.Logf = func(_ string, _ ...any) {}
|
||||
if testing.Verbose() {
|
||||
logf = tstest.WhileTestRunningLogger(t)
|
||||
}
|
||||
|
||||
sys := new(tsd.System)
|
||||
if _, ok := sys.StateStore.GetOK(); !ok {
|
||||
sys.Set(new(mem.Store))
|
||||
}
|
||||
if _, ok := sys.Engine.GetOK(); !ok {
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(eng.Close)
|
||||
|
||||
sys.Set(eng)
|
||||
}
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return lb
|
||||
}
|
||||
|
||||
func newLocalClient(t testing.TB) *local.Client {
|
||||
var logf logger.Logf = func(_ string, _ ...any) {}
|
||||
if testing.Verbose() {
|
||||
logf = tstest.WhileTestRunningLogger(t)
|
||||
}
|
||||
|
||||
logID := logid.PublicID{}
|
||||
|
||||
lb := newLocalBackend(t, logID)
|
||||
t.Cleanup(lb.Shutdown)
|
||||
|
||||
// Connect over Unix domain socket for admin access.
|
||||
l := newLocalListener(t)
|
||||
t.Cleanup(func() { l.Close() })
|
||||
|
||||
srv := ipnserver.New(logf, logID, lb.NetMon())
|
||||
srv.SetLocalBackend(lb)
|
||||
|
||||
go srv.Run(t.Context(), l)
|
||||
|
||||
return &local.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var std net.Dialer
|
||||
return std.DialContext(ctx, "unix", l.Addr().String())
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
var serveCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
se := &serveEnv{lc: localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
// change is limited to make a revert easier and full cleanup to come after the relase.
|
||||
// TODO(tylersmalley): cleanup and removal of newServeLegacyCommand as of 2023-10-16
|
||||
|
@ -68,8 +68,8 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf := newFlagSet("set")
|
||||
|
||||
setf.StringVar(&setArgs.profileName, "nickname", "", "nickname for the current account")
|
||||
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", false, "accept DNS configuration from the admin panel")
|
||||
setf.BoolVar(&setArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
setf.BoolVar(&setArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
setf.StringVar(&setArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
setf.BoolVar(&setArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
setf.BoolVar(&setArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
|
@ -39,7 +39,6 @@ import (
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@ -79,14 +78,8 @@ func effectiveGOOS() string {
|
||||
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
||||
// a function of the platform it's running on.
|
||||
func acceptRouteDefault(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
var p *ipn.Prefs
|
||||
return p.DefaultRouteAll(goos)
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgsGlobal, "up")
|
||||
|
@ -110,7 +110,7 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
Mode: web.LoginServerMode,
|
||||
CGIMode: webArgs.cgi,
|
||||
PathPrefix: webArgs.prefix,
|
||||
LocalClient: &localClient,
|
||||
LocalClient: localClient,
|
||||
}
|
||||
if webArgs.readonly {
|
||||
opts.Mode = web.ReadOnlyServerMode
|
||||
|
19
ipn/prefs.go
19
ipn/prefs.go
@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
@ -664,7 +665,7 @@ func NewPrefs() *Prefs {
|
||||
// Provide default values for options which might be missing
|
||||
// from the json data for any reason. The json can still
|
||||
// override them to false.
|
||||
return &Prefs{
|
||||
p := &Prefs{
|
||||
// ControlURL is explicitly not set to signal that
|
||||
// it's not yet configured, which relaxes the CLI "up"
|
||||
// safety net features. It will get set to DefaultControlURL
|
||||
@ -672,7 +673,6 @@ func NewPrefs() *Prefs {
|
||||
// later anyway.
|
||||
ControlURL: "",
|
||||
|
||||
RouteAll: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
@ -682,6 +682,8 @@ func NewPrefs() *Prefs {
|
||||
Apply: opt.Bool("unset"),
|
||||
},
|
||||
}
|
||||
p.RouteAll = p.DefaultRouteAll(runtime.GOOS)
|
||||
return p
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
@ -711,6 +713,19 @@ func (p *Prefs) ControlURLOrDefault() string {
|
||||
return DefaultControlURL
|
||||
}
|
||||
|
||||
// DefaultRouteAll returns the default value of [Prefs.RouteAll] as a function
|
||||
// of the platform it's running on.
|
||||
func (p *Prefs) DefaultRouteAll(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// AdminPageURL returns the admin web site URL for the current ControlURL.
|
||||
func (p PrefsView) AdminPageURL() string { return p.ж.AdminPageURL() }
|
||||
|
||||
|
@ -24,6 +24,18 @@ func NewBool(b bool) Bool {
|
||||
return Bool(strconv.FormatBool(b))
|
||||
}
|
||||
|
||||
// String implements the [fmt.Stringer] interface.
|
||||
//
|
||||
// It never returns an empty string, since it is easier to read "unset".
|
||||
func (b Bool) String() string {
|
||||
switch b {
|
||||
case "":
|
||||
return "unset"
|
||||
default:
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bool) Set(v bool) {
|
||||
*b = Bool(strconv.FormatBool(v))
|
||||
}
|
||||
@ -41,6 +53,19 @@ func (b Bool) Get() (v bool, ok bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// Not returns the inverse of b, i.e. Bool("true") swapped with Bool("false").
|
||||
// However, b is returned unchanged if it was unset.
|
||||
func (b Bool) Not() Bool {
|
||||
switch b {
|
||||
case "true":
|
||||
return Bool("false")
|
||||
case "false":
|
||||
return Bool("true")
|
||||
default:
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
// Scan implements database/sql.Scanner.
|
||||
func (b *Bool) Scan(src any) error {
|
||||
if src == nil {
|
||||
|
Loading…
x
Reference in New Issue
Block a user