Skip to content

Commit d732881

Browse files
authored
[gcx-token] Name minted tokens by token_name; supersede same-label token (#8155)
Follow-up to #8154 (review comment from @huydhn). Each call to `/api/gcx-token` minted a new long-lived token without removing old ones, so repeat calls piled up valid tokens under the `gcx-<login>` service account. Now the optional `?token_name=` query param (defaults to `default`) labels the token, and minting revokes only the previous token with the **same label**. So a user can hold one token per machine by passing `?token_name=$(hostname)`, and re-running with the same label replaces just that token. No server-side storage; the user holds the token. ```bash export GRAFANA_TOKEN=$(curl -fsSL -H "Authorization: Bearer $(gh auth token)" "https://hud.pytorch.org/api/gcx-token?token_name=$(hostname)") ```
1 parent fed9869 commit d732881

3 files changed

Lines changed: 63 additions & 12 deletions

File tree

torchci/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ Primary usage — reuse an existing GitHub token (no browser, nothing to install
8989
```bash
9090
export GRAFANA_TOKEN=$(curl -fsSL \
9191
-H "Authorization: Bearer $(gh auth token)" \
92-
https://hud.pytorch.org/api/gcx-token)
92+
"https://hud.pytorch.org/api/gcx-token?token_name=$(hostname)")
9393
```
9494

9595
Each GitHub user gets a dedicated service account named `gcx-<github-login>`
96-
with the Viewer role. Tokens are long-lived; revoke them manually in the Grafana
97-
UI (delete the token or the `gcx-<login>` service account).
96+
with the Viewer role. The optional `token_name` param labels the token
97+
(defaults to "default"), so you can hold one token per machine; re-running with
98+
the same label replaces only that token. Revoke manually in the Grafana UI if
99+
needed.
98100

99101
Required server-side env vars (Vercel):
100102

torchci/lib/grafana/serviceAccount.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,57 @@ async function createViewerServiceAccount(name: string): Promise<number> {
8080
return data.id;
8181
}
8282

83+
// Delete tokens on the service account whose name starts with `prefix`, so a
84+
// new mint supersedes only the previous token with the same label, leaving
85+
// other labels' tokens intact.
86+
async function revokeTokensWithPrefix(
87+
saId: number,
88+
prefix: string
89+
): Promise<void> {
90+
const res = await grafanaFetch(`/api/serviceaccounts/${saId}/tokens`);
91+
if (!res.ok) {
92+
throw new Error(
93+
`Grafana token list failed: ${res.status} ${await res.text()}`
94+
);
95+
}
96+
const tokens: Array<{ id: number; name: string }> = (await res.json()) || [];
97+
for (const token of tokens) {
98+
if (token.name.startsWith(prefix)) {
99+
await grafanaFetch(`/api/serviceaccounts/${saId}/tokens/${token.id}`, {
100+
method: "DELETE",
101+
});
102+
}
103+
}
104+
}
105+
106+
// Slug for the caller-supplied token label (no dashes, so it can't collide with
107+
// the dash separators in the token name). Defaults to "default".
108+
function labelSlug(label: string): string {
109+
return (label || "").replace(/[^A-Za-z0-9.]/g, "").slice(0, 40) || "default";
110+
}
111+
83112
/**
84-
* Find-or-create the Viewer service account for `login` and mint a token on it.
85-
* Returns the raw token key (the only time Grafana exposes it).
113+
* Find-or-create the Viewer service account for `login`, revoke any previous
114+
* token with the same `label`, and mint a fresh one. Returns the raw token key
115+
* (the only time Grafana exposes it). Tokens are named per label so a user can
116+
* hold one per machine; re-minting with the same label replaces that token.
86117
*/
87-
export async function mintGcxViewerToken(login: string): Promise<string> {
118+
export async function mintGcxViewerToken(
119+
login: string,
120+
label: string
121+
): Promise<string> {
88122
const name = serviceAccountName(login);
123+
const prefix = `${name}-${labelSlug(label)}-`;
89124

90125
let saId = await findServiceAccountIdByName(name);
91126
if (saId == null) {
92127
saId = await createViewerServiceAccount(name);
128+
} else {
129+
await revokeTokensWithPrefix(saId, prefix);
93130
}
94131

95-
// Timestamp keeps the token name unique per service account.
96-
const tokenName = `${name}-${Date.now()}`;
132+
// Timestamp keeps the token name unique per (service account, label).
133+
const tokenName = `${prefix}${Date.now()}`;
97134
const res = await grafanaFetch(`/api/serviceaccounts/${saId}/tokens`, {
98135
method: "POST",
99136
body: JSON.stringify({ name: tokenName }),

torchci/pages/api/gcx-token.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
*
1616
* export GRAFANA_TOKEN=$(curl -fsSL \
1717
* -H "Authorization: Bearer $(gh auth token)" \
18-
* https://hud.pytorch.org/api/gcx-token)
18+
* "https://hud.pytorch.org/api/gcx-token?token_name=$(hostname)")
19+
*
20+
* The optional `token_name` param labels the token (defaults to "default"), so a
21+
* user can hold one token per machine and re-minting only replaces the token
22+
* with the same label.
1923
*
2024
* Browser-authenticated users (a live NextAuth session) are also accepted as a
2125
* fallback. Returns the raw token as text/plain by default, or JSON when the
@@ -56,10 +60,18 @@ export default async function handler(
5660
return res.status(auth.status).json({ error: auth.error });
5761
}
5862

59-
// 3. Mint a read-only (Viewer) Grafana token for this user.
63+
// 3. Mint a read-only (Viewer) Grafana token for this user. The optional
64+
// `token_name` query param (e.g. ?token_name=$(hostname)) labels the token
65+
// so re-minting only supersedes the token with the same label.
6066
try {
61-
const token = await mintGcxViewerToken(auth.login);
62-
console.log(`gcx-token: minted Viewer token for ${auth.login}`);
67+
const label =
68+
typeof req.query.token_name === "string" ? req.query.token_name : "";
69+
const token = await mintGcxViewerToken(auth.login, label);
70+
console.log(
71+
`gcx-token: minted Viewer token for ${auth.login}${
72+
label ? ` (${label})` : ""
73+
}`
74+
);
6375

6476
const accept = (req.headers["accept"] as string) || "";
6577
if (accept.includes("application/json") || req.query.format === "json") {

0 commit comments

Comments
 (0)