mirror of
https://github.com/tailscale/tailscale.git
synced 2025-11-16 19:07:42 +00:00
types/prefs: add a package containing generic preference types
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>
This commit is contained in:
179
types/prefs/prefs.go
Normal file
179
types/prefs/prefs.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package prefs contains types and functions to work with arbitrary
|
||||
// preference hierarchies.
|
||||
//
|
||||
// Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap]
|
||||
// types which represent individual preferences in a user-defined prefs struct.
|
||||
// A valid prefs struct must contain one or more exported fields of the preference types,
|
||||
// either directly or within nested structs, but not pointers to these types.
|
||||
// Additionally to preferences, a prefs struct may contain any number of
|
||||
// non-preference fields that will be marshalled and unmarshalled but are
|
||||
// otherwise ignored by the prefs package.
|
||||
//
|
||||
// The preference types are compatible with the [tailscale.com/cmd/viewer] and
|
||||
// [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view
|
||||
// of the user-defined prefs structure and use it in place of prefs whenever the prefs
|
||||
// should not be modified.
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrManaged is the error returned when attempting to modify a managed preference.
|
||||
ErrManaged = errors.New("cannot modify a managed preference")
|
||||
// ErrReadOnly is the error returned when attempting to modify a readonly preference.
|
||||
ErrReadOnly = errors.New("cannot modify a readonly preference")
|
||||
)
|
||||
|
||||
// metadata holds type-agnostic preference metadata.
|
||||
type metadata struct {
|
||||
// Managed indicates whether the preference is managed via MDM, Group Policy, or other means.
|
||||
Managed bool `json:",omitzero"`
|
||||
|
||||
// ReadOnly indicates whether the preference is read-only due to any other reasons,
|
||||
// such as user's access rights.
|
||||
ReadOnly bool `json:",omitzero"`
|
||||
}
|
||||
|
||||
// serializable is a JSON-serializable preference data.
|
||||
type serializable[T any] struct {
|
||||
// Value is an optional preference value that is set when the preference is
|
||||
// configured by the user or managed by an admin.
|
||||
Value opt.Value[T] `json:",omitzero"`
|
||||
// Default is the default preference value to be used
|
||||
// when the preference has not been configured.
|
||||
Default T `json:",omitzero"`
|
||||
// Metadata is any additional type-agnostic preference metadata to be serialized.
|
||||
Metadata metadata `json:",inline"`
|
||||
}
|
||||
|
||||
// preference is an embeddable type that provides a common implementation for
|
||||
// concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap].
|
||||
type preference[T any] struct {
|
||||
s serializable[T]
|
||||
}
|
||||
|
||||
// preferenceOf returns a preference with the specified value and/or [Options].
|
||||
func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] {
|
||||
var m metadata
|
||||
for _, o := range opts {
|
||||
o(&m)
|
||||
}
|
||||
return preference[T]{serializable[T]{Value: v, Metadata: m}}
|
||||
}
|
||||
|
||||
// IsSet reports whether p has a value set.
|
||||
func (p preference[T]) IsSet() bool {
|
||||
return p.s.Value.IsSet()
|
||||
}
|
||||
|
||||
// Value returns the value of p if the preference has a value set.
|
||||
// Otherwise, it returns its default value.
|
||||
func (p preference[T]) Value() T {
|
||||
val, _ := p.ValueOk()
|
||||
return val
|
||||
}
|
||||
|
||||
// ValueOk returns the value of p and true if the preference has a value set.
|
||||
// Otherwise, it returns its default value and false.
|
||||
func (p preference[T]) ValueOk() (val T, ok bool) {
|
||||
if val, ok = p.s.Value.GetOk(); ok {
|
||||
return val, true
|
||||
}
|
||||
return p.DefaultValue(), false
|
||||
}
|
||||
|
||||
// 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 (p *preference[T]) SetValue(val T) error {
|
||||
switch {
|
||||
case p.s.Metadata.Managed:
|
||||
return ErrManaged
|
||||
case p.s.Metadata.ReadOnly:
|
||||
return ErrReadOnly
|
||||
default:
|
||||
p.s.Value.Set(val)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ClearValue resets the preference to an unconfigured state.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (p *preference[T]) ClearValue() error {
|
||||
switch {
|
||||
case p.s.Metadata.Managed:
|
||||
return ErrManaged
|
||||
case p.s.Metadata.ReadOnly:
|
||||
return ErrReadOnly
|
||||
default:
|
||||
p.s.Value.Clear()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultValue returns the default value of p.
|
||||
func (p preference[T]) DefaultValue() T {
|
||||
return p.s.Default
|
||||
}
|
||||
|
||||
// SetDefaultValue sets the default value of p.
|
||||
func (p *preference[T]) SetDefaultValue(def T) {
|
||||
p.s.Default = def
|
||||
}
|
||||
|
||||
// IsManaged reports whether p is managed via MDM, Group Policy, or similar means.
|
||||
func (p preference[T]) IsManaged() bool {
|
||||
return p.s.Metadata.Managed
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (p *preference[T]) SetManagedValue(val T) {
|
||||
p.s.Value.Set(val)
|
||||
p.s.Metadata.Managed = true
|
||||
}
|
||||
|
||||
// ClearManaged clears the managed flag of the preference without altering its value.
|
||||
func (p *preference[T]) ClearManaged() {
|
||||
p.s.Metadata.Managed = false
|
||||
}
|
||||
|
||||
// IsReadOnly reports whether p is read-only and cannot be changed by user.
|
||||
func (p preference[T]) IsReadOnly() bool {
|
||||
return p.s.Metadata.ReadOnly || p.s.Metadata.Managed
|
||||
}
|
||||
|
||||
// SetReadOnly sets the read-only status of p, preventing changes by a user if set to true.
|
||||
func (p *preference[T]) SetReadOnly(readonly bool) {
|
||||
p.s.Metadata.ReadOnly = readonly
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (p preference[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return jsonv2.MarshalEncode(out, &p.s, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (p *preference[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
return jsonv2.UnmarshalDecode(in, &p.s, opts)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (p preference[T]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(p) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (p *preference[T]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
|
||||
}
|
||||
Reference in New Issue
Block a user