feat: coordinate Active Profile target panel

This commit is contained in:
proxyctl coordinator
2026-07-17 04:05:14 +08:00
parent 34b45c807f
commit fd113fb316
11 changed files with 652 additions and 36 deletions
+135 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"proxyctl/internal/profile"
@@ -18,6 +19,7 @@ const (
DeleteProfile
ActivateProfile
SetTargetDesired
RefreshTargets
)
type Intent struct {
@@ -44,10 +46,20 @@ type Snapshot struct {
Targets []TargetSnapshot
}
type TargetProgress struct {
TargetID string
Applying bool
State target.State
Evidence string
Err error
}
type Result struct {
Snapshot Snapshot
Notice string
Confirmation string
Progress []TargetProgress
Complete bool
Err error
}
@@ -56,6 +68,13 @@ type Module interface {
Dispatch(Intent) Result
}
// ProgressiveModule is implemented by Applications that can expose each
// coordinated Target update while an Active Profile transition is running.
// The final Result has Complete set and closes the stream.
type ProgressiveModule interface {
DispatchProgress(Intent) <-chan Result
}
type Application struct {
store profile.Store
state profile.State
@@ -103,6 +122,21 @@ func (a *Application) Snapshot() Snapshot {
}
func (a *Application) Dispatch(intent Intent) Result {
return a.dispatch(intent, nil)
}
func (a *Application) DispatchProgress(intent Intent) <-chan Result {
results := make(chan Result, 2*len(a.targets)+1)
go func() {
defer close(results)
final := a.dispatch(intent, func(update Result) { results <- update })
final.Complete = true
results <- final
}()
return results
}
func (a *Application) dispatch(intent Intent, publish func(Result)) Result {
if a.mutationLock != nil {
unlock, err := a.mutationLock.Lock(context.Background())
if err != nil {
@@ -113,14 +147,19 @@ func (a *Application) Dispatch(intent Intent) Result {
if intent.Kind == SetTargetDesired {
return a.setTargetDesired(intent)
}
if intent.Kind == RefreshTargets {
a.refreshTargets()
return Result{Snapshot: a.Snapshot(), Notice: "Refreshed Target availability and state"}
}
if intent.Kind == ActivateProfile {
return a.activateProfile(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 ActivateProfile:
current, notice, mutationErr = activate(current, intent.Name)
case UpdateProfile:
current, notice, mutationErr = update(current, intent.Name, intent.Profile)
case DeleteProfile:
@@ -150,6 +189,100 @@ func (a *Application) Dispatch(intent Intent) Result {
return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err}
}
func (a *Application) activateProfile(intent Intent, publish func(Result)) Result {
if index(a.state, intent.Name) < 0 {
return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Profile %q does not exist", intent.Name)}
}
if a.state.ActiveProfile == intent.Name {
return Result{Snapshot: a.Snapshot(), Notice: fmt.Sprintf("Profile %q is already Active", intent.Name)}
}
var enabled []target.Target
var warnings []string
for _, adapter := range a.targets {
if a.inspections[adapter.ID()].State != target.Enabled {
continue
}
enabled = append(enabled, adapter)
if warning := adapter.Confirmation(true); warning != "" {
warnings = append(warnings, adapter.Description().Name+": "+warning)
}
}
if len(warnings) > 0 && !intent.Confirm {
return Result{Snapshot: a.Snapshot(), Confirmation: "Changing the Active Profile requires confirmation:\n" + strings.Join(warnings, "\n")}
}
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.state = next
if reloaded, loadErr := a.store.Load(); loadErr != nil {
err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr))
} else {
a.state = reloaded
}
}
return Result{Snapshot: a.Snapshot(), Err: err}
}
a.state = next
active := a.activeProfile()
// The Active Profile has changed before any Target write begins. Re-inspect
// immediately so rows not yet updated are truthfully Drifted rather than
// appearing Enabled against stale managed values.
for _, adapter := range a.targets {
a.inspections[adapter.ID()] = adapter.Inspect(context.Background(), active)
}
var progress []TargetProgress
var failures []error
for _, adapter := range enabled {
if publish != nil {
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{{TargetID: adapter.ID(), Applying: true, State: a.inspections[adapter.ID()].State}}, Notice: "Updating " + adapter.Description().Name + " Target"})
}
operation := adapter.SetDesired(context.Background(), true, active)
a.inspections[adapter.ID()] = operation.Inspection
var safeErr error
if operation.Err != nil {
safeErr = errors.New(profile.RedactText(operation.Err.Error()))
}
item := TargetProgress{TargetID: adapter.ID(), State: operation.Inspection.State, Evidence: operation.Inspection.Evidence, Err: safeErr}
progress = append(progress, item)
if publish != nil {
publish(Result{Snapshot: a.Snapshot(), Progress: []TargetProgress{item}, Notice: adapter.Description().Name + " Target update completed", Err: safeErr})
}
if a.logger != nil {
outcome, detail := "success", operation.Inspection.Evidence
if safeErr != nil {
outcome, detail = "failure", safeErr.Error()
}
_ = a.logger.Write(LogEntry{Time: time.Now(), Operation: "switch-Active-Profile", Target: adapter.ID(), Result: outcome, Detail: RedactLogDetail(detail)})
}
if safeErr != nil {
failures = append(failures, fmt.Errorf("%s Target: %w", adapter.Description().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.
updated := make(map[string]bool, len(enabled))
for _, adapter := range enabled {
updated[adapter.ID()] = true
}
for _, adapter := range a.targets {
if !updated[adapter.ID()] {
a.inspections[adapter.ID()] = adapter.Inspect(context.Background(), active)
}
}
result := Result{Snapshot: a.Snapshot(), Progress: progress, Notice: 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))
}
return result
}
func (a *Application) activeProfile() *profile.Profile {
for i := range a.state.Profiles {
if a.state.Profiles[i].Name == a.state.ActiveProfile {