Compare commits

...

39 Commits

Author SHA1 Message Date
cde4e395e9 add user group creation
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-22 17:16:14 +02:00
d19c692f81 fix testcases
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:48:26 +02:00
d5d2be5672 fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:28:48 +02:00
8597db59f5 fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:25:53 +02:00
74fb9492bc fix duration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:25:32 +02:00
defbafb55e fix with users
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 14:24:21 +02:00
e2ed7391bc fix event list creation
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 13:39:42 +02:00
8dcd0dcaa9 remove multiprocess for now
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 18:45:54 +02:00
18eee1b722 rework fixtures, paralelize
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 18:31:14 +02:00
d0f6c815c3 fix
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:09:43 +02:00
b13eba3b0a add meaningful test for provider oauth2
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:08:04 +02:00
77fe4e9fe2 add group and event list
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-18 17:05:40 +02:00
71fe8b4fb3 Merge branch 'main' into benchmarks 2024-04-17 00:42:10 +02:00
b14cb832b2 user list: hopefully fix memory usage
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 23:56:32 +02:00
24b5296d88 fix timeout
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 23:54:03 +02:00
41b7e50bc6 typo
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-16 17:19:36 +02:00
6b750d7c59 fix fixtures idempotency
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 21:50:19 +02:00
d268c28934 allow vus count configuration
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 20:31:39 +02:00
688404b6a5 allow configuring remove write endpoint
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 20:00:14 +02:00
cbd2425a5f remove useless prom args
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 16:45:38 +02:00
877c264d59 idempotent fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 16:18:31 +02:00
2575b540fa proper url tags
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 15:06:29 +02:00
0e0b76a62e fix external labels
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:46:50 +02:00
6d625fd1d7 support other than localhost
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:30:53 +02:00
bd0630e300 fix main fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 14:25:31 +02:00
ffb7d44024 config for thanos
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-15 13:59:16 +02:00
7589b11f98 add tests for policies
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-12 17:58:36 +02:00
ad21dfa2bc Merge branch 'main' into benchmarks 2024-04-11 19:11:27 +02:00
95692f5a7c provider oauth2 test
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-11 19:10:47 +02:00
1f4ed1defa user list: add support for with_groups
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 17:17:20 +02:00
334b183465 optimize fixtures, better user_list tests
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 17:13:24 +02:00
1f789dd4c5 more cleanup
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 16:24:44 +02:00
057e5747c9 remove custom k6 install
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 16:24:18 +02:00
8717a3aaab fix
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 14:18:59 +02:00
527173236a Merge branch 'main' into benchmarks 2024-04-09 14:17:28 +02:00
3e6eb6f248 add login tests
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-09 14:16:58 +02:00
6babf0f1c4 add graphs
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-08 17:29:58 +02:00
ca7cc30504 use tenants for fixtures
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-08 11:59:40 +02:00
a7cb808cad init benchmarks
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-05 05:32:40 +02:00
16 changed files with 1060 additions and 0 deletions

7
.gitignore vendored
View File

@ -209,3 +209,10 @@ source_docs/
### Golang ###
/vendor/
### Benchmark ###
tests/benchmark/k6
tests/benchmark/prometheus
tests/benchmark/**/*.json
tests/benchmark/**/*.ndjson
tests/benchmark/**/*.html

View File

@ -278,3 +278,20 @@ ci-bandit: ci--meta-debug
ci-pending-migrations: ci--meta-debug
ak makemigrations --check
#########################
## Benchmark
#########################
benchmark-fixtures-create:
tests/benchmark/fixtures.py create
benchmark-run:
docker compose -f tests/benchmark/docker-compose.yml up -d
sleep 5
tests/benchmark/run.sh
benchmark-fixtures-delete:
tests/benchmark/fixtures.py delete
benchmark: benchmark-fixtures-create benchmark-run benchmark-fixtures-delete

View File

@ -4,6 +4,7 @@ services:
postgresql:
container_name: postgres
image: docker.io/library/postgres:16
command: "-c max_connections=500"
volumes:
- db-data:/var/lib/postgresql/data
environment:

View File

View File

@ -0,0 +1,30 @@
---
services:
prometheus:
image: quay.io/prometheus/prometheus:latest
restart: unless-stopped
command:
- --enable-feature=native-histograms
- --web.enable-remote-write-receiver
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.console.libraries=/usr/share/prometheus/console_libraries
- --web.console.templates=/usr/share/prometheus/consoles
ports:
- 127.0.0.1:9090:9090
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus:/prometheus
user: root
grafana:
image: grafana/grafana:latest
restart: unless-stopped
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
ports:
- 127.0.0.1:3000:3000
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro

View File

@ -0,0 +1,79 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of events, page size
[1000, 100],
[10000, 20],
[10000, 100],
[100000, 100],
[1000000, 100],
].map((obj, i) => [
`${obj[0]}_${obj[1]}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
EVENT_COUNT: `${obj[0]}`,
PAGE_SIZE: `${obj[1]}`,
},
tags: {
testid: `event_list_${obj[0]}_${obj[1]}`,
event_count: `${obj[0]}`,
page_size: `${obj[1]}`,
},
},
]),
),
};
export default function () {
const event_count = Number(__ENV.EVENT_COUNT);
const domain = `event-list-${event_count}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(event_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/events/events/?page=${page}&page_size=${page_size}`,
params,
);
check(res, {
"status is 200": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/events/events/?page=${page}&page_size=${page_size}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}

345
tests/benchmark/fixtures.py Executable file
View File

@ -0,0 +1,345 @@
#!/usr/bin/env python3
import random
import sys
from collections.abc import Iterable
from multiprocessing import Process
from os import environ
from uuid import uuid4
import django
environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
environ.setdefault("AUTHENTIK_BOOTSTRAP_PASSWORD", "akadmin")
environ.setdefault("AUTHENTIK_BOOTSTRAP_TOKEN", "akadmin")
environ.setdefault("AUTHENTIK_BOOTSTRAP_EMAIL", "akadmin@authentik.test")
django.setup()
from django.conf import settings
from authentik.core.models import Application, Group, User
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow
from authentik.policies.expression.models import ExpressionPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.stages.authenticator_static.models import StaticToken
from authentik.tenants.models import Domain, Tenant
settings.CELERY["task_always_eager"] = True
host = environ.get("BENCH_HOST", "localhost")
class TestSuite:
TEST_NAME: str
TEST_CASES: Iterable[Iterable[int | str | bool]]
@classmethod
def get_testcases(cls):
return [cls(params) for params in cls.TEST_CASES]
def __init__(self, params: Iterable[int | str | bool]):
self.params = params
def __str__(self):
return (
"-".join([self.TEST_NAME] + [str(param) for param in self.params])
.replace("_", "-")
.lower()
)
@property
def schema_name(self):
return f"t_{str(self).replace('-', '_')}"
@property
def domain_name(self):
return f"{str(self)}.{host}"
def create(self):
created = False
t = Tenant.objects.filter(schema_name=self.schema_name).first()
if not t:
created = True
t = Tenant.objects.create(schema_name=self.schema_name, name=uuid4())
Domain.objects.get_or_create(tenant=t, domain=self.domain_name)
if created:
with t:
self.create_data(*self.params)
def create_data(self):
raise NotImplementedError
def delete(self):
Tenant.objects.filter(schema_name=self.schema_name).delete()
class UserList(TestSuite):
TEST_NAME = "user-list"
TEST_CASES = [
(1000, 0, 0),
(10000, 0, 0),
(1000, 3, 0),
(10000, 3, 0),
(1000, 20, 0),
(10000, 20, 0),
(1000, 20, 3),
(10000, 20, 3),
]
def create_data(self, user_count: int, groups_per_user: int, parents_per_group: int):
Group.objects.bulk_create([Group(name=uuid4()) for _ in range(groups_per_user * 5)])
for group in Group.objects.exclude(name="authentik Admins"):
for _ in range(parents_per_group):
new_group = Group.objects.create(name=uuid4())
group.parent = new_group
group.save()
group = new_group
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(user_count)
]
)
if groups_per_user:
for user in User.objects.exclude_anonymous().exclude(username="akadmin"):
user.ak_groups.set(
Group.objects.exclude(name="authentik Admins").order_by("?")[:groups_per_user]
)
class GroupList(TestSuite):
TEST_NAME = "group-list"
TEST_CASES = [
(1000, 0, False),
(10000, 0, False),
(1000, 1000, False),
(1000, 10000, False),
(1000, 0, True),
(10000, 0, True),
]
def create_data(self, group_count, users_per_group, with_parent):
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(users_per_group * 5)
]
)
if with_parent:
parents = Group.objects.bulk_create([Group(name=uuid4()) for _ in range(group_count)])
groups = Group.objects.bulk_create(
[
Group(name=uuid4(), parent=(parents[i] if with_parent else None))
for i in range(group_count)
]
)
if users_per_group:
for group in groups:
group.users.set(
User.objects.exclude_anonymous()
.exclude(username="akadmin")
.order_by("?")[:users_per_group]
)
class Login(TestSuite):
TEST_NAME = "login"
TEST_CASES = [
("no-mfa",),
("with-mfa",),
]
def create_data(self, mfa: str):
user = User(username="test", name=uuid4())
user.set_password("verySecurePassword")
user.save()
if mfa == "with-mfa":
device = user.staticdevice_set.create()
# Multiple token with same token for all the iterations in the test
device.token_set.bulk_create(
[StaticToken(device=device, token=f"staticToken") for _ in range(1_000_000)]
)
class ProviderOauth2(TestSuite):
TEST_NAME = "provider-oauth2"
TEST_CASES = [
(2, 50, 2),
(0, 0, 0),
(10, 0, 0),
(100, 0, 0),
(0, 10, 0),
(0, 100, 0),
(0, 0, 10),
(0, 0, 100),
(10, 10, 10),
(100, 100, 100),
]
def create_data(
self, user_policies_count: int, group_policies_count: int, expression_policies_count: int
):
user = User(username="test", name=uuid4())
user.set_password("verySecurePassword")
user.save()
provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
),
signing_key=CertificateKeyPair.objects.get(name="authentik Self-signed Certificate"),
redirect_uris="http://test.localhost",
client_id="123456",
client_secret="123456",
)
application = Application.objects.create(slug="test", name="test", provider=provider)
User.objects.bulk_create(
[
User(
username=uuid4(),
name=uuid4(),
)
for _ in range(user_policies_count)
]
)
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
user=user,
target=application,
order=random.randint(1, 1_000_000),
)
for user in User.objects.exclude(username="akadmin").exclude_anonymous()
]
)
Group.objects.bulk_create([Group(name=uuid4()) for _ in range(group_policies_count)])
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
group=group,
target=application,
order=random.randint(1, 1_000_000),
)
for group in Group.objects.exclude(name="authentik Admins")
]
)
user.ak_groups.set(Group.objects.exclude(name="authentik Admins").order_by("?")[:1])
[
ExpressionPolicy(
name=f"test-{uuid4()}",
expression="return True",
).save()
for _ in range(expression_policies_count)
]
PolicyBinding.objects.bulk_create(
[
PolicyBinding(
policy=policy,
target=application,
order=random.randint(1, 1_000_000),
)
for policy in ExpressionPolicy.objects.filter(name__startswith="test-")
]
)
class EventList(TestSuite):
TEST_NAME = "event-list"
TEST_CASES = [
(1_000,),
(10_000,),
(100_000,),
(1_000_000,),
]
def create_data(self, event_count: int):
for _ in range(event_count // 1000):
Event.objects.bulk_create(
[
Event(
user={
"pk": str(uuid4()),
"name": str(uuid4()),
"username": str(uuid4()),
"email": f"{uuid4()}@example.org",
},
action="custom_benchmark",
app="tests_benchmarks",
context={
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
str(uuid4()): str(uuid4()),
},
client_ip="192.0.2.42",
)
for _ in range(1000)
]
)
class UserGroupCreate(TestSuite):
TEST_NAME = "user-group-create"
TEST_CASES = [
(),
]
def create_data(self):
pass
def main(action: str, selected_suite: str | None = None):
testsuites = TestSuite.__subclasses__()
testcases = []
for testsuite in testsuites:
testcases += testsuite.get_testcases()
match action:
case "create":
to_create = []
for testcase in testcases:
if selected_suite and testcase.TEST_NAME != selected_suite:
continue
testcase.create()
# to_create.append(testcase)
# processes = [Process(target=testcase.create) for testcase in to_create]
# for p in processes:
# p.start()
# for p in processes:
# p.join()
case "list":
print(*[testsuite.TEST_NAME for testsuite in testsuites], sep="\n")
case "delete":
for testcase in testcases:
if selected_suite and testcase.TEST_NAME != selected_suite:
continue
testcase.delete()
case _:
print("Unknown action. Should be create, list or delete")
exit(1)
if __name__ == "__main__":
if len(sys.argv) < 2:
action = "create"
else:
action = sys.argv[1]
if len(sys.argv) < 3:
testsuite = None
else:
testsuite = sys.argv[2]
main(action, testsuite)

View File

@ -0,0 +1,8 @@
---
apiVersion: 1
providers:
- name: default
folder: k6
type: file
options:
path: /var/lib/grafana/dashboards

View File

@ -0,0 +1,11 @@
---
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
uid: prometheus
url: http://prometheus:9090
jsonData:
timeInterval: 1s

View File

@ -0,0 +1,93 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of groups, number of users per group, with parent, page size, include users
[1000, 0, false, 20, false],
[10000, 0, false, 20, false],
[1000, 0, false, 100, false],
[10000, 0, false, 100, false],
[1000, 1000, false, 100, false],
[1000, 10000, false, 100, false],
[1000, 1000, false, 100, true],
[1000, 10000, false, 100, true],
[1000, 0, true, 100, false],
[10000, 0, true, 100, false],
].map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2] ? "with_parents" : "without_parents"}_${obj[3]}_${obj[4] ? "with_users" : "without_users"}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
GROUP_COUNT: `${obj[0]}`,
USERS_PER_GROUP: `${obj[1]}`,
WITH_PARENTS: `${obj[2]}`,
PAGE_SIZE: `${obj[3]}`,
WITH_USERS: `${obj[4] ? "true" : "false"}`,
},
tags: {
testid: `group_list_${obj[0]}_${obj[1]}_${obj[2] ? "with_parents" : "without_parents"}_${obj[3]}_${obj[4] ? "with_users" : "without_users"}`,
group_count: `${obj[0]}`,
users_per_group: `${obj[1]}`,
with_parents: `${obj[2]}`,
page_size: `${obj[3]}`,
with_users: `${obj[4] ? "true" : "false"}`,
},
},
]),
),
};
export default function () {
const group_count = Number(__ENV.GROUP_COUNT);
const users_per_group = Number(__ENV.USERS_PER_GROUP);
const with_parents = __ENV.WITH_PARENTS;
const with_users = __ENV.WITH_USERS;
const domain = `group-list-${group_count}-${users_per_group}-${with_parents}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(group_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/core/groups/?page=${page}&page_size=${page_size}&include_users=${with_users}`,
params,
);
check(res, {
"status is 200": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/core/groups/?page=${page}&page_size=${page_size}&include_users=${with_users}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}

78
tests/benchmark/login.js Normal file
View File

@ -0,0 +1,78 @@
import http from "k6/http";
import { check, fail } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
scenarios: Object.fromEntries(
["no-mfa", "with-mfa"].map((obj, i) => [
obj,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
DOMAIN: `login-${obj}`,
},
tags: {
testid: `login-${obj}`,
},
},
]),
),
};
export default function () {
const domain = __ENV.DOMAIN;
const url = http.url`http://${domain}.${host}:9000/api/v3/flows/executor/default-authentication-flow/`;
const cookieJar = new http.CookieJar();
const params = {
jar: cookieJar,
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
let res = http.get(url, params);
let i = 0;
while (true) {
if (i > 10) {
fail("Test made more than 10 queries.");
break;
}
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
break;
}
const component = res.json()["component"];
let payload = {};
if (component === "ak-stage-identification") {
payload = {
uid_field: "test",
};
} else if (component === "ak-stage-password") {
payload = {
password: "verySecurePassword",
};
} else if (component === "ak-stage-authenticator-validate") {
payload = {
code: "staticToken",
};
} else if (component === "xak-flow-redirect") {
break;
} else {
console.log(`Unknown component type: ${component}`);
break;
}
payload["component"] = component;
res = http.post(url, JSON.stringify(payload), params);
i++;
}
}

View File

@ -0,0 +1,13 @@
---
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: benchmarks
prometheus: benchmarks
prometheus_replica: "0"
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]

View File

@ -0,0 +1,179 @@
import crypto from "k6/crypto";
import exec from "k6/execution";
import http from "k6/http";
import { check, fail } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
const testcases = [
[2, 50, 2],
[0, 0, 0],
[10, 0, 0],
[100, 0, 0],
[0, 10, 0],
[0, 100, 0],
[0, 0, 10],
[0, 0, 100],
[10, 10, 10],
[100, 100, 100],
];
export const options = {
setupTimeout: "10m",
scenarios: Object.fromEntries(
testcases.map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2]}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
USER_POLICIES_COUNT: `${obj[0]}`,
GROUP_POLICIES_COUNT: `${obj[1]}`,
EXPRESSION_POLICIES_COUNT: `${obj[2]}`,
},
tags: {
testid: `provider-oauth2-${obj[0]}_${obj[1]}_${obj[2]}`,
user_policies_count: `${obj[0]}`,
group_policies_count: `${obj[1]}`,
expression_policies_count: `${obj[2]}`,
},
},
]),
),
};
export function setup() {
let cookies = {};
for (let vu = 0; vu < VUs; vu++) {
cookies[vu] = {};
for (const testcase of testcases) {
const user_policies_count = testcase[0];
const group_policies_count = testcase[1];
const expression_policies_count = testcase[2];
const domain = `provider-oauth2-${user_policies_count}-${group_policies_count}-${expression_policies_count}.${host}:9000`;
const url = http.url`http://${domain}/api/v3/flows/executor/default-authentication-flow/`;
const params = {
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
http.cookieJar().clear(`http://${domain}`);
let res = http.get(url, params);
let i = 0;
while (true) {
if (i > 10) {
fail("Test made more than 10 queries.");
break;
}
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
break;
}
const component = res.json()["component"];
let payload = {};
if (component === "ak-stage-identification") {
payload = {
uid_field: "test",
};
} else if (component === "ak-stage-password") {
payload = {
password: "verySecurePassword",
};
} else if (component === "xak-flow-redirect") {
break;
} else {
fail(`Unknown component type: ${component}`);
break;
}
payload["component"] = component;
res = http.post(url, JSON.stringify(payload), params);
i++;
}
cookies[vu][domain] = http
.cookieJar()
.cookiesForURL(`http://${domain}`);
}
}
return { cookies };
}
export default function (data) {
// Restore cookies
let jar = http.cookieJar();
const vu = exec.vu.idInTest % VUs;
Object.keys(data.cookies[vu]).forEach((domain) => {
Object.keys(data.cookies[vu][domain]).forEach((key) => {
jar.set(`http://${domain}`, key, data.cookies[vu][domain][key][0]);
});
});
const user_policies_count = Number(__ENV.USER_POLICIES_COUNT);
const group_policies_count = Number(__ENV.GROUP_POLICIES_COUNT);
const expression_policies_count = Number(__ENV.EXPRESSION_POLICIES_COUNT);
const domain = `provider-oauth2-${user_policies_count}-${group_policies_count}-${expression_policies_count}.${host}:9000`;
const params = {
headers: {
"Content-Type": "application/json",
Accept: "*/*",
},
};
const random = (length = 32) => {
let chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
};
const state = random(32);
const nonce = random(32);
const code_verifier = random(64);
const code_challenge = crypto.sha256(code_verifier, "base64");
const urlParams = {
response_type: "code",
scope: "openid profile email",
client_id: "123456",
redirect_uri: "http://test.localhost",
state: state,
nonce: nonce,
code_challenge: code_challenge,
code_challenge_method: "S256",
};
let url = http.url`http://${domain}/application/o/authorize/?${Object.entries(
urlParams,
)
.map((kv) => kv.map(encodeURIComponent).join("="))
.join("&")}`;
let res = http.get(url, params);
check(res, {
"status is 200": (res) => res.status === 200,
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
return;
}
url = http.url`http://${domain}/api/v3/flows/executor/default-provider-authorization-implicit-consent/`;
res = http.get(url, params);
check(res, {
"status is 200": (res) => res.status === 200,
"last redirect is present": (res) => res.json()["type"] === "redirect",
});
if (res.status !== 200) {
fail("Endpoint did not return 200.");
return;
}
}

32
tests/benchmark/run.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
function _k6 {
local filename="${1}"
K6_PROMETHEUS_RW_SERVER_URL=${PROMETHEUS_REMOTE_WRITE_ENDPOINT:-http://localhost:9090/api/v1/write} \
K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \
K6_PROMETHEUS_RW_PUSH_INTERVAL=1s \
k6 run \
--out experimental-prometheus-rw \
--out "json=${filename%.*}.json" \
"${@}"
}
filename=""
if [ "${#}" -ge 1 ]; then
filename="${1:-}"
shift
fi
if [ -f "${filename}" ]; then
_k6 "${filename}" "${@}"
else
find "${BASE_DIR}" -name '*.js' | while read -r f; do
_k6 "${f}" "${@}"
done
fi

View File

@ -0,0 +1,68 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
vus: VUs,
duration: "150s",
tags: {
testid: `user-group-create`,
},
};
export default function () {
const domain = `user-group-create.${host}:9000`;
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
const random = (length = 32) => {
let chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let str = "";
for (let i = 0; i < length; i++) {
str += chars.charAt(Math.floor(Math.random() * chars.length));
}
return str;
};
let user_res = http.post(
http.url`http://${domain}/api/v3/core/users/`,
JSON.stringify({
username: random(16),
name: random(16),
}),
params,
);
check(user_res, {
"user status is 201": (res) => res.status === 201,
});
let group_res = http.post(
http.url`http://${domain}/api/v3/core/groups/`,
JSON.stringify({
name: random(16),
}),
params,
);
check(group_res, {
"group status is 201": (res) => res.status === 201,
});
let user_group_res = http.post(
http.url`http://${domain}/api/v3/core/groups/${group_res.json()["pk"]}/add_user/`,
JSON.stringify({
pk: user_res.json()["pk"],
}),
params,
);
check(user_group_res, {
"user group status is 204": (res) => res.status === 204,
});
}

View File

@ -0,0 +1,99 @@
import exec from "k6/execution";
import http from "k6/http";
import { check } from "k6";
const host = __ENV.BENCH_HOST ? __ENV.BENCH_HOST : "localhost";
const VUs = __ENV.VUS ? __ENV.VUS : 8;
export const options = {
discardResponseBodies: true,
scenarios: Object.fromEntries(
[
// Number of users, number of groups per user, number of parents per group, page size, with groups
[1000, 0, 0, 20, true],
[10000, 0, 0, 20, true],
[1000, 0, 0, 20, false],
[10000, 0, 0, 20, false],
[1000, 0, 0, 100, true],
[10000, 0, 0, 100, true],
[1000, 3, 0, 20, true],
[10000, 3, 0, 20, true],
[1000, 20, 0, 20, true],
[10000, 20, 0, 20, true],
[1000, 20, 3, 20, true],
[10000, 20, 3, 20, true],
[1000, 20, 0, 20, false],
[10000, 20, 0, 20, false],
[1000, 20, 3, 20, false],
[10000, 20, 3, 20, false],
].map((obj, i) => [
`${obj[0]}_${obj[1]}_${obj[2]}_${obj[3]}_${obj[4] ? "with_groups" : "without_groups"}`,
{
executor: "constant-vus",
vus: VUs,
duration: "150s",
startTime: `${165 * i}s`,
env: {
USER_COUNT: `${obj[0]}`,
GROUPS_PER_USER: `${obj[1]}`,
PARENTS_PER_GROUP: `${obj[2]}`,
PAGE_SIZE: `${obj[3]}`,
WITH_GROUPS: `${obj[4] ? "true" : "false"}`,
},
tags: {
testid: `user_list_${obj[0]}_${obj[1]}_${obj[2]}_${obj[3]}_${obj[4] ? "with_groups" : "without_groups"}`,
user_count: `${obj[0]}`,
groups_per_user: `${obj[1]}`,
parents_per_group: `${obj[2]}`,
page_size: `${obj[3]}`,
with_groups: `${obj[4] ? "true" : "false"}`,
},
},
]),
),
};
export default function () {
const user_count = Number(__ENV.USER_COUNT);
const groups_per_user = Number(__ENV.GROUPS_PER_USER);
const parents_per_group = Number(__ENV.PARENTS_PER_GROUP);
const with_groups = __ENV.WITH_GROUPS;
const domain = `user-list-${user_count}-${groups_per_user}-${parents_per_group}.${host}:9000`;
const page_size = Number(__ENV.PAGE_SIZE);
const pages = Math.round(user_count / page_size);
const params = {
headers: {
Authorization: "Bearer akadmin",
"Content-Type": "application/json",
Accept: "*/*",
},
};
if (pages <= 10) {
for (let page = 1; page <= pages; page++) {
let res = http.get(
http.url`http://${domain}/api/v3/core/users/?page=${page}&page_size=${page_size}&include_groups=${with_groups}`,
params,
);
check(res, {
"status is 100": (res) => res.status === 200,
});
}
} else {
let requests = [];
for (let page = 1; page <= pages; page++) {
requests.push([
"GET",
http.url`http://${domain}/api/v3/core/users/?page=${page}&page_size=${page_size}&include_groups=${with_groups}`,
null,
params,
]);
}
const responses = http.batch(requests);
for (let page = 1; page <= pages; page++) {
check(responses[page - 1], {
"status is 200": (res) => res.status === 200,
});
}
}
}