Skip to content

Commit

Permalink
util/syspolicy: add rsop package that provides access to the resultan…
Browse files Browse the repository at this point in the history
…t policy

In this PR we add syspolicy/rsop package that facilitates policy source registration
and provides access to the resultant policy merged from all registered sources for a
given scope.

Updates tailscale#12687

Signed-off-by: Nick Khyl <[email protected]>
  • Loading branch information
nickkhyl committed Oct 16, 2024
1 parent 2aa9125 commit ff5f233
Show file tree
Hide file tree
Showing 9 changed files with 1,834 additions and 18 deletions.
3 changes: 3 additions & 0 deletions util/syspolicy/internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import (
"tailscale.com/version"
)

// Init facilitates deferred invocation of initializers.
var Init lazy.DeferredInit

// OSForTesting is the operating system override used for testing.
// It follows the same naming convention as [version.OS].
var OSForTesting lazy.SyncValue[string]
Expand Down
107 changes: 107 additions & 0 deletions util/syspolicy/rsop/change_callbacks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package rsop

import (
"reflect"
"slices"
"sync"
"time"

"tailscale.com/util/set"
"tailscale.com/util/syspolicy/internal/loggerx"
"tailscale.com/util/syspolicy/setting"
)

// Change represents a change from the Old to the New value of type T.
type Change[T any] struct {
New, Old T
}

// PolicyChangeCallback is a function called whenever a policy changes.
type PolicyChangeCallback func(*PolicyChange)

// PolicyChange describes a policy change.
type PolicyChange struct {
snapshots Change[*setting.Snapshot]
}

// New returns the [setting.Snapshot] after the change.
func (c PolicyChange) New() *setting.Snapshot {
return c.snapshots.New
}

// Old returns the [setting.Snapshot] before the change.
func (c PolicyChange) Old() *setting.Snapshot {
return c.snapshots.Old
}

// HasChanged reports whether a policy setting with the specified [setting.Key], has changed.
func (c PolicyChange) HasChanged(key setting.Key) bool {
new, newErr := c.snapshots.New.GetErr(key)
old, oldErr := c.snapshots.Old.GetErr(key)
if newErr != nil && oldErr != nil {
return false
}
if newErr != nil || oldErr != nil {
return true
}
switch newVal := new.(type) {
case bool, uint64, string, setting.Visibility, setting.PreferenceOption, time.Duration:
return newVal != old
case []string:
oldVal, ok := old.([]string)
return !ok || !slices.Equal(newVal, oldVal)
default:
loggerx.Errorf("[unexpected] %q has an unsupported value type: %T", key, newVal)
return !reflect.DeepEqual(new, old)
}
}

// policyChangeCallbacks are the callbacks to invoke when the effective policy changes.
// It is safe for concurrent use.
type policyChangeCallbacks struct {
mu sync.Mutex
cbs set.HandleSet[PolicyChangeCallback]
}

// Register adds the specified callback to be invoked whenever the policy changes.
func (c *policyChangeCallbacks) Register(callback PolicyChangeCallback) (unregister func()) {
c.mu.Lock()
handle := c.cbs.Add(callback)
c.mu.Unlock()
return func() {
c.mu.Lock()
delete(c.cbs, handle)
c.mu.Unlock()
}
}

// Invoke calls the registered callback functions with the specified policy change info.
func (c *policyChangeCallbacks) Invoke(snapshots Change[*setting.Snapshot]) {
var wg sync.WaitGroup
defer wg.Wait()

c.mu.Lock()
defer c.mu.Unlock()

wg.Add(len(c.cbs))
change := &PolicyChange{snapshots: snapshots}
for _, cb := range c.cbs {
go func() {
defer wg.Done()
cb(change)
}()
}
}

// Close awaits the completion of active callbacks and prevents any further invocations.
func (c *policyChangeCallbacks) Close() {
c.mu.Lock()
defer c.mu.Unlock()
if c.cbs != nil {
clear(c.cbs)
c.cbs = nil
}
}
Loading

0 comments on commit ff5f233

Please sign in to comment.