mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-20 23:41:28 +02:00
150 lines
5.2 KiB
Go
150 lines
5.2 KiB
Go
package mdm
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// defaultReloadInterval is the production cadence at which the desktop daemon
|
|
// re-reads the OS-native MDM policy. Picked to balance responsiveness against
|
|
// registry/plist I/O overhead. Mobile builds use OS-side notifications
|
|
// instead and bypass this ticker entirely. Unexported on purpose: callers do
|
|
// not pass it — NewTicker owns the default (see reloadInterval).
|
|
const defaultReloadInterval = 1 * time.Minute
|
|
|
|
// testReloadInterval is the cadence used under `go test` (detected via
|
|
// testing.Testing()) so the reload path is exercised in seconds rather than
|
|
// minutes. It has no effect on production builds, where testing.Testing()
|
|
// always returns false.
|
|
const testReloadInterval = 1 * time.Second
|
|
|
|
// reloadInterval returns the production cadence, or the accelerated test
|
|
// cadence when running under `go test`. Centralising the choice here keeps
|
|
// reloadInterval selects the polling interval used to re-read the OS-native MDM policy.
|
|
// It returns testReloadInterval when tests are running (testing.Testing() == true) and defaultReloadInterval otherwise.
|
|
func reloadInterval() time.Duration {
|
|
if testing.Testing() {
|
|
return testReloadInterval
|
|
}
|
|
return defaultReloadInterval
|
|
}
|
|
|
|
// policyLoader is the indirection through which the ticker reads the
|
|
// OS-native policy, both for the initial observation and on every tick.
|
|
// Production points it at LoadPolicy; tests in this package override it to
|
|
// feed a scripted sequence of policies without touching the real OS store.
|
|
var policyLoader = LoadPolicy
|
|
|
|
// Ticker periodically re-reads the OS-native MDM policy via LoadPolicy and
|
|
// invokes onChange whenever the observed Policy diverges from the last
|
|
// observation (added / removed / changed keys). Launch with Run from a
|
|
// goroutine; cancel the supplied context to stop.
|
|
type Ticker struct {
|
|
interval time.Duration
|
|
onChange func(prev, curr *Policy)
|
|
prev *Policy
|
|
}
|
|
|
|
// NewTicker constructs a Ticker that re-reads the OS-native policy every
|
|
// reloadInterval() and invokes onChange on any diff. The cadence is owned by
|
|
// reloadInterval (production default, accelerated under `go test`); callers
|
|
// NewTicker creates a Ticker that polls the OS-native MDM policy at the package reload interval and invokes onChange when a policy change is detected.
|
|
// If onChange is nil the ticker will only log detected changes.
|
|
// The ticker's initial snapshot is populated by loading the current policy.
|
|
func NewTicker(onChange func(prev, curr *Policy)) *Ticker {
|
|
return &Ticker{
|
|
interval: reloadInterval(),
|
|
onChange: onChange,
|
|
prev: policyLoader(),
|
|
}
|
|
}
|
|
|
|
// Run blocks until ctx is cancelled, polling the OS-native policy store at
|
|
// the configured cadence and emitting log lines + onChange callback on
|
|
// every observed diff.
|
|
func (t *Ticker) Run(ctx context.Context) {
|
|
tk := time.NewTicker(t.interval)
|
|
defer tk.Stop()
|
|
log.Infof("MDM policy reload ticker started (interval=%s)", t.interval)
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Info("MDM policy reload ticker stopped")
|
|
return
|
|
case <-tk.C:
|
|
curr := policyLoader()
|
|
if PoliciesEqual(t.prev, curr) {
|
|
continue
|
|
}
|
|
added, removed, changed := diffPolicies(t.prev, curr)
|
|
log.Infof("MDM policy changed: added=%v removed=%v changed=%v",
|
|
added, removed, changed)
|
|
prev := t.prev
|
|
t.prev = curr
|
|
if t.onChange != nil {
|
|
t.onChange(prev, curr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// PoliciesEqual reports whether two Policy instances carry the same managed
|
|
// value maps for deep equality.
|
|
func PoliciesEqual(a, b *Policy) bool {
|
|
if a.IsEmpty() && b.IsEmpty() {
|
|
return true
|
|
}
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
return reflect.DeepEqual(a.values, b.values)
|
|
}
|
|
|
|
// diffPolicies returns the keys added in curr, removed from prev, and whose
|
|
// diffPolicies reports keys that were added, removed, or changed between two policies.
|
|
// The returned slices contain keys present only in `curr` (added), only in `prev` (removed),
|
|
// and present in both but whose values differ (changed). Each slice is sorted
|
|
// lexicographically for stable logging output; value differences are determined
|
|
// using deep equality.
|
|
func diffPolicies(prev, curr *Policy) (added, removed, changed []string) {
|
|
prevKeys := mapOf(prev)
|
|
currKeys := mapOf(curr)
|
|
for k := range currKeys {
|
|
if _, ok := prevKeys[k]; !ok {
|
|
added = append(added, k)
|
|
} else if !reflect.DeepEqual(prevKeys[k], currKeys[k]) {
|
|
changed = append(changed, k)
|
|
}
|
|
}
|
|
for k := range prevKeys {
|
|
if _, ok := currKeys[k]; !ok {
|
|
removed = append(removed, k)
|
|
}
|
|
}
|
|
sort.Strings(added)
|
|
sort.Strings(removed)
|
|
sort.Strings(changed)
|
|
return added, removed, changed
|
|
}
|
|
|
|
// mapOf returns a (possibly empty, never nil) copy of the underlying values
|
|
// map of a Policy so callers outside this package can compare across the
|
|
// mapOf returns a non-nil copy of the given Policy's key/value map.
|
|
// If p is nil, mapOf returns an empty map; otherwise it returns a newly
|
|
// allocated map containing the same key/value pairs as p.values.
|
|
func mapOf(p *Policy) map[string]any {
|
|
if p == nil {
|
|
return map[string]any{}
|
|
}
|
|
out := make(map[string]any, len(p.values))
|
|
for k, v := range p.values {
|
|
out[k] = v
|
|
}
|
|
return out
|
|
}
|