package app import ( "context" "errors" "fmt" "proxyctl/internal/profile" "proxyctl/internal/target" ) type IntentKind int const ( CreateProfile IntentKind = iota + 1 UpdateProfile DeleteProfile ActivateProfile SetTargetDesired ) type Intent struct { Kind IntentKind Name string Profile profile.Profile Confirm bool TargetID string Enabled bool } type TargetSnapshot struct { ID string Description target.Description Available bool AvailabilityEvidence string State target.State Evidence string } type Snapshot struct { Profiles []profile.Profile ActiveProfile string Targets []TargetSnapshot } type Result struct { Snapshot Snapshot Notice string Err error } type Module interface { Snapshot() Snapshot Dispatch(Intent) Result } type Application struct { store profile.Store state profile.State targets []target.Target inspections map[string]target.Inspection } func New(store profile.Store) (*Application, error) { return NewWithTargets(store, nil) } func NewWithTargets(store profile.Store, targets []target.Target) (*Application, error) { state, err := store.Load() if err != nil { return nil, err } a := &Application{store: store, state: state, targets: append([]target.Target(nil), targets...), inspections: make(map[string]target.Inspection)} a.refreshTargets() return a, nil } func (a *Application) Snapshot() Snapshot { profiles := append([]profile.Profile(nil), a.state.Profiles...) snapshot := Snapshot{Profiles: profiles, ActiveProfile: a.state.ActiveProfile} for _, adapter := range a.targets { availability := adapter.Availability(context.Background()) inspection := a.inspections[adapter.ID()] snapshot.Targets = append(snapshot.Targets, TargetSnapshot{ID: adapter.ID(), Description: adapter.Description(), Available: availability.Available, AvailabilityEvidence: availability.Evidence, State: inspection.State, Evidence: inspection.Evidence}) } return snapshot } func (a *Application) Dispatch(intent Intent) Result { if intent.Kind == SetTargetDesired { return a.setTargetDesired(intent) } 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: current, notice, mutationErr = deleteProfile(current, intent.Name, intent.Confirm) default: mutationErr = errors.New("unsupported Profile action") } return current, mutationErr }) if err == nil { a.state = next a.refreshTargets() } 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.state = next reloaded, loadErr := a.store.Load() if loadErr != nil { err = errors.Join(err, fmt.Errorf("reload committed Profile configuration: %w", loadErr)) } else { a.state = reloaded } } } return Result{Snapshot: a.Snapshot(), Notice: notice, Err: err} } func (a *Application) activeProfile() *profile.Profile { for i := range a.state.Profiles { if a.state.Profiles[i].Name == a.state.ActiveProfile { value := a.state.Profiles[i] return &value } } return nil } func (a *Application) refreshTargets() { active := a.activeProfile() for _, adapter := range a.targets { a.inspections[adapter.ID()] = adapter.Inspect(context.Background(), active) } } func (a *Application) setTargetDesired(intent Intent) Result { var selected target.Target for _, adapter := range a.targets { if adapter.ID() == intent.TargetID { selected = adapter break } } if selected == nil { return Result{Snapshot: a.Snapshot(), Err: fmt.Errorf("Target %q does not exist", intent.TargetID)} } active := a.activeProfile() if intent.Enabled && active == nil { return Result{Snapshot: a.Snapshot(), Err: errors.New("no Active Profile; open Profile management before enabling a Target")} } result := selected.SetDesired(context.Background(), intent.Enabled, active) a.inspections[selected.ID()] = result.Inspection notice := "" if result.Err == nil { notice = fmt.Sprintf("%s Target is now %s", selected.Description().Name, result.Inspection.State) } return Result{Snapshot: a.Snapshot(), Notice: notice, 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...) return state }