mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
af3d3c433b
This adds a new package containing generic types to be used for defining preference hierarchies. These include prefs.Item, prefs.List, prefs.StructList, and prefs.StructMap. Each of these types represents a configurable preference, holding the preference's state, value, and metadata. The metadata includes the default value (if it differs from the zero value of the Go type) and flags indicating whether a preference is managed via syspolicy or is hidden/read-only for another reason. This information can be marshaled and sent to the GUI, CLI and web clients as a source of truth regarding preference configuration, management, and visibility/mutability states. We plan to use these types to define device preferences, such as the updater preferences, the permission mode to be used on Windows with #tailscale/corp#18342, and certain global options that are currently exposed as tailscaled flags. We also aim to eventually use these types for profile-local preferences in ipn.Prefs and and as a replacement for ipn.MaskedPrefs. The generic preference types are compatible with the tailscale.com/cmd/viewer and tailscale.com/cmd/cloner utilities. Updates #12736 Signed-off-by: Nick Khyl <nickk@tailscale.com>
179 lines
5.4 KiB
Go
179 lines
5.4 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package prefs
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
jsonv2 "github.com/go-json-experiment/json"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
"tailscale.com/types/opt"
|
|
"tailscale.com/types/ptr"
|
|
"tailscale.com/types/views"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
// Item is a single preference item that can be configured.
|
|
// T must either be an immutable type or implement the [views.ViewCloner] interface.
|
|
type Item[T any] struct {
|
|
preference[T]
|
|
}
|
|
|
|
// ItemOf returns an [Item] configured with the specified value and [Options].
|
|
func ItemOf[T any](v T, opts ...Options) Item[T] {
|
|
return Item[T]{preferenceOf(opt.ValueOf(must.Get(deepClone(v))), opts...)}
|
|
}
|
|
|
|
// ItemWithOpts returns an unconfigured [Item] with the specified [Options].
|
|
func ItemWithOpts[T any](opts ...Options) Item[T] {
|
|
return Item[T]{preferenceOf(opt.Value[T]{}, opts...)}
|
|
}
|
|
|
|
// SetValue configures the preference with the specified value.
|
|
// It fails and returns [ErrManaged] if p is a managed preference,
|
|
// and [ErrReadOnly] if p is a read-only preference.
|
|
func (i *Item[T]) SetValue(val T) error {
|
|
return i.preference.SetValue(must.Get(deepClone(val)))
|
|
}
|
|
|
|
// SetManagedValue configures the preference with the specified value
|
|
// and marks the preference as managed.
|
|
func (i *Item[T]) SetManagedValue(val T) {
|
|
i.preference.SetManagedValue(must.Get(deepClone(val)))
|
|
}
|
|
|
|
// Clone returns a copy of i that aliases no memory with i.
|
|
// It is a runtime error to call [Item.Clone] if T contains pointers
|
|
// but does not implement [views.Cloner].
|
|
func (i Item[T]) Clone() *Item[T] {
|
|
res := ptr.To(i)
|
|
if v, ok := i.ValueOk(); ok {
|
|
res.s.Value.Set(must.Get(deepClone(v)))
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Equal reports whether i and i2 are equal.
|
|
// If the template type T implements an Equal(T) bool method, it will be used
|
|
// instead of the == operator for value comparison.
|
|
// If T is not comparable, it reports false.
|
|
func (i Item[T]) Equal(i2 Item[T]) bool {
|
|
if i.s.Metadata != i2.s.Metadata {
|
|
return false
|
|
}
|
|
return i.s.Value.Equal(i2.s.Value)
|
|
}
|
|
|
|
func deepClone[T any](v T) (T, error) {
|
|
if c, ok := any(v).(views.Cloner[T]); ok {
|
|
return c.Clone(), nil
|
|
}
|
|
if !views.ContainsPointers[T]() {
|
|
return v, nil
|
|
}
|
|
var zero T
|
|
return zero, fmt.Errorf("%T contains pointers, but does not implement Clone", v)
|
|
}
|
|
|
|
// ItemView is a read-only view of an [Item][T], where T is a mutable type
|
|
// implementing [views.ViewCloner].
|
|
type ItemView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
|
// ж is the underlying mutable value, named with a hard-to-type
|
|
// character that looks pointy like a pointer.
|
|
// It is named distinctively to make you think of how dangerous it is to escape
|
|
// to callers. You must not let callers be able to mutate it.
|
|
ж *Item[T]
|
|
}
|
|
|
|
// ItemViewOf returns a read-only view of i.
|
|
// It is used by [tailscale.com/cmd/viewer].
|
|
func ItemViewOf[T views.ViewCloner[T, V], V views.StructView[T]](i *Item[T]) ItemView[T, V] {
|
|
return ItemView[T, V]{i}
|
|
}
|
|
|
|
// Valid reports whether the underlying [Item] is non-nil.
|
|
func (iv ItemView[T, V]) Valid() bool {
|
|
return iv.ж != nil
|
|
}
|
|
|
|
// AsStruct implements [views.StructView] by returning a clone of the preference
|
|
// which aliases no memory with the original.
|
|
func (iv ItemView[T, V]) AsStruct() *Item[T] {
|
|
if iv.ж == nil {
|
|
return nil
|
|
}
|
|
return iv.ж.Clone()
|
|
}
|
|
|
|
// IsSet reports whether the preference has a value set.
|
|
func (iv ItemView[T, V]) IsSet() bool {
|
|
return iv.ж.IsSet()
|
|
}
|
|
|
|
// Value returns a read-only view of the value if the preference has a value set.
|
|
// Otherwise, it returns a read-only view of its default value.
|
|
func (iv ItemView[T, V]) Value() V {
|
|
return iv.ж.Value().View()
|
|
}
|
|
|
|
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
|
// Otherwise, it returns an invalid view and false.
|
|
func (iv ItemView[T, V]) ValueOk() (val V, ok bool) {
|
|
if val, ok := iv.ж.ValueOk(); ok {
|
|
return val.View(), true
|
|
}
|
|
return val, false
|
|
}
|
|
|
|
// DefaultValue returns a read-only view of the default value of the preference.
|
|
func (iv ItemView[T, V]) DefaultValue() V {
|
|
return iv.ж.DefaultValue().View()
|
|
}
|
|
|
|
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
|
|
func (iv ItemView[T, V]) IsManaged() bool {
|
|
return iv.ж.IsManaged()
|
|
}
|
|
|
|
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
|
|
func (iv ItemView[T, V]) IsReadOnly() bool {
|
|
return iv.ж.IsReadOnly()
|
|
}
|
|
|
|
// Equal reports whether iv and iv2 are equal.
|
|
func (iv ItemView[T, V]) Equal(iv2 ItemView[T, V]) bool {
|
|
if !iv.Valid() && !iv2.Valid() {
|
|
return true
|
|
}
|
|
if iv.Valid() != iv2.Valid() {
|
|
return false
|
|
}
|
|
return iv.ж.Equal(*iv2.ж)
|
|
}
|
|
|
|
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
|
func (iv ItemView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
|
return iv.ж.MarshalJSONV2(out, opts)
|
|
}
|
|
|
|
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
|
func (iv *ItemView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
|
var x Item[T]
|
|
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
|
return err
|
|
}
|
|
iv.ж = &x
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements [json.Marshaler].
|
|
func (iv ItemView[T, V]) MarshalJSON() ([]byte, error) {
|
|
return jsonv2.Marshal(iv) // uses MarshalJSONV2
|
|
}
|
|
|
|
// UnmarshalJSON implements [json.Unmarshaler].
|
|
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
|
|
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONV2
|
|
}
|