1 import Base 2 import ./json.bend as Json 3 import ./act.bend as Act 4 import ./clock.bend as Clock 5 import ./cmd.bend as Cmd 6 import ./fs.bend as Fs 7 import ./setload.bend as Setload 8 import ./store/base.bend as SBase 9 import ./swbase.bend as B 10 import ./swlive.bend as L 11 import ./switch.bend as Sw 12 import ./cliswitch.bend as Cliswitch 13 import ./profile.bend as P 14 import ./policy/types.bend as T 15 import ./policy/safe.bend as SF 16 import ./policy/algo.bend as A 17 import ./daemon/config.bend as Cfg 18 import ./daemon/gather.bend as G 19 import ./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 30 def ActM(-A: Type) -> Type: 31 Act.of(A) 32 33 def ActM.pure(-A: Type, x: A) -> Act.of(A): 34 Act.pure(A, x) 35 36 def 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 39 def snap_live(+s: G.Snap) -> L.Live: 40 match s: 41 case G.Snap{seq, live, active, st, rows, ents}: 42 live 43 44 def snap_ents(+s: G.Snap) -> List<&2, G.Ent>: 45 G.snap_ents(s) 46 47 def snap_rows(+s: G.Snap) -> Json.Json: 48 G.snap_rows(s) 49 50 def 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 53 def 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 56 def pad_detail2(+empty: Bool, +d: String) -> String: 57 match empty: 58 case True{}: 59 "" 60 case False{}: 61 " (" ++ d ++ ")" 62 63 def pad_detail(+d: String) -> String: 64 pad_detail2(String.is_empty(d), d) 65 66 def nothing_msg(+reason: T.Reason, +detail: String) -> String: 67 "Staying put: " ++ T.reason.name(reason) ++ pad_detail(detail) 68 69 def 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: 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 78 def 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 81 def pcfg_for(+p: P.Prof, +raw: Json.Json) -> T.Config: 82 V.pol_cfg(P.with_prof(Cfg.from_settings(raw), p)) 83 84 def 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 87 def cur_ref(+s: G.Snap, +a: String) -> Json.Json: 88 B.ref(Some{a}, L.lv_email(snap_live(s))) 89 90 def 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 93 def 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 100 def 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 107 def rotate_act(+p: P.Prof, +ctx: SBase.Ctx, +opts: L.Opts, +raw: Json.Json) -> Act.of(Json.Json): 108 do ActM: 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 113 def 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 116 def 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 121 def 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 124 def has(+m: Maybe<&2, String>) -> Bool: 125 match m: 126 case Some{x}: 127 True{} 128 case None{}: 129 False{} 130 131 def 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. 136 def 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 143 def 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 150 def 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. 160 def targeted(+ident: Maybe<&2, String>, +strategy: Maybe<&2, String>) -> Bool: 161 has(ident) || has(strategy) 162 163 def 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 166 def 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 173 def 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] ...` 181 def 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)