profswitch.bend

cswap-bend-cli@0.1.0.0 · 182 lines · 33 definitions

1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/act.bend as Act
4import cswap-bend-cli@0.1.0.0/clock.bend as Clock
5import cswap-bend-cli@0.1.0.0/cmd.bend as Cmd
6import cswap-bend-cli@0.1.0.0/fs.bend as Fs
7import cswap-bend-cli@0.1.0.0/setload.bend as Setload
8import cswap-bend-cli@0.1.0.0/store/base.bend as SBase
9import cswap-bend-cli@0.1.0.0/swbase.bend as B
10import cswap-bend-cli@0.1.0.0/swlive.bend as L
11import cswap-bend-cli@0.1.0.0/switch.bend as Sw
12import cswap-bend-cli@0.1.0.0/cliswitch.bend as Cliswitch
13import cswap-bend-cli@0.1.0.0/profile.bend as P
14import cswap-bend-cli@0.1.0.0/policy/types.bend as T
15import cswap-bend-cli@0.1.0.0/policy/safe.bend as SF
16import cswap-bend-cli@0.1.0.0/policy/algo.bend as A
17import cswap-bend-cli@0.1.0.0/daemon/config.bend as Cfg
18import cswap-bend-cli@0.1.0.0/daemon/gather.bend as G
19import cswap-bend-cli@0.1.0.0/daemon/view.bend as V
20
21# `cswap switch` with no target under a profile (`--profile NAME`, or the
22# active profile): rotate to the account the profile's strategy picks now.
23# The threshold and the cooldown do not apply (the user asked to move); an
24# account that is not safe (see src/policy/safe.bend) is never chosen, and
25# nothing moves while the active account's usage is unreadable.
26#
27# With no profile in play, or the `default` profile, `switch` is the legacy
28# rotation (src/switch.bend), byte for byte.
29
30def ActM(-A: Type) -> Type:
31  Act.of(A)
32
33def ActM.pure(-A: Type, x: A) -> Act.of(A):
34  Act.pure(A, x)
35
36def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B):
37  Act.bind(A, B, m, f)
38
39def snap_live(+s: G.Snap) -> L.Live:
40  match s:
41    case G.Snap{seq, live, active, st, rows, ents}:
42      live
43
44def snap_ents(+s: G.Snap) -> List<&2, G.Ent>:
45  G.snap_ents(s)
46
47def snap_rows(+s: G.Snap) -> Json.Json:
48  G.snap_rows(s)
49
50def tag(+p: P.Prof, +r: Json.Json) -> Json.Json:
51  Json.set("strategy", Json.JStr{SF.name(P.algo_base(p))}, Json.set("profile", Json.JStr{P.name_of(p)}, r))
52
53def noop(+p: P.Prof, +reason: String, +message: String, +to: Json.Json) -> Json.Json:
54  tag(p, B.noop(SF.name(P.algo_base(p)), reason, message, to, []))
55
56def pad_detail2(+empty: Bool, +d: String) -> String:
57  match empty:
58    case True{}:
59      ""
60    case False{}:
61      " (" ++ d ++ ")"
62
63def pad_detail(+d: String) -> String:
64  pad_detail2(String.is_empty(d), d)
65
66def nothing_msg(+reason: T.Reason, +detail: String) -> String:
67  "Staying put: " ++ T.reason.name(reason) ++ pad_detail(detail)
68
69def decided(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +cur: Json.Json, +d: T.Decision) -> Act.of(Json.Json):
70  match d:
71    case T.Switch{to, trig, why, h, mode, rec, bar}:
72      do ActM<Json.Json>:
73        r : Json.Json <- Sw.switch_to(ctx, Nat.show(to), False{}, opts)
74        return tag(p, r)
75    case T.NoSwitch{reason, detail}:
76      Act.pure(Json.Json, noop(p, T.reason.name(reason), nothing_msg(reason, detail), cur))
77
78def usages_for(+s: G.Snap, +a: String, +t: U32) -> List<&2, T.Usage>:
79  V.usages_of(snap_rows(s), snap_ents(s), Some{a}, t)
80
81def pcfg_for(+p: P.Prof, +raw: Json.Json) -> T.Config:
82  V.pol_cfg(P.with_prof(Cfg.from_settings(raw), p))
83
84def decision_for(+p: P.Prof, +raw: Json.Json, +s: G.Snap, +a: String, +t: U32) -> T.Decision:
85  A.rotate(P.algo_base(p), V.slot_nat_of(a), V.accounts_of(snap_ents(s)), usages_for(s, a, t), V.nat(t), pcfg_for(p, raw))
86
87def cur_ref(+s: G.Snap, +a: String) -> Json.Json:
88  B.ref(Some{a}, L.lv_email(snap_live(s)))
89
90def rotate_at(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +raw: Json.Json, +s: G.Snap, +a: String, +t: U32) -> Act.of(Json.Json):
91  decided(p, ctx, opts, cur_ref(s, a), decision_for(p, raw, s, a, t))
92
93def rotate_snap(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +raw: Json.Json, +s: G.Snap, +act: Maybe<&2, String>, +t: U32) -> Act.of(Json.Json):
94  match act:
95    case None{}:
96      Act.pure(Json.Json, noop(p, "unmanaged-account", "Active account is not managed; run cswap --add-account", B.ref(None{}, L.lv_email(snap_live(s)))))
97    case Some{a}:
98      rotate_at(p, ctx, opts, raw, s, a, t)
99
100def rotate_opt(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +raw: Json.Json, +m: Maybe<&2, G.Snap>, +t: U32) -> Act.of(Json.Json):
101  match m:
102    case None{}:
103      Act.fail(Json.Json, B.e_config(), "No accounts are managed yet")
104    case Some{s}:
105      rotate_snap(p, ctx, opts, raw, s, G.snap_active(s), t)
106
107def rotate_act(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +raw: Json.Json) -> Act.of(Json.Json):
108  do ActM<Json.Json>:
109    m : Maybe<&2, G.Snap> <- G.snapshot(ctx)
110    t : U32 <- Act.lift(U32, Clock.now_s())
111    rotate_opt(p, ctx, opts, raw, m, t)
112
113def run_profile(+p: P.Prof, +ctx: SBase.Ctx, +raw: Json.Json, +json: Bool, +opts: L.Opts) -> IO(Cmd.Out):
114  IO.bind(Fs.Res(Json.Json), Cmd.Out, rotate_act(p, ctx, opts, raw), r => IO.pure(Cmd.Out, Cliswitch.switch_out(json, r, [], "")))
115
116def with_opts(+p: P.Prof, +ctx: SBase.Ctx, +raw: Json.Json, +json: Bool) -> IO(Cmd.Out):
117  IO.bind(L.Opts, Cmd.Out, Cliswitch.switch_opts(), opts => run_profile(p, ctx, raw, json, opts))
118
119# ---- which path a `switch` takes
120
121def legacy(+ctx: SBase.Ctx, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
122  Cliswitch.switch_cmd(ctx, ident, force, strategy, model, json)
123
124def has(+m: Maybe<&2, String>) -> Bool:
125  match m:
126    case Some{x}:
127      True{}
128    case None{}:
129      False{}
130
131def usage_err(+msg: String) -> IO(Cmd.Out):
132  IO.pure(Cmd.Out, Cmd.Out{2, "", "cswap: error: " ++ msg ++ "\n"})
133
134# A name from `--profile` that matches nothing is an error; a stale
135# `active_profile` falls back to the legacy rotation.
136def unknown_name(+explicit: Bool, +name: String, +ctx: SBase.Ctx, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
137  match explicit:
138    case True{}:
139      IO.pure(Cmd.Out, Cmd.failed(json, "ConfigError", "unknown profile '" ++ name ++ "'; `cswap profile list` shows the profiles"))
140    case False{}:
141      legacy(ctx, ident, force, strategy, model, json)
142
143def by_name(+is_default: Bool, +p: P.Prof, +ctx: SBase.Ctx, +raw: Json.Json, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
144  match is_default:
145    case True{}:
146      legacy(ctx, ident, force, strategy, model, json)
147    case False{}:
148      with_opts(p, ctx, raw, json)
149
150def resolved(+res: P.Res, +over: Maybe<&2, String>, +ctx: SBase.Ctx, +raw: Json.Json, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
151  match res:
152    case P.RNone{}:
153      legacy(ctx, ident, force, strategy, model, json)
154    case P.RUnknown{name}:
155      unknown_name(has(over), name, ctx, ident, force, strategy, model, json)
156    case P.ROk{p}:
157      by_name(String.eq(P.name_of(p), "default"), p, ctx, raw, ident, force, strategy, model, json)
158
159# Only a bare `switch` (no target, no --strategy) follows a profile.
160def targeted(+ident: Maybe<&2, String>, +strategy: Maybe<&2, String>) -> Bool:
161  has(ident) || has(strategy)
162
163def loaded(+over: Maybe<&2, String>, +ctx: SBase.Ctx, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool, +raw: Json.Json) -> IO(Cmd.Out):
164  resolved(P.resolve(raw, over), over, ctx, raw, ident, force, strategy, model, json)
165
166def targeted_out(+over: Maybe<&2, String>, +ctx: SBase.Ctx, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
167  match over:
168    case Some{o}:
169      usage_err("--profile can only be used with a bare 'switch' (no target and no --strategy)")
170    case None{}:
171      legacy(ctx, ident, force, strategy, model, json)
172
173def route(+tgt: Bool, +over: Maybe<&2, String>, +ctx: SBase.Ctx, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
174  match tgt:
175    case True{}:
176      targeted_out(over, ctx, ident, force, strategy, model, json)
177    case False{}:
178      IO.bind(Json.Json, Cmd.Out, Setload.load(SBase.root_of(ctx)), raw => loaded(over, ctx, ident, force, strategy, model, json, raw))
179
180# `cswap switch [TARGET] [--profile NAME] ...`
181def switch_cmd(+ctx: SBase.Ctx, +over: Maybe<&2, String>, +ident: Maybe<&2, String>, +force: Bool, +strategy: Maybe<&2, String>, +model: Maybe<&2, String>, +json: Bool) -> IO(Cmd.Out):
182  route(targeted(ident, strategy), over, ctx, ident, force, strategy, model, json)