Compare commits
39 Commits
core/impro
...
benchmarks
Author | SHA1 | Date | |
---|---|---|---|
cde4e395e9 | |||
d19c692f81 | |||
d5d2be5672 | |||
8597db59f5 | |||
74fb9492bc | |||
defbafb55e | |||
e2ed7391bc | |||
8dcd0dcaa9 | |||
18eee1b722 | |||
d0f6c815c3 | |||
b13eba3b0a | |||
77fe4e9fe2 | |||
71fe8b4fb3 | |||
b14cb832b2 | |||
24b5296d88 | |||
41b7e50bc6 | |||
6b750d7c59 | |||
d268c28934 | |||
688404b6a5 | |||
cbd2425a5f | |||
877c264d59 | |||
2575b540fa | |||
0e0b76a62e | |||
6d625fd1d7 | |||
bd0630e300 | |||
ffb7d44024 | |||
7589b11f98 | |||
ad21dfa2bc | |||
95692f5a7c | |||
1f4ed1defa | |||
334b183465 | |||
1f789dd4c5 | |||
057e5747c9 | |||
8717a3aaab | |||
527173236a | |||
3e6eb6f248 | |||
6babf0f1c4 | |||
ca7cc30504 | |||
a7cb808cad |
7
.gitignore
vendored
7
.gitignore
vendored
@ -209,3 +209,10 @@ source_docs/
|
||||
|
||||
### Golang ###
|
||||
/vendor/
|
||||
|
||||
### Benchmark ###
|
||||
tests/benchmark/k6
|
||||
tests/benchmark/prometheus
|
||||
tests/benchmark/**/*.json
|
||||
tests/benchmark/**/*.ndjson
|
||||
tests/benchmark/**/*.html
|
||||
|
17
Makefile
17
Makefile
@ -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
|
||||
|
@ -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:
|
||||
|
0
tests/benchmark/__init__.py
Normal file
0
tests/benchmark/__init__.py
Normal file
30
tests/benchmark/docker-compose.yml
Normal file
30
tests/benchmark/docker-compose.yml
Normal 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
|
79
tests/benchmark/event_list.js
Normal file
79
tests/benchmark/event_list.js
Normal 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
345
tests/benchmark/fixtures.py
Executable 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)
|
@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: 1
|
||||
providers:
|
||||
- name: default
|
||||
folder: k6
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
@ -0,0 +1,11 @@
|
||||
---
|
||||
apiVersion: 1
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
orgId: 1
|
||||
uid: prometheus
|
||||
url: http://prometheus:9090
|
||||
jsonData:
|
||||
timeInterval: 1s
|
93
tests/benchmark/group_list.js
Normal file
93
tests/benchmark/group_list.js
Normal 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
78
tests/benchmark/login.js
Normal 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++;
|
||||
}
|
||||
}
|
13
tests/benchmark/prometheus.yml
Normal file
13
tests/benchmark/prometheus.yml
Normal 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"]
|
179
tests/benchmark/provider_oauth2.js
Normal file
179
tests/benchmark/provider_oauth2.js
Normal 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
32
tests/benchmark/run.sh
Executable 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
|
68
tests/benchmark/user_group_create.js
Normal file
68
tests/benchmark/user_group_create.js
Normal 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,
|
||||
});
|
||||
}
|
99
tests/benchmark/user_list.js
Normal file
99
tests/benchmark/user_list.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user