|
|
@@ -30,7 +30,8 @@ POSTs) and asserts the Workstream B features against the wire contract:
|
|
|
Plan 10 phase 1 (work acquisition flip):
|
|
|
|
|
|
- per-saddle tokens: registration mints + /register-delivers them,
|
|
|
- legacy tokens migrate onto the row at startup, removal revokes
|
|
|
+ the row is the single credential store (the api_tokens fallback
|
|
|
+ died with arch 24), removal revokes
|
|
|
- token auth on every /api/saddle/* endpoint
|
|
|
- next-task allocation: concurrent asks race exactly one winner,
|
|
|
affinity pinning for parked tasks, resume_notice on suspended resumes
|
|
|
@@ -4944,11 +4945,12 @@ def bundle_answers(ctx):
|
|
|
timeout=20, what="unreachable flag") is not None,
|
|
|
"queue row shows the unreachable state in the window")
|
|
|
|
|
|
- print("== phase 1: startup migrates the per-saddle token ==")
|
|
|
- # Saddles registered before the flip hold legacy api_tokens rows.
|
|
|
- # On startup every saddle row gets its canonical token: the
|
|
|
- # legacy value it already holds (daemons keep their credential)
|
|
|
- # or a freshly minted one. Fixture: this shard's own racer.
|
|
|
+ print("== phase 1: saddles.token is the single credential store (arch 24) ==")
|
|
|
+ # The legacy api_tokens table and every fallback read died with the
|
|
|
+ # poll era: a restart proves the migrated schema carries no
|
|
|
+ # api_tokens table, the row token survives and still authenticates,
|
|
|
+ # and a legacy-shaped token string authorises nothing (no fallback
|
|
|
+ # read exists to honour it). Fixture: this shard's own racer.
|
|
|
fake2 = FakeSaddle()
|
|
|
fake2.ask_on_register = False
|
|
|
fake2.serve(port=free_port())
|
|
|
@@ -4957,15 +4959,8 @@ def bundle_answers(ctx):
|
|
|
"host": "127.0.0.1:%d" % fake2.port})
|
|
|
racer_two_id = int([s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
|
|
|
if s["name"] == "racer-two"][0]["id"])
|
|
|
- conn = db()
|
|
|
- conn.execute("insert into api_tokens (saddle_id, token, created_at)"
|
|
|
- " values (?, 'saddle-legacy-copied-token', strftime('%s','now'))",
|
|
|
- (racer_two_id,))
|
|
|
- conn.execute("update saddles set token = null where name = 'racer-two'")
|
|
|
- conn.execute("insert into saddles (name, host, status, current_task_id)"
|
|
|
- " values ('tokenless-orphan', '127.0.0.1:1', 'offline', 0)")
|
|
|
- conn.commit()
|
|
|
- conn.close()
|
|
|
+ racer_two_token = [s for s in slot_public(auth, "/saddles", "saddles")["saddles"]
|
|
|
+ if s["name"] == "racer-two"][0]["token"]
|
|
|
|
|
|
app.terminate()
|
|
|
app.wait(timeout=10)
|
|
|
@@ -4974,37 +4969,31 @@ def bundle_answers(ctx):
|
|
|
ctx.app = app # teardown tracks the restarted process
|
|
|
ctx.auth = auth
|
|
|
|
|
|
- def saddle_row(name):
|
|
|
+ def schema_tables():
|
|
|
conn = db()
|
|
|
try:
|
|
|
- return conn.execute("select id, token from saddles where name = ?",
|
|
|
- (name,)).fetchone()
|
|
|
+ return {row[0] for row in conn.execute(
|
|
|
+ "select name from sqlite_master where type = 'table'")}
|
|
|
finally:
|
|
|
conn.close()
|
|
|
|
|
|
- check(wait_for(lambda: saddle_row("tokenless-orphan") is not None
|
|
|
- and (saddle_row("tokenless-orphan")[1] or "").startswith("saddle-")
|
|
|
- or False, timeout=20, what="tokenless row minted") is not None,
|
|
|
- "startup mints a token for a saddle row without one")
|
|
|
- check(wait_for(lambda: saddle_row("racer-two") is not None
|
|
|
- and saddle_row("racer-two")[1] == "saddle-legacy-copied-token"
|
|
|
- or False, timeout=20, what="legacy token copied") is not None,
|
|
|
- "startup copies the legacy api_tokens value onto the row")
|
|
|
- fake2.token = "saddle-legacy-copied-token" # the daemon asks with it
|
|
|
- status, _ = saddle_api("POST", "/api/saddle/next-task", "saddle-legacy-copied-token")
|
|
|
- check(status in (200, 204),
|
|
|
- "the legacy token still authenticates (copied, not replaced)")
|
|
|
+ check("api_tokens" not in schema_tables(),
|
|
|
+ "the fresh schema carries no api_tokens table (arch 24)")
|
|
|
conn = db()
|
|
|
- conn.execute("delete from saddles where name = 'tokenless-orphan'")
|
|
|
- conn.commit()
|
|
|
+ row_token = conn.execute("select token from saddles where id = ?",
|
|
|
+ (racer_two_id,)).fetchone()[0]
|
|
|
conn.close()
|
|
|
-
|
|
|
- # The restart wiped the in-memory ask bookkeeping; the fleet's
|
|
|
- # daemons re-establish sleeping with a fresh ask each (the real
|
|
|
- # daemons' idle-awake ask / 15-minute safety self-ask).
|
|
|
- for daemon in (fake2,):
|
|
|
- daemon.ask_now()
|
|
|
- check(True, "fleet re-asks after the restart (sleeping re-established)")
|
|
|
+ check(row_token == racer_two_token and (row_token or "").startswith("saddle-"),
|
|
|
+ "the per-saddle row token survives the restart unchanged")
|
|
|
+ status, _ = saddle_api("POST", "/api/saddle/next-task", racer_two_token)
|
|
|
+ check(status in (200, 204),
|
|
|
+ "the row token still authenticates after the restart")
|
|
|
+ status, _ = saddle_api("POST", "/api/saddle/next-task", "saddle-legacy-copied-token")
|
|
|
+ check(status == 401,
|
|
|
+ "a legacy api_tokens-shaped credential authorises nothing (fallback gone)")
|
|
|
+ fake2.token = racer_two_token
|
|
|
+ fake2.ask_now()
|
|
|
+ check(True, "the fleet re-asks after the restart (sleeping re-established)")
|
|
|
|
|
|
|
|
|
# =============================================================================
|