Documentation
¶
Overview ¶
Package ddmsync enqueues declarative synchronization commands and clears declarative state on enrollment lifecycle events.
Design ¶
Notifier drains persistent changes by enrollment, coalesces recent edits, builds DeclarativeManagement commands with tokens and calls the normal service enqueue path. Failed work retains attempts and retry state; pushes and events are separate from device acknowledgement. Kick reduces polling latency without making the engine depend on dispatch.
ServiceHook uses enrollment storage to find dependent user channels and calls Engine.ClearEnrollment on Authenticate and CheckOut. Cleanup across MDM and DDM stores is not a distributed transaction.
References ¶
- Decision record 0020: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0020-ddm-engine-membership-and-storage.md
- Decision record 0022: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0022-change-notifier.md
- Decision record 0039: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0039-ddm-is-an-extension-of-mdm.md
- Decision record 0044: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/research/decisions/0044-repository-layout.md
- Threat model: https://github.com/deploymenttheory/go-apple-dm/blob/main/docs/security/threat-model.md (declarative state on re-enrollment)
- Apple: https://developer.apple.com/documentation/devicemanagement/integrating-declarative-management
- Apple: https://developer.apple.com/documentation/devicemanagement/declarativemanagementcommand
Index ¶
Constants ¶
const ( DefaultNotifyWindow = 2 * time.Second DefaultNotifyPoll = time.Second DefaultNotifyBatch = 500 )
Defaults for NotifierConfig.
const DefaultDedupeKey = "ddm"
DefaultDedupeKey is the dedupe key NotifierConfig.DedupeKey uses when it is not set. It suppresses a second DeclarativeManagement while one is still pending for the same enrollment.
Suppression is safe for this command specifically, and only for it: the command is a doorbell, not a payload. A device that receives it fetches the current tokens and declaration items, so a pending command already carries every change made since it was queued. It would not be safe for a command whose payload is the instruction, which is why the key is opt-in per caller rather than a property of the queue.
Variables ¶
var ErrNotifierConfig = errors.New("ddmsync: notifier needs Store, Tokens, and Enqueuer")
ErrNotifierConfig reports a missing required dependency.
Functions ¶
This section is empty.
Types ¶
type DrainResult ¶
type DrainResult struct {
// Deferred enrollments were left for a later drain because a change
// arrived within Window.
Deferred int
// Queued enrollments received a new command; Deduped ones already had
// one pending and were pushed anyway; Skipped ones cannot be
// commanded (disabled or unknown) and were completed without a push.
Queued, Deduped, Skipped int
// Failed enrollments had their change rows scheduled for a retry.
Failed int
// Pushed counts enrollments handed to the Pusher.
Pushed int
}
DrainResult counts what one drain did.// DrainResult counts what one drain did.
func (DrainResult) Empty ¶
func (r DrainResult) Empty() bool
Empty reports a drain that did nothing, so a caller can skip reporting it.
type Enqueuer ¶
type Enqueuer interface {
Enqueue(
ctx context.Context,
ids []mdm.EnrollmentID,
cmd *mdm.Command,
o storage.EnqueueOptions,
) (storage.EnqueueResult, error)
}
Enqueuer queues a command for enrollments; *service.Core satisfies it.
type Notifier ¶
type Notifier struct {
// contains filtered or unexported fields
}
Notifier turns committed change rows into DeclarativeManagement commands and pushes (decision record 0022).
func NewNotifier ¶
func NewNotifier(cfg NotifierConfig) (*Notifier, error)
NewNotifier validates the configuration and applies defaults.
func (*Notifier) DrainOnce ¶
func (n *Notifier) DrainOnce(ctx context.Context) (DrainResult, error)
DrainOnce processes the due change rows once. Store failures are returned; per-enrollment failures are recorded on the rows with backoff.
type NotifierConfig ¶
type NotifierConfig struct {
Store ddm.ChangeStore
Tokens TokenSource
Enqueuer Enqueuer
Pusher Pusher
Bus event.Publisher
Clock clock.Clock
Logger *slog.Logger
// Window defers an enrollment while its newest change is younger than
// this, so a burst of uploads becomes one command. Default 2s.
Window time.Duration
// Poll is how often Run drains without a Kick. Default 1s.
Poll time.Duration
// Batch bounds the change rows read per drain. Default 500.
Batch int
// Backoff maps the attempt count to the retry delay. Default
// storage.NotNowBackoff.
Backoff func(attempt int) time.Duration
// DedupeKey suppresses a new DeclarativeManagement while one is still
// pending for the same enrollment. Nil uses DefaultDedupeKey; an empty
// string turns suppression off, so every drain queues a command.
//
// It is a pointer for the same reason service.Config.ValidateTargets is:
// the zero value has to mean "not set" so that "" can mean "off".
// Whether to suppress is the consumer's decision, not this package's.
DedupeKey *string
// OnDrain, when set, is called with the outcome of every drain. A
// suppressed command is counted in DrainResult.Deduped and is otherwise
// invisible, which is the whole problem with suppression: it has to be
// observable to be defensible.
OnDrain func(ctx context.Context, res DrainResult)
}
NotifierConfig configures NewNotifier. Store, Tokens, and Enqueuer are required; Pusher, Bus, and Logger are optional.
type Pusher ¶
type Pusher interface {
Notify(ctx context.Context, ids []mdm.EnrollmentID) (map[mdm.EnrollmentID]push.Result, error)
}
Pusher sends APNs wake-ups; *pushnotify.Notifier satisfies it.
type ServiceHook ¶
type ServiceHook struct {
// contains filtered or unexported fields
}
ServiceHook clears DDM state after successful checkout or authentication, including the device's user channels. Cleanup failures are logged.
func NewServiceHook ¶
func NewServiceHook(e *ddm.Engine, enrollments storage.EnrollmentStore, log *slog.Logger) *ServiceHook
NewServiceHook builds the hook; enrollments is used to find a device's user channels.
type TokenSource ¶
TokenSource renders an enrollment's TokensResponse; *ddm.Engine satisfies it.