package app import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "sync" "time" "proxyctl/internal/profile" "proxyctl/internal/target" ) type IntentKind int const ( CreateProfile IntentKind = iota + 1 UpdateProfile DeleteProfile ActivateProfile RefreshTargets EnableTarget DisableTarget ApplyProfileToTarget ReapplyActiveProfile RestoreOriginal ) type Intent struct { Kind IntentKind Name string Profile profile.Profile Confirm bool ConfirmationToken string TargetID string } func confirmationToken(parts ...string) string { hash := sha256.New() for _, part := range parts { _, _ = hash.Write([]byte(part)) _, _ = hash.Write([]byte{0}) } return hex.EncodeToString(hash.Sum(nil)) } // TargetActionKind identifies a Target action independently of display labels. type TargetActionKind uint8 const ( EnableTargetAction TargetActionKind = iota + 1 DisableTargetAction ApplyProfileTargetAction ResolveDriftTargetAction ReapplyActiveProfileAction RestoreOriginalAction ) // TargetAction exposes whether an intent is currently available and, when it // is not, the reason presentation layers should show instead of ignoring it. type TargetAction struct { Kind TargetActionKind Available bool DisabledReason string } type targetActionPolicy struct { kind TargetActionKind intent IntentKind state target.State desired bool primary bool requiresProfile bool operation string } var targetActionPolicies = []targetActionPolicy{ {kind: EnableTargetAction, intent: EnableTarget, state: target.Disabled, desired: true, primary: true, requiresProfile: true, operation: "enable"}, {kind: DisableTargetAction, intent: DisableTarget, state: target.Enabled, primary: true, operation: "disable"}, {kind: ApplyProfileTargetAction, intent: ApplyProfileToTarget, state: target.External, desired: true, primary: true, requiresProfile: true, operation: "apply-Profile"}, {kind: ResolveDriftTargetAction, state: target.Drifted, primary: true}, {kind: ReapplyActiveProfileAction, intent: ReapplyActiveProfile, state: target.Drifted, desired: true, requiresProfile: true, operation: "reapply-Active-Profile"}, {kind: RestoreOriginalAction, intent: RestoreOriginal, state: target.Drifted, operation: "restore-original"}, } func targetActionPolicyForKind(kind TargetActionKind) (targetActionPolicy, bool) { for _, policy := range targetActionPolicies { if policy.kind == kind { return policy, true } } return targetActionPolicy{}, false } // Intent creates the Application intent represented by a structured Target // action. Presentation layers use this as the sole action-to-intent boundary. func (kind TargetActionKind) Intent(targetID string) (Intent, bool) { policy, ok := targetActionPolicyForKind(kind) if !ok || policy.intent == 0 { return Intent{}, false } return Intent{Kind: policy.intent, TargetID: targetID}, true } type TargetDetectionPhase uint8 const ( // TargetReady is the zero value so snapshots supplied by non-progressive // Modules remain compatible with the established dashboard contract. TargetReady TargetDetectionPhase = iota TargetChecking TargetDetectionFailed ) type TargetSnapshot struct { ID string Description target.Description DetectionPhase TargetDetectionPhase DetectionErr error Available bool AvailabilityEvidence string State target.State Evidence string PrimaryAction TargetAction DriftResolutionActions []TargetAction } type Snapshot struct { Profiles []profile.Profile ActiveProfile string Targets []TargetSnapshot } type TargetProgress struct { TargetID string Phase TargetProgressPhase State target.State Evidence string Err error } type Result struct { Snapshot Snapshot Notice string ConfirmationRequest *Confirmation Status OperationStatus Progress []TargetProgress FirstFailedTargetID string Err error } type Module interface { Snapshot() Snapshot Dispatch(Intent) Result } type Application struct { store profile.Store state profile.State targets []target.Target targetIDs []string targetDescriptions []target.Description availabilities map[string]target.Availability inspections map[string]target.Inspection mutationBlockReasons map[string]string detectionPhase map[string]TargetDetectionPhase detectionErr map[string]error detectionGeneration uint64 stateMu sync.RWMutex dispatchMu sync.Mutex interactiveMu sync.Mutex interactiveOperation bool mutationLock MutationLock logger OperationLogger } func New(store profile.Store) (*Application, error) { return NewWithTargets(store, nil) } func NewWithTargets(store profile.Store, targets []target.Target) (*Application, error) { return NewWithTargetsAndLock(store, targets, nil) } // NewWithTargetsAndLock configures the cross-process lock used by every // Profile or Target mutation. A nil lock is intended only for isolated tests. func NewWithTargetsAndLock(store profile.Store, targets []target.Target, mutationLock MutationLock) (*Application, error) { return NewWithTargetsAndRuntime(store, targets, mutationLock, nil) } // NewWithTargetsAndRuntime configures shared mutation serialization and local // operation diagnostics for production callers. func NewWithTargetsAndRuntime(store profile.Store, targets []target.Target, mutationLock MutationLock, logger OperationLogger) (*Application, error) { state, err := store.Load() if err != nil { return nil, err } a := &Application{ store: store, state: state, targets: append([]target.Target(nil), targets...), availabilities: make(map[string]target.Availability), inspections: make(map[string]target.Inspection), mutationBlockReasons: make(map[string]string), detectionPhase: make(map[string]TargetDetectionPhase), detectionErr: make(map[string]error), mutationLock: mutationLock, logger: logger, } for _, adapter := range a.targets { id := adapter.ID() a.targetIDs = append(a.targetIDs, id) a.targetDescriptions = append(a.targetDescriptions, adapter.Description()) a.detectionPhase[id] = TargetChecking } return a, nil } func (a *Application) Snapshot() Snapshot { a.stateMu.RLock() defer a.stateMu.RUnlock() return a.snapshotLocked() } func (a *Application) snapshotLocked() Snapshot { state := cloneState(a.state) snapshot := Snapshot{Profiles: state.Profiles, ActiveProfile: state.ActiveProfile} for i, id := range a.targetIDs { availability := a.availabilities[id] inspection := a.inspections[id] phase := a.detectionPhase[id] primary, resolution := contextualTargetActions(phase, availability.Available, inspection.State, state.ActiveProfile != "", a.mutationBlockReasons[id]) snapshot.Targets = append(snapshot.Targets, TargetSnapshot{ ID: id, Description: a.targetDescriptions[i], DetectionPhase: phase, DetectionErr: a.detectionErr[id], Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence, PrimaryAction: primary, DriftResolutionActions: resolution, }) } return snapshot } func contextualTargetActions(phase TargetDetectionPhase, available bool, state target.State, hasActiveProfile bool, mutationBlockReason string) (TargetAction, []TargetAction) { primaryKind := primaryTargetActionKind(state) primary := TargetAction{Kind: primaryKind, Available: primaryKind != 0} if primaryKind == 0 { primary.DisabledReason = "Target actions are unavailable because this Target state is unknown" } if reason := targetActionDisabledReason(phase, available); reason != "" { primary.Available = false primary.DisabledReason = reason } if policy, ok := targetActionPolicyForKind(primaryKind); ok && primary.Available && policy.requiresProfile && !hasActiveProfile { primary.Available = false primary.DisabledReason = "This action is unavailable because there is no Active Profile; open Profile management first" } if primary.Available && mutationBlockReason != "" { primary.Available = false primary.DisabledReason = mutationBlockReason } if state != target.Drifted { return primary, nil } var resolution []TargetAction for _, policy := range targetActionPolicies { if policy.state != target.Drifted || policy.primary || policy.intent == 0 { continue } action := TargetAction{Kind: policy.kind, Available: true} if policy.requiresProfile && !hasActiveProfile { action.Available = false action.DisabledReason = "Reapply Active Profile is unavailable because there is no Active Profile; open Profile management first" } if reason := targetActionDisabledReason(phase, available); reason != "" { action.Available = false action.DisabledReason = reason } if action.Available && mutationBlockReason != "" { action.Available = false action.DisabledReason = mutationBlockReason } resolution = append(resolution, action) } return primary, resolution } func primaryTargetActionKind(state target.State) TargetActionKind { for _, policy := range targetActionPolicies { if policy.primary && policy.state == state { return policy.kind } } return 0 } func targetActionDisabledReason(phase TargetDetectionPhase, available bool) string { switch phase { case TargetChecking: return "Target actions are unavailable while this Target is still being checked" case TargetDetectionFailed: return "Target actions are unavailable because this Target check failed; refresh Targets to retry" default: if !available { return "Target actions are unavailable because this Target is unavailable" } return "" } } type targetDetectionOutcome struct { targetID string detection target.Detection mutationBlockReason string } type targetDetectionUpdate struct { snapshot Snapshot targetID string complete bool } func (a *Application) beginTargetDetection(ctx context.Context) (Snapshot, <-chan targetDetectionUpdate) { a.stateMu.Lock() a.detectionGeneration++ generation := a.detectionGeneration for _, id := range a.targetIDs { a.detectionPhase[id] = TargetChecking a.detectionErr[id] = nil } active := activeProfile(a.state) checking := a.snapshotLocked() a.stateMu.Unlock() updates := make(chan targetDetectionUpdate, len(a.targets)+1) outcomes := make(chan targetDetectionOutcome, len(a.targets)) for i, adapter := range a.targets { adapter, id := adapter, a.targetIDs[i] go func() { outcomes <- targetDetectionOutcome{ targetID: id, detection: detectTarget(ctx, adapter, active), mutationBlockReason: mutationBlockReason(ctx, adapter), } }() } go func() { defer close(updates) for range a.targets { outcome := <-outcomes a.stateMu.Lock() if generation != a.detectionGeneration { a.stateMu.Unlock() continue } phase := TargetReady if outcome.detection.Err != nil { phase = TargetDetectionFailed } a.availabilities[outcome.targetID] = outcome.detection.Availability a.inspections[outcome.targetID] = outcome.detection.Inspection a.mutationBlockReasons[outcome.targetID] = outcome.mutationBlockReason a.detectionPhase[outcome.targetID] = phase a.detectionErr[outcome.targetID] = outcome.detection.Err update := targetDetectionUpdate{snapshot: a.snapshotLocked(), targetID: outcome.targetID} // The buffered send cannot wait on a consumer and happens before a // newer Begin call can make this generation stale. updates <- update a.stateMu.Unlock() } a.stateMu.Lock() if generation == a.detectionGeneration { updates <- targetDetectionUpdate{snapshot: a.snapshotLocked(), complete: true} } a.stateMu.Unlock() }() return checking, updates } func detectTarget(ctx context.Context, adapter target.Target, active *profile.Profile) target.Detection { if detector, ok := adapter.(target.Detector); ok { return detector.Detect(ctx, active) } return target.Detection{ Availability: adapter.Availability(ctx), Inspection: adapter.Inspect(ctx, active), } } func mutationBlockReason(ctx context.Context, adapter target.Target) string { authorizer, ok := adapter.(target.MutationAuthorizer) if !ok { return "" } check := authorizer.MutationAuthority(ctx) if check.OK { return "" } if check.Evidence == "" { return "The current process does not have authority to change this Target" } return check.Evidence } func (a *Application) Dispatch(intent Intent) Result { ctx, cancel := context.WithTimeout(context.Background(), operationTimeout) defer cancel() return a.dispatch(ctx, intent, nil) } func (a *Application) dispatch(ctx context.Context, intent Intent, publish func(Result)) Result { // Serialize local mutation workflows even when tests intentionally omit the // cross-process MutationLock. a.dispatchMu.Lock() defer a.dispatchMu.Unlock() if a.mutationLock != nil { unlock, err := a.mutationLock.Lock(ctx) if err != nil { return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("lock proxyctl mutation: %w", err)} } defer unlock() } // A different process may have changed the persisted Active Profile since // this Application was constructed. Reload only after the global mutation // lock is held, before selecting a Profile or invoking a Target. Load is // deliberately separate from Store.Mutate so no Profile-store lock is nested. fresh, err := a.store.Load() if err != nil { return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("reload Profile state for mutation: %w", err)} } a.replaceState(fresh) if intent.Kind == ActivateProfile { a.refreshInspections(ctx) } if isTargetMutationIntent(intent.Kind) { return a.setTargetDesired(ctx, intent, publish) } if intent.Kind == RefreshTargets { return a.refreshTargets(ctx, publish) } if intent.Kind == ActivateProfile { return a.activateProfile(ctx, intent, publish) } var notice string next, err := a.store.Mutate(func(current profile.State) (profile.State, error) { var mutationErr error switch intent.Kind { case CreateProfile: current, notice, mutationErr = create(current, intent.Profile) case UpdateProfile: current, notice, mutationErr = update(current, intent.Name, intent.Profile) case DeleteProfile: current, notice, mutationErr = deleteProfile(current, intent.Name, intent.Confirm) default: mutationErr = errors.New("unsupported Profile action") } return current, mutationErr }) if err == nil { a.replaceState(next) a.refreshInspections(ctx) } else { var committed *profile.CommitError if errors.As(err, &committed) { // Rename committed next before directory durability reporting failed. // Prefer that state even if the required verification reload also fails. a.replaceState(next) reloaded, loadErr := a.store.Load() if loadErr != nil { err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr)) } else { a.replaceState(reloaded) } } } return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err} } func (a *Application) activateProfile(ctx context.Context, intent Intent, publish func(Result)) Result { state := a.stateCopy() if index(state, intent.Name) < 0 { return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Profile %q does not exist", intent.Name)} } if state.ActiveProfile == intent.Name { return Result{Snapshot: a.Snapshot(), Notice: fmt.Sprintf("Profile %q is already Active", intent.Name)} } inspections := a.inspectionsCopy() var enabled []int var warnings []string confirmationScope := []string{"activate-profile", intent.Name} for i, adapter := range a.targets { id := a.targetIDs[i] if inspections[id].State != target.Enabled { continue } enabled = append(enabled, i) confirmationScope = append(confirmationScope, id) if warning := adapter.Confirmation(true); warning != "" { detail := a.targetDescriptions[i].Name + ": " + warning warnings = append(warnings, detail) confirmationScope = append(confirmationScope, detail) } else { confirmationScope = append(confirmationScope, "") } } token := confirmationToken(confirmationScope...) if len(warnings) > 0 && (!intent.Confirm || intent.ConfirmationToken != token) { confirmation := &Confirmation{ Title: "Confirm Active Profile change", Summary: "Changing the Active Profile has high-impact Target effects.", Details: warnings, ConfirmLabel: "Change Profile", CancelLabel: "Cancel", HighImpact: true, Token: token, } return Result{Snapshot: a.Snapshot(), ConfirmationRequest: confirmation, Status: OperationStatus{Summary: confirmation.Title}} } next, err := a.store.Mutate(func(current profile.State) (profile.State, error) { next, _, mutationErr := activate(current, intent.Name) return next, mutationErr }) if err != nil { var committed *profile.CommitError if errors.As(err, &committed) { a.replaceState(next) if reloaded, loadErr := a.store.Load(); loadErr != nil { err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr)) } else { a.replaceState(reloaded) } } return Result{Snapshot: a.Snapshot(), Err: err} } a.replaceState(next) active := a.activeProfile() // The Active Profile has changed before any Target write begins. Re-inspect // without the state lock, then publish all observations atomically. observed := make(map[string]target.Inspection, len(a.targets)) for i, adapter := range a.targets { observed[a.targetIDs[i]] = adapter.Inspect(ctx, active) } a.replaceInspections(observed) var progress []TargetProgress var failures []error updated := make(map[string]bool, len(enabled)) for _, targetIndex := range enabled { adapter := a.targets[targetIndex] id := a.targetIDs[targetIndex] name := a.targetDescriptions[targetIndex].Name updated[id] = true if publish != nil { publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{{TargetID: id, Phase: TargetProgressRunning, State: a.inspection(id).State}}, Notice: "Updating " + name + " Target", Status: OperationStatus{Summary: "Updating " + name + " Target", TargetID: id}}) } operation := adapter.SetDesired(ctx, true, active) inspection := operation.Inspection var safeErr error if operation.Err != nil { // A failure-returned inspection may predate a partial write. Always // inspect explicitly against the new Active Profile before publishing. inspection = adapter.Inspect(ctx, active) safeErr = fmt.Errorf("%s (fresh inspection: %s — %s)", profile.RedactText(operation.Err.Error()), inspection.State, profile.RedactText(inspection.Evidence)) } a.replaceInspection(id, inspection) if a.logger != nil { outcome, detail := "success", inspection.Evidence if safeErr != nil { outcome, detail = "failure", safeErr.Error() } if logErr := a.logger.Write(LogEntry{Time: time.Now(), Operation: "switch-Active-Profile", Target: id, Result: outcome, Detail: RedactLogDetail(detail)}); logErr != nil { safeErr = errors.Join(safeErr, fmt.Errorf("write %s Target operation log: %w", name, logErr)) } } progressPhase := TargetProgressSucceeded if safeErr != nil { progressPhase = TargetProgressFailed } item := TargetProgress{TargetID: id, Phase: progressPhase, State: inspection.State, Evidence: inspection.Evidence, Err: safeErr} progress = append(progress, item) if publish != nil { status := OperationStatus{Summary: name + " Target update completed", TargetID: id} if safeErr != nil { status = OperationStatus{Summary: name + " Target update failed", TargetID: id} } publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{item}, Notice: name + " Target update completed", Status: status, Err: safeErr}) } if safeErr != nil { failures = append(failures, fmt.Errorf("%s Target: %w", name, safeErr)) } } // Re-inspect Targets which were not part of the transition so any managed // configuration is interpreted against the new Active Profile as well. remaining := make(map[string]target.Inspection) for i, adapter := range a.targets { id := a.targetIDs[i] if !updated[id] { remaining[id] = adapter.Inspect(ctx, active) } } a.mergeInspections(remaining) result := Result{Snapshot: a.Snapshot(), Progress: progress, Notice: fmt.Sprintf("Activated Profile %q", intent.Name), Status: OperationStatus{Summary: fmt.Sprintf("Activated Profile %q", intent.Name)}} if len(failures) > 0 { result.Err = errors.Join(failures...) result.Notice = fmt.Sprintf("Activated Profile %q with %d Target update failure(s); successful updates were kept and no global rollback was attempted", intent.Name, len(failures)) result.Status = OperationStatus{Summary: fmt.Sprintf("Activated Profile %q with %d Target update failure(s)", intent.Name, len(failures))} for _, item := range progress { if item.Err != nil { result.FirstFailedTargetID = item.TargetID result.Status.TargetID = item.TargetID break } } } return result } func activeProfile(state profile.State) *profile.Profile { for i := range state.Profiles { if state.Profiles[i].Name == state.ActiveProfile { value := state.Profiles[i] value.Bypass = append([]string(nil), value.Bypass...) return &value } } return nil } func (a *Application) activeProfile() *profile.Profile { a.stateMu.RLock() defer a.stateMu.RUnlock() return activeProfile(a.state) } func (a *Application) stateCopy() profile.State { a.stateMu.RLock() defer a.stateMu.RUnlock() return cloneState(a.state) } func (a *Application) replaceState(state profile.State) { a.stateMu.Lock() defer a.stateMu.Unlock() a.detectionGeneration++ a.state = cloneState(state) } func (a *Application) inspectionsCopy() map[string]target.Inspection { a.stateMu.RLock() defer a.stateMu.RUnlock() result := make(map[string]target.Inspection, len(a.inspections)) for id, inspection := range a.inspections { result[id] = inspection } return result } func (a *Application) inspection(id string) target.Inspection { a.stateMu.RLock() defer a.stateMu.RUnlock() return a.inspections[id] } func (a *Application) replaceInspection(id string, inspection target.Inspection) { a.stateMu.Lock() defer a.stateMu.Unlock() a.detectionGeneration++ a.inspections[id] = inspection } // publishTargetDetection atomically replaces all availability, inspection, and // failure fields from one targeted check. Detect runs before this method, so no // Application state mutex is held while adapter code executes. func (a *Application) publishTargetDetection(id string, detection target.Detection, mutationBlockReason string) Snapshot { a.stateMu.Lock() defer a.stateMu.Unlock() a.detectionGeneration++ a.availabilities[id] = detection.Availability a.inspections[id] = detection.Inspection a.mutationBlockReasons[id] = mutationBlockReason a.detectionErr[id] = detection.Err a.detectionPhase[id] = TargetReady if detection.Err != nil { a.detectionPhase[id] = TargetDetectionFailed } return a.snapshotLocked() } func (a *Application) replaceInspections(inspections map[string]target.Inspection) { a.stateMu.Lock() defer a.stateMu.Unlock() a.detectionGeneration++ for _, id := range a.targetIDs { a.inspections[id] = inspections[id] } } func (a *Application) mergeInspections(inspections map[string]target.Inspection) { if len(inspections) == 0 { return } a.stateMu.Lock() defer a.stateMu.Unlock() a.detectionGeneration++ for id, inspection := range inspections { a.inspections[id] = inspection } } func targetByID(snapshot Snapshot, id string) (TargetSnapshot, string) { for _, item := range snapshot.Targets { if item.ID == id { name := item.Description.Name if name == "" { name = id } return item, name } } return TargetSnapshot{ID: id}, id } func (a *Application) refreshTargets(ctx context.Context, publish func(Result)) Result { checking, updates := a.beginTargetDetection(ctx) if publish != nil { progress := make([]TargetProgress, 0, len(checking.Targets)) for _, item := range checking.Targets { progress = append(progress, TargetProgress{TargetID: item.ID, Phase: TargetProgressRunning, State: item.State}) } publish(Result{Snapshot: checking, Status: OperationStatus{Summary: "Checking Target availability and state"}, Progress: progress}) } var failures []error firstFailed := "" final := checking for update := range updates { final = update.snapshot if update.complete { continue } item, name := targetByID(update.snapshot, update.targetID) progress := TargetProgress{TargetID: update.targetID, Phase: TargetProgressSucceeded, State: item.State, Evidence: item.Evidence} status := OperationStatus{Summary: "Checked " + name + " Target", TargetID: update.targetID} if item.DetectionPhase == TargetDetectionFailed { progress.Phase = TargetProgressFailed progress.Err = item.DetectionErr status.Summary = "Could not check " + name + " Target" if item.DetectionErr != nil { failures = append(failures, fmt.Errorf("%s Target: %w", name, item.DetectionErr)) } if firstFailed == "" { firstFailed = update.targetID } } if publish != nil { publish(Result{Snapshot: update.snapshot, Status: status, Progress: []TargetProgress{progress}, Err: progress.Err}) } } result := Result{Snapshot: final, Notice: "Target availability and state checks completed", Status: OperationStatus{Summary: "Target availability and state checks completed"}} if len(failures) > 0 { result.Err = errors.Join(failures...) result.FirstFailedTargetID = firstFailed result.Status = OperationStatus{Summary: "Some Target checks failed", TargetID: firstFailed} } return result } func (a *Application) refreshInspections(ctx context.Context) { active := a.activeProfile() inspections := make(map[string]target.Inspection, len(a.targets)) for i, adapter := range a.targets { inspections[a.targetIDs[i]] = adapter.Inspect(ctx, active) } a.replaceInspections(inspections) } func isTargetMutationIntent(kind IntentKind) bool { for _, policy := range targetActionPolicies { if policy.intent == kind { return true } } return false } func requestedTargetAction(intent Intent) (TargetActionKind, bool) { for _, policy := range targetActionPolicies { if policy.intent == intent.Kind { return policy.kind, true } } return 0, false } func structuredTargetAction(selected TargetSnapshot, kind TargetActionKind) (TargetAction, bool) { if selected.PrimaryAction.Kind == kind { return selected.PrimaryAction, true } for _, action := range selected.DriftResolutionActions { if action.Kind == kind { return action, true } } return TargetAction{}, false } func validateTargetAction(intent Intent, selected TargetSnapshot) (targetActionPolicy, error) { kind, ok := requestedTargetAction(intent) if !ok { return targetActionPolicy{}, fmt.Errorf("Target %q action is no longer available because its state is %s", selected.ID, selected.State) } action, ok := structuredTargetAction(selected, kind) if !ok { return targetActionPolicy{}, fmt.Errorf("Target %q action is no longer available because its state is %s", selected.ID, selected.State) } if !action.Available { return targetActionPolicy{}, errors.New(action.DisabledReason) } policy, _ := targetActionPolicyForKind(kind) return policy, nil } func (a *Application) setTargetDesired(ctx context.Context, intent Intent, publish func(Result)) Result { selectedIndex := -1 for i, id := range a.targetIDs { if id == intent.TargetID { selectedIndex = i break } } if selectedIndex < 0 { return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Target %q does not exist", intent.TargetID)} } selected := a.targets[selectedIndex] active := a.activeProfile() // This targeted check runs inside dispatch serialization and immediately // before action validation. Adapter calls happen without stateMu; the full // outcome is then published under one lock before policy is derived. detection := detectTarget(ctx, selected, active) snapshot := a.publishTargetDetection(intent.TargetID, detection, mutationBlockReason(ctx, selected)) selectedSnapshot := snapshot.Targets[selectedIndex] policy, err := validateTargetAction(intent, selectedSnapshot) if err != nil { return Result{Snapshot: snapshot, Err: err} } if warning := selected.Confirmation(policy.desired); warning != "" { token := confirmationToken("target", intent.TargetID, fmt.Sprint(intent.Kind), fmt.Sprint(selectedSnapshot.State), fmt.Sprint(policy.desired), warning) if !intent.Confirm || intent.ConfirmationToken != token { confirmation := &Confirmation{Title: "Confirm " + a.targetDescriptions[selectedIndex].Name + " Target change", Summary: warning, ConfirmLabel: "Continue", CancelLabel: "Cancel", HighImpact: true, Token: token} return Result{Snapshot: a.Snapshot(), ConfirmationRequest: confirmation, Status: OperationStatus{Summary: confirmation.Title, TargetID: intent.TargetID}} } } if publish != nil { publish(Result{Snapshot: snapshot, Progress: []TargetProgress{{TargetID: intent.TargetID, Phase: TargetProgressRunning, State: selectedSnapshot.State}}, Status: OperationStatus{Summary: "Updating " + a.targetDescriptions[selectedIndex].Name + " Target", TargetID: intent.TargetID}}) } result := selected.SetDesired(ctx, policy.desired, active) inspection := result.Inspection if result.Err != nil { inspection = selected.Inspect(ctx, active) } a.replaceInspection(intent.TargetID, inspection) outcome, detail := "success", inspection.Evidence if result.Err != nil { outcome, detail = "failure", result.Err.Error() } if a.logger != nil { if logErr := a.logger.Write(LogEntry{Time: time.Now(), Operation: policy.operation, Target: intent.TargetID, Result: outcome, Detail: RedactLogDetail(detail)}); logErr != nil { result.Err = errors.Join(result.Err, fmt.Errorf("write Target operation log: %w", logErr)) } } notice := "" progressPhase := TargetProgressSucceeded status := OperationStatus{TargetID: intent.TargetID} if result.Err == nil { notice = fmt.Sprintf("%s Target is now %s", a.targetDescriptions[selectedIndex].Name, inspection.State) status.Summary = notice } else { progressPhase = TargetProgressFailed status.Summary = a.targetDescriptions[selectedIndex].Name + " Target update failed" } progress := TargetProgress{TargetID: intent.TargetID, Phase: progressPhase, State: inspection.State, Evidence: inspection.Evidence, Err: result.Err} return Result{Snapshot: a.Snapshot(), Notice: notice, Status: status, Progress: []TargetProgress{progress}, FirstFailedTargetID: map[bool]string{true: intent.TargetID}[result.Err != nil], Err: result.Err} } func create(state profile.State, input profile.Profile) (profile.State, string, error) { p, err := profile.ValidateAndNormalize(input) if err != nil { return state, "", err } if index(state, p.Name) >= 0 { return state, "", fmt.Errorf("a Profile named %q already exists", p.Name) } next := cloneState(state) next.Profiles = append(next.Profiles, p) return next, fmt.Sprintf("Created Profile %q", p.Name), nil } func activate(state profile.State, name string) (profile.State, string, error) { if index(state, name) < 0 { return state, "", fmt.Errorf("Profile %q does not exist", name) } next := cloneState(state) next.ActiveProfile = name return next, fmt.Sprintf("Activated Profile %q", name), nil } func update(state profile.State, originalName string, input profile.Profile) (profile.State, string, error) { profileIndex := index(state, originalName) if profileIndex < 0 { return state, "", fmt.Errorf("Profile %q does not exist", originalName) } p, err := profile.ValidateAndNormalize(input) if err != nil { return state, "", err } if other := index(state, p.Name); other >= 0 && other != profileIndex { return state, "", fmt.Errorf("a Profile named %q already exists", p.Name) } next := cloneState(state) next.Profiles[profileIndex] = p if next.ActiveProfile == originalName { next.ActiveProfile = p.Name } return next, fmt.Sprintf("Updated Profile %q", p.Name), nil } func deleteProfile(state profile.State, name string, confirm bool) (profile.State, string, error) { profileIndex := index(state, name) if profileIndex < 0 { return state, "", fmt.Errorf("Profile %q does not exist", name) } if len(state.Profiles) == 1 && !confirm { return state, "", errors.New("deleting the final Profile requires confirmation") } next := cloneState(state) next.Profiles = append(next.Profiles[:profileIndex], next.Profiles[profileIndex+1:]...) if next.ActiveProfile == name { next.ActiveProfile = "" } return next, fmt.Sprintf("Deleted Profile %q", name), nil } func index(state profile.State, name string) int { for i := range state.Profiles { if state.Profiles[i].Name == name { return i } } return -1 } func cloneState(state profile.State) profile.State { state.Profiles = append([]profile.Profile(nil), state.Profiles...) for i := range state.Profiles { state.Profiles[i].Bypass = append([]string(nil), state.Profiles[i].Bypass...) } return state }