mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-09 23:55:12 +00:00
docs(perf): v4.17.1 results and harness fixes (#12651)
Publishes the v4.17.1 benchmark sweep and the harness fixes the sweep uncovered. ## Results All 11 published targets ran for 30 minutes each against a fixed 7-container deployment (vCPU 6 / 6 Gi, psql v17.4, US1 client and server), at 600 VUs except where noted. Latencies are `http_req_duration` over the test phase. | target | VUs | iter/s | p50 | p95 | p99 | failed requests | |:---|---:|---:|---:|---:|---:|:---| | machine_pat_login | 600 | 3452 | 24.53ms | 872ms | 1.00s | 29 / 6,335,633 (0.00%) | | user_info | 600 | 3217 | 29.32ms | 943ms | 1.13s | 66 / 5,800,788 (0.00%) | | introspect | 600 | 2297 | 46.49ms | 1.16s | 1.36s | 1,400 / 4,159,863 (0.03%) | | machine_jwt_profile_grant | 600 | 1076 | 85.34ms | 2.57s | 2.93s | 97 / 1,986,156 (0.00%) | | machine_client_credentials_login | 600 | 834 | 56.78ms | 1.74s | 2.42s | 1,789 / 3,073,497 (0.05%) | | add_session | 600 | 767 | 95.25ms | 3.53s | 4.15s | 1 / 1,416,546 (0.00%) | | oidc_session | 600 | 326 | 77.47ms | 3.41s | 4.43s | 3 / 1,767,162 (0.00%) | | otp_session | 600 | 84 | 47.80ms | 13.83s | 20.22s | 182 / 466,756 (0.03%) | | password_session | 600 | 68 | 408.63ms | 20.00s | 24.05s | 1 / 253,280 (0.00%) | | human_password_login | 600 | 49 | 146.67ms | 4.08s | 1m0s | 9,227 / 683,256 (1.35%) | | manipulate_user | **200** | 19 | 304.58ms | 10.23s | 38.43s | 225 / 171,987 (0.13%) | Against the goals: - **#8352 is met.** `machine_jwt_profile_grant` reached **1076/s** against the 1000/s goal, with p50 at 85ms. - **#4424 is not met.** `human_password_login` runs at **49/s** against a 1200/s goal. The sweep ran with `improvedPerformance` enabled (`PROJECT_GRANT`, `PROJECT`, `USER_GRANT`, `ORG_DOMAIN_VERIFIED`), captured from the instance before the first target rather than assumed; the full flag set is on every page. ## What the observability rows show The containers were never the constraint. ZITADEL CPU sat at 9–50% (p50), memory at 6–7% (p50, peaking at 16%) of 6 GiB, and instance count was flat at 7 with no container startups in any window. Database CPU was at **86–98% median on nine of eleven targets**. Any "ZITADEL achieves N/s" figure that omits which resource ran out is true and misleading, so the pages say. The query breakdown then shows what the CPU numbers cannot: **advisory lock wait is the single largest category of database time across the sweep (91,857 s accumulated)**, ahead of event reads (64,805 s), event push (26,411 s) and projection reads (14,625 s). The three locks are the projection handler lock (`handler/v2/handler.go`), the eventstore read barrier (`repository/sql/query.go`) and the shared lock every push takes (`eventstore/v3/push.go`). That is what sets the `flowchart outcome` on each page, and it is not uniform: - **Scale** on the seven targets where a resource genuinely ran out — database CPU at 92–98% with 0% lock share. - **Potential issues** on `human_password_login`, `password_session`, `otp_session` and `manipulate_user`, where 45–74% of the top query time is lock wait. `password_session` runs at 51% database CPU and 50% container CPU and still only manages 68 iter/s; adding vCPU there buys nothing. Autovacuum is cleared as a contributor: 93.9% median DB CPU in buckets containing a vacuum against 93.1% without, within noise. The new v4.17 eventstore autovacuum is barely triggered — `events2` is not in the top twenty vacuumed tables. ## Harness fixes Defects found during the sweep, all of which corrupted published numbers or aborted runs: 1. **Colliding check names.** `updateHuman`, `lockUser` and `deleteUser` all registered checks as `update user is status ok`, two of them asserting `status === 201` against endpoints returning 200. k6 aggregates checks by name, so this produced a permanent, fictional 28% failure rate. Each check now has its own name and accepts 2xx. 2. **User-creation read-after-write race**, in three variants, each found after the previous fix. The read-back resolved `undefined` when the projection was behind, surfacing much later as `Cannot read property '0' of undefined`; then an unguarded `res.json()` turned every request timeout into a killed iteration; then a *partial* read — a valid user object with `loginNames` not yet projected — passed an `if (user)` guard and failed identically. `readUserAfterCreate()` now guards status and body before parsing, requires the field rather than the object, and retries with backoff, rejecting with the user id and username. 3. **Metric cardinality.** Id-bearing URLs produced 800k+ time series against k6's suggested 100k, driving k6 to 6.8 GB and an OOM mid-run. All 16 such call sites now set a grouped `name` tag. 4. **`password_session` wrote its CSV to `otp_session`'s filename** in the Makefile, silently overwriting the previous target's output. The read-back race is not only a harness defect: at 600 VUs it measures real load-induced projection lag, exceeding 25 s for login-name projection and 100 s for the user projection, against instant reads on an unloaded instance. ## Stable v2 user API Four calls in `benchmark/src/user.ts` still went to `/v2beta/users` — update, lock, delete and the machine read-back. They date from the original load-test bootstrap in April 2024 (#7635), when the user service was still v2beta, and were never moved when it was promoted; they surfaced in this diff only because the read-after-write fix rewrote the surrounding functions. All four are now on stable v2. `updateHuman` is the one that changed shape: `PUT /v2beta/users/{id}` is `UpdateHumanUser`, whose stable-v2 counterpart `PUT /v2/users/human/{id}` is deprecated in the proto, so it now calls `PATCH /v2/users/{id}` (`UpdateUser`) and `manipulate_user` nests the profile under `human`. Both RPCs land on `command.ChangeUserHuman`, so the measured work is unchanged. `lockUser`, `deleteUser` and the `createMachine` read-back move over unchanged in method and payload. No published result is affected: per-endpoint latencies come from custom Trends, and `run-bench.sh` drops the `name` system tag, so no artefact ever recorded a v2beta URL. The remaining `/management/v1` and `/admin/v1` calls are left alone — migrating those changes which API surface is benchmarked and would need a re-run. ## Timestamps Pages reported `Test start` as a bare `HH:MM`, so a run could not be correlated against monitoring the next day. Pages now carry the full UTC date and time and a `Test end` row, both derived from the first and last per-second row of `output.json`. `_template.mdx` is updated to match. ## Post-mortem `benchmark/postmortems/2026-08-27-503-burst.md` records two bursts of HTTP 503 `Service error -27` — an HTML page from the Google frontend, not a ZITADEL error — on `manipulate_user` and `human_password_login`, six hours apart. **The investigation is open**, and the note is explicit about what is established (no container startups; the two bursts have opposite server-side signatures), what is provisionally ruled out (restart, panic), and what is not yet known. It is included because both affected pages report the 503s as a separate population from the ZITADEL failures beside them, and that split needs its evidence somewhere. It also records two defects found by accident in the collected logs, unrelated to the 503s: `sql: transaction has already been committed or rolled back` (232 occurrences under load) and a missing i18n key, `"unable to get user by id" not found in language "en"`. ## Notes for review - **`manipulate_user` ran at 200 VUs, not 600, and the page says so.** Throughput saturates near 100 VUs; at 600 the same target produced 4,283 failed requests for identical throughput against 225 at 200 VUs. Its failure mode is degradation over sustained single-org churn — per-operation p50 climbs 1.7x–6x between the first and last five minutes of a run — not a concurrency threshold. - **Two `Observed errors` rows are hand-corrected** and deliberately differ from the generated text, on `manipulate_user` and `human_password_login`. Both cases are one generated number covering two unrelated failure populations; each page says it was corrected and why. - Org deletion failing with a 500 (`V3-C8l3V`) after heavy user churn is already tracked as #8510 and was hit repeatedly during cleanup. - **The run/publish tooling is no longer part of this PR.** It moved to a private repository, along with the cloud collection scripts, because deployment identity does not belong here. What remains is the published pages, the post-mortem findings and the harness fixes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8de625dc48
commit
3be21ccbf0
@@ -19,6 +19,7 @@ export function createAPI(name: string, projectId: string, org: Org, accessToken
|
||||
authMethodType: 'API_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT',
|
||||
}),
|
||||
{
|
||||
tags: { name: '/management/v1/projects/{projectId}/apps/api' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -51,6 +52,7 @@ export function createAppKey(appId: string, projectId: string, org: Org, accessT
|
||||
type: 'KEY_TYPE_JSON',
|
||||
}),
|
||||
{
|
||||
tags: { name: '/management/v1/projects/{projectId}/apps/{appId}/keys' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -172,6 +172,7 @@ export async function token(request: TokenRequest): Promise<Tokens> {
|
||||
const authRequestByIDTrend = new Trend('oidc_auth_request_by_id_duration', true);
|
||||
export async function authRequestByID(id: string, tokens: any): Promise<Response> {
|
||||
const response = http.get(url(`/v2/oidc/auth_requests/${id}`), {
|
||||
tags: { name: '/v2/oidc/auth_requests/{id}' },
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
},
|
||||
@@ -195,6 +196,7 @@ export async function finalizeAuthRequest(id: string, session: any, tokens: any)
|
||||
},
|
||||
}),
|
||||
{
|
||||
tags: { name: '/v2/oidc/auth_requests/{id}' },
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
export async function mapPool<T, R>(
|
||||
items: T[],
|
||||
concurrency: number,
|
||||
concurrency: number,
|
||||
fn: (item: T, index: number) => Promise<R>,
|
||||
): Promise<R[]> {
|
||||
if (items.length === 0) {
|
||||
|
||||
@@ -40,6 +40,7 @@ export function setSession(id: string, session: any, accessToken: string, challe
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('PATCH', url(`/v2/sessions/${id}`), JSON.stringify(body), {
|
||||
tags: { name: '/v2/sessions/{id}' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { loginByUsernamePassword } from '../login_ui';
|
||||
import { createOrg, removeOrg } from '../org';
|
||||
import { createHuman, updateHuman, lockUser, deleteUser, User } from '../user';
|
||||
import { Config } from '../config';
|
||||
import { check } from 'k6';
|
||||
|
||||
export async function setup() {
|
||||
const tokens = loginByUsernamePassword(Config.admin as User);
|
||||
@@ -16,29 +15,22 @@ export async function setup() {
|
||||
|
||||
export default async function (data: any) {
|
||||
const human = await createHuman(`vu-${__VU}-${new Date(Date.now()).getTime()}`, data.org, data.tokens.accessToken);
|
||||
const updateRes = await updateHuman(
|
||||
// updateHuman/lockUser/deleteUser each register their own check; repeating them
|
||||
// here would double count, and k6 aggregates checks by name.
|
||||
await updateHuman(
|
||||
{
|
||||
profile: {
|
||||
nickName: `${new Date(Date.now()).toISOString()}`,
|
||||
human: {
|
||||
profile: {
|
||||
nickName: `${new Date(Date.now()).toISOString()}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
human.userId,
|
||||
data.org,
|
||||
data.tokens.accessToken,
|
||||
);
|
||||
check(updateRes, {
|
||||
'update user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
|
||||
const lockRes = await lockUser(human.userId, data.org, data.tokens.accessToken);
|
||||
check(lockRes, {
|
||||
'lock user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
|
||||
const deleteRes = await deleteUser(human.userId, data.org, data.tokens.accessToken);
|
||||
check(deleteRes, {
|
||||
'delete user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
await lockUser(human.userId, data.org, data.tokens.accessToken);
|
||||
await deleteUser(human.userId, data.org, data.tokens.accessToken);
|
||||
}
|
||||
|
||||
export function teardown(data: any) {
|
||||
|
||||
+186
-95
@@ -3,6 +3,7 @@ import { Org } from './org';
|
||||
import http, { RefinedResponse } from 'k6/http';
|
||||
import url from './url';
|
||||
import { check } from 'k6';
|
||||
import { setTimeout } from 'k6/timers';
|
||||
import { Metadata } from './metadata';
|
||||
|
||||
export type User = {
|
||||
@@ -15,64 +16,149 @@ export interface Human extends User {
|
||||
loginNames: string[];
|
||||
}
|
||||
|
||||
const createHumanTrend = new Trend('user_create_human_duration', true);
|
||||
export function createHuman(username: string, org: Org, accessToken: string): Promise<Human> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest(
|
||||
'POST',
|
||||
url('/v2/users/human'),
|
||||
JSON.stringify({
|
||||
username: username,
|
||||
organization: {
|
||||
orgId: org.organizationId,
|
||||
},
|
||||
profile: {
|
||||
givenName: 'Gigi',
|
||||
familyName: 'Zitizen',
|
||||
},
|
||||
email: {
|
||||
email: `${username}@zitadel.com`,
|
||||
isVerified: true,
|
||||
},
|
||||
password: {
|
||||
password: 'Password1!',
|
||||
changeRequired: false,
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
// A user is created by a write, but its login names can only be read back from the
|
||||
// query side. Under load that projection can lag behind the write, and the read
|
||||
// then returns no user at all. Retry briefly rather than resolving undefined, which
|
||||
// surfaces much later as "Cannot read property '0' of undefined" while mapping
|
||||
// login names, hiding the real cause.
|
||||
// Diagnostics from the last attempt, so an exhausted retry loop can say *why* it gave
|
||||
// up rather than only that it did. Two failures on 2026-08-27 burned 30 attempts each
|
||||
// and reported nothing more useful than "not readable", which cost a whole cycle.
|
||||
export type ReadUserResult = { user?: any; lastStatus: number; lastBody: string; attempts: number };
|
||||
|
||||
// Backoff that yields to the event loop. `sleep()` would not: k6 documents that it "blocks VU
|
||||
// execution and prevents promises from resolving and event handlers from running", and the same
|
||||
// is true of the synchronous `http.get`. That matters here and nowhere else in this file, because
|
||||
// this is the one helper called from inside a `.then()` callback while up to MaxVUs sibling
|
||||
// creations are still in flight -- blocking serialises every one of them behind this loop.
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function readUserAfterCreate(path: string, userId: string, org: Org, accessToken: string): Promise<ReadUserResult> {
|
||||
let lastStatus = -1;
|
||||
let lastBody = '';
|
||||
let attempt = 0;
|
||||
for (attempt = 0; attempt < 60; attempt++) {
|
||||
const res = await http.asyncRequest('GET', url(`${path}/${userId}`), null, {
|
||||
tags: { name: `${path}/{userId}` },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
});
|
||||
// A timed-out request has status 0 and a null body. Calling res.json() on that
|
||||
// throws "GoError: the body is null" and kills the iteration instead of consuming
|
||||
// a retry, which is exactly the failure this retry loop exists to absorb.
|
||||
if (res.status >= 200 && res.status < 300 && res.body) {
|
||||
const user = res.json('user') as any;
|
||||
// The user record can come back before its login names have been projected.
|
||||
// Accepting a merely-truthy user here returns a partial record, and every
|
||||
// caller immediately does loginNames[0] on it -- so this must wait for the
|
||||
// field itself, not just for the object. Observed at 600 VUs on
|
||||
// human_password_login, 2026-08-27: ten retries available, none consumed.
|
||||
if (user && Array.isArray(user.loginNames) && user.loginNames.length > 0) {
|
||||
// A loop that only reports itself when it gives up says nothing about how close it
|
||||
// came, which is how the budget below got guessed at three times before it was
|
||||
// measured once. Deliberately a log line and not a Trend: a Trend recorded here
|
||||
// reaches output.json for manipulate_user, where this runs inside the measured
|
||||
// iteration, and moving a published series to observe it would defeat the point.
|
||||
if (attempt > 0) {
|
||||
console.warn(
|
||||
`read-back retried: user ${userId} readable after ${attempt + 1} attempts, previous status ${lastStatus}`,
|
||||
);
|
||||
}
|
||||
return { user, lastStatus: res.status, lastBody: '', attempts: attempt + 1 };
|
||||
}
|
||||
}
|
||||
lastStatus = res.status;
|
||||
lastBody = (res.body ? String(res.body) : '<null body>').slice(0, 200);
|
||||
// ~100s of waiting in total (backoff rises to 2s and stays there). Sized against a
|
||||
// measured behaviour, not a guess: unloaded, login names are present on the *first*
|
||||
// GET, but a burst of MaxVUs concurrent creations queues them behind the projection
|
||||
// handler advisory lock -- the largest single database cost in the whole v4.17.1 sweep.
|
||||
//
|
||||
// Two things measured on 2026-08-31 that this budget cannot fix, recorded so the next
|
||||
// person does not widen it a fourth time:
|
||||
//
|
||||
// Retries are never consumed successfully. Across every run that day the warn above
|
||||
// fired zero times -- each user either had its login names on the *first* GET or never
|
||||
// had them at all. The read-back is bimodal, so the budget only sets how long we wait
|
||||
// before reporting a failure, not how many succeed.
|
||||
//
|
||||
// And some of those failures are permanent. In one 200-VU run all 200 creates returned
|
||||
// 2xx and, ten minutes later, the query side was still missing zitizen-6 entirely and
|
||||
// zitizen-7/-24/-25 had no login names. Widening the budget to ~220s changed nothing
|
||||
// except how long it took to give up, which is why it was reverted to this.
|
||||
//
|
||||
// Note also that serialising this loop used to grant an accidental grace period: user N
|
||||
// was not asked until users 0..N-1 had finished, so a late user got the queue's time for
|
||||
// free. Awaiting concurrently aligns every deadline on the same instant. That is correct
|
||||
// and strictly less forgiving, and it is a real effect -- it was simply not the cause of
|
||||
// the failures above, which were loss rather than lag.
|
||||
await delay(Math.min(100 * (attempt + 1), 2000));
|
||||
}
|
||||
return { user: undefined, lastStatus, lastBody, attempts: attempt };
|
||||
}
|
||||
|
||||
const createHumanTrend = new Trend('user_create_human_duration', true);
|
||||
export async function createHuman(username: string, org: Org, accessToken: string): Promise<Human> {
|
||||
const res = await http.asyncRequest(
|
||||
'POST',
|
||||
url('/v2/users/human'),
|
||||
JSON.stringify({
|
||||
username: username,
|
||||
organization: {
|
||||
orgId: org.organizationId,
|
||||
},
|
||||
profile: {
|
||||
givenName: 'Gigi',
|
||||
familyName: 'Zitizen',
|
||||
},
|
||||
email: {
|
||||
email: `${username}@zitadel.com`,
|
||||
isVerified: true,
|
||||
},
|
||||
password: {
|
||||
password: 'Password1!',
|
||||
changeRequired: false,
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!check(res, {
|
||||
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
})
|
||||
) {
|
||||
throw new Error(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
|
||||
}
|
||||
createHumanTrend.add(res.timings.duration);
|
||||
|
||||
const userId = res.json('userId') as string;
|
||||
const read = await readUserAfterCreate('/v2/users', userId, org, accessToken);
|
||||
if (!read.user) {
|
||||
throw new Error(
|
||||
`user ${userId} not readable with login names after create ` +
|
||||
`(username: ${username}, attempts: ${read.attempts}, last status: ${read.lastStatus}, ` +
|
||||
`last body: ${read.lastBody})`,
|
||||
);
|
||||
|
||||
response
|
||||
.then((res) => {
|
||||
check(res, {
|
||||
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
}) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
|
||||
createHumanTrend.add(res.timings.duration);
|
||||
|
||||
const user = http.get(url(`/v2/users/${res.json('userId')!}`), {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
});
|
||||
resolve(user.json('user')! as unknown as Human);
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
return read.user as unknown as Human;
|
||||
}
|
||||
|
||||
const setEmailOTPOnHumanTrend = new Trend('set_human_email_otp_duration', true);
|
||||
export async function setEmailOTPOnHuman(user: User, org: Org, accessToken: string): Promise<void> {
|
||||
const response = await http.asyncRequest('POST', url(`/v2/users/${user.userId}/otp_email`), null, {
|
||||
tags: { name: '/v2/users/{userId}/otp_email' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -95,7 +181,8 @@ export function updateHuman(
|
||||
accessToken: string,
|
||||
): Promise<RefinedResponse<any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('PUT', url(`/v2beta/users/${userId}`), JSON.stringify(payload), {
|
||||
let response = http.asyncRequest('PATCH', url(`/v2/users/${userId}`), JSON.stringify(payload), {
|
||||
tags: { name: '/v2/users/{userId}' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -106,7 +193,7 @@ export function updateHuman(
|
||||
response
|
||||
.then((res) => {
|
||||
check(res, {
|
||||
'update user is status ok': (r) => r.status === 201,
|
||||
'update user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
updateHumanTrend.add(res.timings.duration);
|
||||
resolve(res);
|
||||
@@ -122,46 +209,44 @@ export interface Machine extends User {
|
||||
}
|
||||
|
||||
const createMachineTrend = new Trend('user_create_machine_duration', true);
|
||||
export function createMachine(username: string, org: Org, accessToken: string): Promise<Machine> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest(
|
||||
'POST',
|
||||
url('/management/v1/users/machine'),
|
||||
JSON.stringify({
|
||||
userName: username,
|
||||
name: username,
|
||||
// bearer
|
||||
access_token_type: 0,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
export async function createMachine(username: string, org: Org, accessToken: string): Promise<Machine> {
|
||||
const res = await http.asyncRequest(
|
||||
'POST',
|
||||
url('/management/v1/users/machine'),
|
||||
JSON.stringify({
|
||||
userName: username,
|
||||
name: username,
|
||||
// bearer
|
||||
access_token_type: 0,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
!check(res, {
|
||||
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
})
|
||||
) {
|
||||
throw new Error(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
|
||||
}
|
||||
createMachineTrend.add(res.timings.duration);
|
||||
|
||||
const userId = res.json('userId') as string;
|
||||
const read = await readUserAfterCreate('/v2/users', userId, org, accessToken);
|
||||
if (!read.user) {
|
||||
throw new Error(
|
||||
`user ${userId} not readable with login names after create ` +
|
||||
`(username: ${username}, attempts: ${read.attempts}, last status: ${read.lastStatus}, ` +
|
||||
`last body: ${read.lastBody})`,
|
||||
);
|
||||
|
||||
response
|
||||
.then((res) => {
|
||||
check(res, {
|
||||
'create user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
}) || reject(`unable to create user(username: ${username}) status: ${res.status} body: ${res.body}`);
|
||||
createMachineTrend.add(res.timings.duration);
|
||||
|
||||
const user = http.get(url(`/v2beta/users/${res.json('userId')!}`), {
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-zitadel-orgid': org.organizationId,
|
||||
},
|
||||
});
|
||||
resolve(user.json('user')! as unknown as Machine);
|
||||
})
|
||||
.catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
return read.user as unknown as Machine;
|
||||
}
|
||||
|
||||
export type MachinePat = {
|
||||
@@ -172,6 +257,7 @@ const addMachinePatTrend = new Trend('user_add_machine_pat_duration', true);
|
||||
export function addMachinePat(userId: string, org: Org, accessToken: string): Promise<MachinePat> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/pats`), null, {
|
||||
tags: { name: '/management/v1/users/{userId}/pats' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -198,6 +284,7 @@ const addMachineSecretTrend = new Trend('user_add_machine_secret_duration', true
|
||||
export function addMachineSecret(userId: string, org: Org, accessToken: string): Promise<MachineSecret> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('PUT', url(`/management/v1/users/${userId}/secret`), null, {
|
||||
tags: { name: '/management/v1/users/{userId}/secret' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -232,6 +319,7 @@ export function addMachineKey(userId: string, org: Org, accessToken: string, pub
|
||||
publicKey: publicKey,
|
||||
}),
|
||||
{
|
||||
tags: { name: '/management/v1/users/{userId}/keys' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -253,7 +341,8 @@ export function addMachineKey(userId: string, org: Org, accessToken: string, pub
|
||||
const lockUserTrend = new Trend('lock_user_duration', true);
|
||||
export function lockUser(userId: string, org: Org, accessToken: string): Promise<RefinedResponse<any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('POST', url(`/v2beta/users/${userId}/lock`), null, {
|
||||
let response = http.asyncRequest('POST', url(`/v2/users/${userId}/lock`), null, {
|
||||
tags: { name: '/v2/users/{userId}/lock' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -264,7 +353,7 @@ export function lockUser(userId: string, org: Org, accessToken: string): Promise
|
||||
response
|
||||
.then((res) => {
|
||||
check(res, {
|
||||
'update user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
'lock user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
lockUserTrend.add(res.timings.duration);
|
||||
resolve(res);
|
||||
@@ -278,7 +367,8 @@ export function lockUser(userId: string, org: Org, accessToken: string): Promise
|
||||
const deleteUserTrend = new Trend('delete_user_duration', true);
|
||||
export function deleteUser(userId: string, org: Org, accessToken: string): Promise<RefinedResponse<any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('DELETE', url(`/v2beta/users/${userId}`), null, {
|
||||
let response = http.asyncRequest('DELETE', url(`/v2/users/${userId}`), null, {
|
||||
tags: { name: '/v2/users/{userId}' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
@@ -289,7 +379,7 @@ export function deleteUser(userId: string, org: Org, accessToken: string): Promi
|
||||
response
|
||||
.then((res) => {
|
||||
check(res, {
|
||||
'update user is status ok': (r) => r.status === 201,
|
||||
'delete user is status ok': (r) => r.status >= 200 && r.status < 300,
|
||||
});
|
||||
deleteUserTrend.add(res.timings.duration);
|
||||
resolve(res);
|
||||
@@ -304,6 +394,7 @@ const setUserMetadataTrend = new Trend('set_user_metadata_duration', true);
|
||||
export function setUserMetadata(metadata: Metadata[], userId: string, accessToken: string): Promise<RefinedResponse<any>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let response = http.asyncRequest('POST', url(`/v2/users/${userId}/metadata`), JSON.stringify({ metadata: metadata }), {
|
||||
tags: { name: '/v2/users/{userId}/metadata' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -26,6 +26,7 @@ export async function addUserGrant(
|
||||
roleKeys: roles,
|
||||
}),
|
||||
{
|
||||
tags: { name: '/management/v1/users/{userId}/grants' },
|
||||
headers: {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
Reference in New Issue
Block a user