reconcile

package
v0.4.33 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: AGPL-3.0 Imports: 8 Imported by: 0

Documentation

Overview

Package reconcile computes the 3-way plan (git desired ↔ committed baseline ↔ live ABM) for bidirectional, newest-wins sync. Phase 1 computes + reports the plan only; Phase 2 executes it.

Index

Constants

View Source
const NotPersistedMessage = notPersisted

NotPersistedMessage is the operator-facing diagnosis of a 2xx write Apple did not store. Exported so the imperative commands (which issue the same writes against the same API) report it in the same words as the reconcile engine — one incident, one explanation, one place to improve it.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

type Action string

Action is the reconcile verb chosen for one config in the plan.

const (
	Create    Action = "create-abm"   // new in git → POST
	Update    Action = "update-abm"   // changed in git → PATCH
	Pull      Action = "pull-git"     // changed in ABM → write into git
	PullNew   Action = "pull-new-git" // new in ABM (console-created) → write into git
	DeleteABM Action = "delete-abm"   // removed from git → DELETE (prune, gated)
	DeleteGit Action = "delete-git"   // removed from ABM → remove git file
	Conflict  Action = "conflict"     // changed in BOTH → newest-wins (resolved at apply)
)

Reconcile actions, one per planned change to a config.

type Applier

type Applier interface {
	CreateConfiguration(name, xml string, platforms []string) (id, updated string, err error)
	UpdateConfiguration(id, name, xml string) (updated string, err error)
	// FetchCustomSettingDetail re-reads one configuration *with its profile XML* so a
	// write can be confirmed against what Apple actually stored (see push). The name
	// and signature are *ab.Client's own, so the real client keeps satisfying Applier
	// with no adapter.
	FetchCustomSettingDetail(id string) (ab.LiveConfig, error)
	DeleteConfiguration(id string) error
	CreateBlueprint(name, description string, members map[string][]string) (*ab.Resource, error)
	AddBlueprintMembers(bpID, rel, memberType string, ids []string) error
	RemoveBlueprintMembers(bpID, rel, memberType string, ids []string) error
	// BlueprintRelationship re-reads one relationship so a membership write can be
	// confirmed against what Apple actually holds. The project's rule — a 2xx is not
	// proof — was enforced for configuration writes and nowhere for membership, which
	// left `attached` reported on the strength of the response code alone. Signature is
	// *ab.Client's own, so the real client keeps satisfying Applier with no adapter.
	BlueprintRelationship(bpID, rel string) ([]ab.Resource, error)
}

Applier is the subset of the Apple Business write API the executor needs. It is an interface so Apply can be unit-tested with a fake — no production writes.

type Archiver

type Archiver interface {
	Archive(name, reason string, xml []byte, meta map[string]string) (path string, err error)
}

Archiver files a pre-overwrite live profile + sidecar (see internal/archive). Injected so Apply stays disk-free and testable; it returns the archive path.

type BlueprintAction

type BlueprintAction string

BlueprintAction is the reconcile verb for a blueprint or one of its members.

const (
	Attach        BlueprintAction = "attach-config" // member in git, not in ABM → POST membership (per collection below)
	Detach        BlueprintAction = "detach-config" // member in ABM, not in git → DELETE membership (gated --prune)
	AttachApp     BlueprintAction = "attach-app"
	DetachApp     BlueprintAction = "detach-app"
	AttachPackage BlueprintAction = "attach-package"
	DetachPackage BlueprintAction = "detach-package"
	AttachDevice  BlueprintAction = "attach-device"
	DetachDevice  BlueprintAction = "detach-device"
	AttachUser    BlueprintAction = "attach-user"
	DetachUser    BlueprintAction = "detach-user"
	AttachGroup   BlueprintAction = "attach-group"
	DetachGroup   BlueprintAction = "detach-group"
	AdoptConfig   BlueprintAction = "adopt-config" // member in ABM, not in git → write it INTO the manifest (local, no tenant write)
	AdoptApp      BlueprintAction = "adopt-app"
	AdoptPackage  BlueprintAction = "adopt-package"
	AdoptDevice   BlueprintAction = "adopt-device"
	AdoptUser     BlueprintAction = "adopt-user"
	AdoptGroup    BlueprintAction = "adopt-group"
	BlueprintNew  BlueprintAction = "blueprint-new"   // blueprint in git, not in ABM → POST blueprints, then attach members
	BlueprintGone BlueprintAction = "blueprint-adopt" // blueprint in ABM, not in git → run seed to adopt (reported)
)

Blueprint reconcile actions. Attach/detach verbs are collection-qualified; the configurations pair keeps its original values ("attach-config" / "detach-config") so existing JSON consumers are unaffected.

func (BlueprintAction) IsAdopt added in v0.4.20

func (a BlueprintAction) IsAdopt() bool

IsAdopt reports whether the action writes a live member into the git manifest. The "adopt-" prefix is deliberately distinct from the blueprint-level "blueprint-adopt" (a reported-only row), so this never matches that.

func (BlueprintAction) IsAttach added in v0.4.15

func (a BlueprintAction) IsAttach() bool

IsAttach / IsDetach / IsAdopt classify a membership verb regardless of collection.

func (BlueprintAction) IsDetach added in v0.4.15

func (a BlueprintAction) IsDetach() bool

IsDetach reports whether the action is a membership detach (any collection).

type BlueprintItem

type BlueprintItem struct {
	Blueprint  string          `json:"blueprint"`
	BPID       string          `json:"bp_id,omitempty"` // empty for a git-only blueprint (filled by the create at apply time)
	Action     BlueprintAction `json:"action"`
	Collection string          `json:"collection,omitempty"` // member collection key (ab.Collection*); empty on blueprint-level rows and legacy items (= configurations)
	// Config / ConfigID carry the MEMBER display name and ABM id for every
	// collection — the JSON keys predate non-config membership and are kept
	// stable for existing consumers (abgui decodes them).
	Config      string `json:"config,omitempty"`
	ConfigID    string `json:"config_id,omitempty"`
	Description string `json:"description,omitempty"` // blueprint-new: the manifest description the create sends
	Detail      string `json:"detail"`
}

BlueprintItem is one planned blueprint change.

func (BlueprintItem) IsActionable added in v0.4.3

func (it BlueprintItem) IsActionable() bool

IsActionable reports whether sync can perform this item in the current run. blueprint-new is a real CREATE (API v2.0). An attach needs a resolved member id — an empty id means the row is blocked until the member exists in ABM (a config not yet created/adopted, or a member name the tenant doesn't know). An adopt is a LOCAL manifest write and is only ever planned for a member that already resolved, so it is always performable.

type BlueprintOutcome

type BlueprintOutcome struct {
	Blueprint string          `json:"blueprint"`
	Config    string          `json:"config,omitempty"`
	Action    BlueprintAction `json:"action"`
	Status    string          `json:"status"` // "done" | "skipped" | "error"
	Detail    string          `json:"detail"`
}

BlueprintOutcome records what happened to one planned blueprint item.

type BlueprintPlan

type BlueprintPlan struct {
	Items []BlueprintItem `json:"items"`
}

BlueprintPlan is the ordered set of planned blueprint changes.

func ComputeBlueprints

func ComputeBlueprints(desired map[string]gitops.BlueprintSpec, live []ab.LiveBlueprint, idByName map[string]map[string]string, mode MembershipMode) *BlueprintPlan

ComputeBlueprints diffs the git blueprint manifests against live ABM blueprints, per managed member collection. idByName maps collection key → member display name → ABM id; for configurations it is built from the sync baseline (or the post-apply baseline, so freshly-created configs resolve) — an ownership gate, not a full-tenant list. A blueprint is matched by name across git and ABM; a git-only blueprint plans a CREATE followed by its member attaches, and an ABM-only blueprint is reported for adoption (never deleted). mode decides what an ABM-only MEMBER means: detach (GitAuthoritative) or adopt into the manifest (Bidirectional).

func (*BlueprintPlan) HasChanges

func (p *BlueprintPlan) HasChanges() bool

HasChanges reports whether the plan contains any items (actionable or reported).

func (*BlueprintPlan) HasReconcilableChanges

func (p *BlueprintPlan) HasReconcilableChanges() bool

HasReconcilableChanges reports whether the plan has drift that sync can act on. Reported-only rows and attach rows without a member id are excluded, so --exit-on-diff does not loop forever and --apply does not confirm then skip.

func (*BlueprintPlan) ReconcilableCount

func (p *BlueprintPlan) ReconcilableCount() int

ReconcilableCount is the number of items apply can perform (for a confirm prompt).

type BlueprintResult

type BlueprintResult struct {
	Outcomes []BlueprintOutcome `json:"outcomes"`
	Writes   int                `json:"writes"`
	Errors   int                `json:"errors"`
	Skipped  int                `json:"skipped"`
}

BlueprintResult summarizes a blueprint apply run.

type Engine

type Engine struct {
	Client   Applier
	Archiver Archiver
	Files    FileStore
}

Engine executes a reconcile plan against a live tenant. It archives before every overwrite or delete and keeps the committed baseline exact so the next 3-way diff is correct.

func (*Engine) Apply

func (e *Engine) Apply(p *Plan, desired map[string][]byte, live []ab.LiveConfig, base *state.State, opts Opts) *Result

Apply executes the plan. Every error is captured per-item in the Result (Errors count) rather than aborting the run, so independent configs still converge; the baseline (base) is mutated in place and should be saved by the caller only after Apply returns. Archiving always precedes the write it protects — if the archive fails, the write is skipped so the audit trail is never bypassed.

func (*Engine) ApplyBlueprints

func (e *Engine) ApplyBlueprints(p *BlueprintPlan, opts Opts, priorWrites int) *BlueprintResult

ApplyBlueprints executes the blueprint plan: create git-only blueprints, then attach (always) and detach (only with --prune) members via per-member POST/DELETE (the relationship is additive/merges, so this converges). An attach whose blueprint was created this run resolves its id from that create; if the create failed or was skipped, the attach skips benignly. priorWrites is the tenant writes already spent this run (e.g. by config apply) so --limit-writes is a single shared budget. Reported-only items (adopt) are surfaced as skips.

type FileStore

type FileStore interface {
	WriteConfig(name string, content []byte) error
	RemoveConfig(name string) error
	LoadBlueprints() (map[string]gitops.BlueprintSpec, error)
	WriteBlueprintSpec(gitops.BlueprintSpec) error
}

FileStore is the git-side desired-state tree (see internal/gitops). Pull writes a profile; an ABM-side delete removes one; an adopt row records a live blueprint member in its manifest. Every method here writes LOCAL files only — nothing in this interface reaches the tenant.

type Item

type Item struct {
	Name   string `json:"name"`
	Action Action `json:"action"`
	Detail string `json:"detail"`
}

Item is one planned change: the config name, the action, and a human-readable detail.

type MembershipMode added in v0.4.20

type MembershipMode int

MembershipMode selects what a member attached in ABM but absent from the git manifest means for this run — the membership half of --git-source-of-truth.

const (
	// Bidirectional is the default: sync is additive/newest-wins, so an ABM-only
	// member is adopted INTO the manifest (mirrors pull-new-git for configs).
	Bidirectional MembershipMode = iota
	// GitAuthoritative is --git-source-of-truth: the manifest is the complete
	// desired state, so an ABM-only member is detached (gated behind --prune).
	GitAuthoritative
)

type Opts

type Opts struct {
	Prune       bool     // enable DeleteABM (off by default — never prune unasked)
	LimitWrites int      // circuit breaker: max tenant writes per run (0 = unlimited)
	Platforms   []string // configuredForPlatforms for newly-created configs
	Progress    func(string)
	// GitTime resolves the git-side timestamp of a config (last commit time, else
	// file mtime) for newest-wins conflict resolution. ok=false → the timestamp is
	// unknown, so the conflict is skipped rather than guessed (never clobber a side
	// on missing information).
	GitTime func(name string) (t time.Time, ok bool)
}

Opts tunes one apply run.

type Outcome

type Outcome struct {
	Name    string `json:"name"`
	Action  Action `json:"action"` // the effective action (a Conflict resolves to Update or Pull)
	Status  string `json:"status"` // "done" | "skipped" | "error"
	Detail  string `json:"detail"`
	Archive string `json:"archive,omitempty"` // archive path, when one was written
	// ABMID is the configuration id the write targeted (Update) or created (Create).
	// It is recorded on EVERY create/update outcome, including the ones whose baseline
	// entry is deliberately not written (see push) — a caller that wants to re-read an
	// unconfirmed write has no other source for the id, since the baseline only ever
	// describes a confirmed one.
	ABMID string `json:"abm_id,omitempty"`
	// Verified is the read-back verdict for a create/update (empty for everything
	// else). "done" + VerifyUnconfirmed is a real state: the write was made, the
	// confirmation was not obtained, and the baseline was deliberately left alone.
	Verified Verification `json:"verified,omitempty"`
}

Outcome records what happened to one planned item.

type Plan

type Plan struct {
	Items []Item `json:"items"`
}

Plan is the ordered set of changes the reconcile produced.

func Compute

func Compute(desired map[string][]byte, base *state.State, live []ab.LiveConfig) *Plan

Compute diffs desired (git lib/) vs baseline (state) vs live (ABM). Only changed items are returned; unchanged configs are omitted.

func ComputeGitSourceOfTruth added in v0.4.5

func ComputeGitSourceOfTruth(desired map[string][]byte, live []ab.LiveConfig) *Plan

ComputeGitSourceOfTruth treats git as the complete desired state. Live Apple Business configs are only inspected to decide which POST/PATCH/DELETE writes are needed; live-only configs are not pulled into git.

func (*Plan) HasChanges

func (p *Plan) HasChanges() bool

HasChanges reports whether the plan contains any changes.

type Result

type Result struct {
	Outcomes []Outcome `json:"outcomes"`
	Writes   int       `json:"writes"`  // tenant writes performed (POST/PATCH/DELETE)
	Errors   int       `json:"errors"`  // items that failed
	Skipped  int       `json:"skipped"` // items intentionally not applied (prune off / limit reached / unresolved)
}

Result summarizes an apply run.

type Verification added in v0.4.19

type Verification string

Verification is the verdict of the confirming read-back a create/update performs (see push). It is recorded on the Outcome so a caller can report on the write WITHOUT re-reading the config: the GET has already happened, and Apple rate-limits hard, so a second identical fetch buys nothing.

const (
	// VerifyConfirmed: read back from Apple and byte-identical to git. This is the
	// only verdict that lets the baseline be written.
	VerifyConfirmed Verification = "confirmed"
	// VerifyNotPersisted: Apple answered 2xx and did NOT store the profile — the
	// stored bytes still differ, or the write echoed an unchanged updatedDateTime.
	VerifyNotPersisted Verification = "not-persisted"
	// VerifyUnconfirmed: the read-back itself did not produce an answer (it failed,
	// or came back without profile XML). This says nothing about the write, which
	// may well have landed — it is NOT evidence of failure.
	VerifyUnconfirmed Verification = "unconfirmed"
)

Read-back verdicts. Only create/update outcomes carry one; everything else (pull, delete, skip, a write that never reached Apple) leaves it empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL