Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions cmd/daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log"
"log/slog"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
Expand Down Expand Up @@ -568,6 +569,23 @@ func main() {
}()
}

if enterpriseControls.HasAppReconcile() {
appInstaller := enterprisecontrol.PilotctlInstaller{BinaryPath: pilotctlBinaryPath()}
reconcileApps(fleetControlCtx, enterpriseControls, appInstaller)
go func() {
ticker := time.NewTicker(enterpriseControls.AppReconcileInterval())
defer ticker.Stop()
for {
select {
case <-fleetControlCtx.Done():
return
case <-ticker.C:
reconcileApps(fleetControlCtx, enterpriseControls, appInstaller)
}
}
}()
}

receiptExportCtx, receiptExportCancel := context.WithCancel(context.Background())
if enterpriseControls.HasReceiptExport() {
if err := enterpriseControls.ExportReceiptsOnce(receiptExportCtx); err != nil {
Expand Down Expand Up @@ -769,6 +787,39 @@ func synchronizeFleetControl(ctx context.Context, controls *enterprisecontrol.Ru
}
}

// pilotctlBinaryPath resolves the pilotctl that ships beside this daemon.
// Preferring the sibling binary over $PATH keeps the verified install path
// pinned to the same release as the daemon rather than to whatever a user
// happens to have earlier in their environment.
func pilotctlBinaryPath() string {
if executable, err := os.Executable(); err == nil {
sibling := filepath.Join(filepath.Dir(executable), "pilotctl")
if info, statErr := os.Stat(sibling); statErr == nil && !info.IsDir() {
return sibling
}
}
if resolved, err := exec.LookPath("pilotctl"); err == nil {
return resolved
}
return "pilotctl"
}

// reconcileApps converges installed apps toward the authority's desired set.
// A failure here must never disturb policy enforcement or the state mirror, so
// it is logged and retried on the next tick rather than propagated.
func reconcileApps(ctx context.Context, controls *enterprisecontrol.Runtime, installer enterprisecontrol.AppInstaller) {
result, err := controls.ReconcileApps(ctx, installer)
if err != nil {
slog.Warn("managed app reconcile failed", "err", err)
return
}
if result.Installed+result.Staged+result.Removed+result.Failed > 0 {
slog.Info("managed apps reconciled",
"desired", result.Desired, "installed", result.Installed,
"awaiting_grants", result.Staged, "removed", result.Removed, "failed", result.Failed)
}
}

func synchronizeFleetState(ctx context.Context, controls *enterprisecontrol.Runtime) {
result, err := controls.SyncFleetState(ctx)
if err != nil {
Expand Down
43 changes: 43 additions & 0 deletions internal/enterprisecontrol/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Config struct {
Receipts *ReceiptConfig `json:"receipts,omitempty"`
Rollout *RolloutConfig `json:"rollout,omitempty"`
Fleet *FleetConfig `json:"fleet,omitempty"`
Apps *AppsConfig `json:"apps,omitempty"`
OutboundDecisions *OutboundDecisionConfig `json:"outbound_decisions,omitempty"`
ActionControl *ActionControlConfig `json:"action_control,omitempty"`
ContentInspection *ContentInspectionConfig `json:"content_inspection,omitempty"`
Expand Down Expand Up @@ -277,6 +278,12 @@ type Runtime struct {
fleetStateRevision uint64
fleetStateRootHash string
fleetStatePendingResults []authority.FleetStateMutationResult
appsEnabled bool
appsInterval time.Duration
appsInstallRoot string
appsStagingRoot string
appsMu sync.Mutex
appsManaged map[string]struct{}
outboundClient *decisionhttp.Client
outboundAgentID string
outboundKeyID string
Expand Down Expand Up @@ -636,6 +643,42 @@ func Load(path string) (*Runtime, error) {
}
}
}
if config.Apps != nil && config.Apps.Enabled {
// Apps ride on the state mirror: the desired document arrives as an
// ordinary signed state mutation, so without state sync there is no
// channel to receive one and nothing to reconcile toward.
if !runtime.fleetStateEnabled {
return nil, fmt.Errorf("enterprise control: app reconciliation requires fleet state sync")
}
home, homeErr := os.UserHomeDir()
if homeErr != nil && (config.Apps.InstallRoot == "" || config.Apps.StagingRoot == "") {
return nil, fmt.Errorf("enterprise control: app roots: %w", homeErr)
}
installRoot := strings.TrimSpace(config.Apps.InstallRoot)
if installRoot == "" {
installRoot = filepath.Join(home, ".pilot", "apps")
}
stagingRoot := strings.TrimSpace(config.Apps.StagingRoot)
if stagingRoot == "" {
// Deliberately a sibling of the install root, never inside it: the
// supervisor scans the install root, and an app awaiting grant
// acceptance must be somewhere it will not be spawned from.
stagingRoot = filepath.Join(home, ".pilot", "apps-pending")
}
if installRoot == stagingRoot {
return nil, fmt.Errorf("enterprise control: app staging_root must differ from install_root")
}
if withinDirectory(installRoot, stagingRoot) {
return nil, fmt.Errorf("enterprise control: app staging_root must not sit inside install_root")
}
runtime.appsEnabled = true
runtime.appsInstallRoot, runtime.appsStagingRoot = installRoot, stagingRoot
runtime.appsManaged = make(map[string]struct{})
runtime.appsInterval = time.Duration(config.Apps.ReconcileIntervalSeconds) * time.Second
if runtime.appsInterval == 0 {
runtime.appsInterval = 30 * time.Second
}
}
if config.DataExchange != nil {
runtime.dataEnabled = true
runtime.dataRequired = config.DataExchange.RequireGoverned
Expand Down
Loading
Loading