schedule form and helpers

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
Marc 'risson' Schmitt
2025-04-03 16:51:24 +02:00
parent c92cbd7e22
commit c62f73400a
9 changed files with 223 additions and 7 deletions

View File

@ -1,15 +1,21 @@
from dramatiq.actor import Actor
from dramatiq.broker import get_broker
from dramatiq.errors import ActorNotFound
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.mixins import (
ListModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.utils import ModelSerializer
from authentik.rbac.decorators import permission_required
from authentik.tasks.schedules.models import Schedule
@ -23,6 +29,7 @@ class ScheduleSerializer(ModelSerializer):
"uid",
"actor_name",
"crontab",
"paused",
"next_run",
"description",
]
@ -52,3 +59,12 @@ class ScheduleViewSet(
"uid",
)
ordering = ("next_run", "uid")
@permission_required("authentik_tasks_schedules.send_schedule")
@extend_schema(request=OpenApiTypes.NONE)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
def send(self, request: Request, pk=None) -> Response:
"""Trigger this schedule now"""
schedule: Schedule = self.get_object()
schedule.send()
return Response({})

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.13 on 2025-04-03 14:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_tasks_schedules", "0003_schedule_options"),
]
operations = [
migrations.AlterModelOptions(
name="schedule",
options={
"default_permissions": ("change", "view"),
"permissions": [("send_schedule", "Manually trigger a schedule")],
"verbose_name": "Schedule",
"verbose_name_plural": "Schedules",
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.13 on 2025-04-03 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_tasks_schedules", "0004_alter_schedule_options"),
]
operations = [
migrations.AddField(
model_name="schedule",
name="paused",
field=models.BooleanField(default=False, help_text="Pause this schedule"),
),
]

View File

@ -40,6 +40,7 @@ class Schedule(SerializerModel):
rel_obj = GenericForeignKey("rel_obj_content_type", "rel_obj_id")
crontab = models.TextField(validators=[validate_crontab], help_text=_("When to schedule tasks"))
paused = models.BooleanField(default=False, help_text=_("Pause this schedule"))
next_run = models.DateTimeField(auto_now_add=True, editable=False)
@ -50,6 +51,9 @@ class Schedule(SerializerModel):
"change",
"view",
)
permissions = [
("send_schedule", _("Manually trigger a schedule")),
]
indexes = (models.Index(fields=("rel_obj_content_type", "rel_obj_id")),)
def __str__(self):

View File

@ -40,7 +40,9 @@ class Scheduler:
tenant=tenant.schema_name,
)
with transaction.atomic(using=router.db_for_write(Schedule)):
for schedule in Schedule.objects.select_for_update().filter(next_run__lt=now()):
for schedule in Schedule.objects.select_for_update().filter(
next_run__lt=now(), paused=False
):
self.process_schedule(schedule)
def run(self):

View File

@ -7754,6 +7754,7 @@
"authentik_stages_user_write.view_userwritestage",
"authentik_tasks.view_task",
"authentik_tasks_schedules.change_schedule",
"authentik_tasks_schedules.send_schedule",
"authentik_tasks_schedules.view_schedule",
"authentik_tenants.add_domain",
"authentik_tenants.add_tenant",
@ -14268,6 +14269,7 @@
"authentik_stages_user_write.view_userwritestage",
"authentik_tasks.view_task",
"authentik_tasks_schedules.change_schedule",
"authentik_tasks_schedules.send_schedule",
"authentik_tasks_schedules.view_schedule",
"authentik_tenants.add_domain",
"authentik_tenants.add_tenant",
@ -15218,6 +15220,11 @@
"minLength": 1,
"title": "Crontab",
"description": "When to schedule tasks"
},
"paused": {
"type": "boolean",
"title": "Paused",
"description": "Pause this schedule"
}
},
"required": []
@ -15233,6 +15240,7 @@
"permission": {
"type": "string",
"enum": [
"send_schedule",
"change_schedule",
"view_schedule"
]

View File

@ -39096,6 +39096,41 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/tasks/schedules/{id}/send/:
post:
operationId: tasks_schedules_send_create
description: Trigger this schedule now
parameters:
- in: path
name: id
schema:
type: string
format: uuid
description: A UUID string identifying this Schedule.
required: true
tags:
- tasks
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Schedule'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/tasks/tasks/:
get:
operationId: tasks_tasks_list
@ -52931,6 +52966,9 @@ components:
type: string
minLength: 1
description: When to schedule tasks
paused:
type: boolean
description: Pause this schedule
PatchedScopeMappingRequest:
type: object
description: ScopeMapping Serializer
@ -56807,6 +56845,9 @@ components:
crontab:
type: string
description: When to schedule tasks
paused:
type: boolean
description: Pause this schedule
next_run:
type: string
format: date-time
@ -56829,6 +56870,9 @@ components:
type: string
minLength: 1
description: When to schedule tasks
paused:
type: boolean
description: Pause this schedule
required:
- crontab
ScopeMapping:

View File

@ -0,0 +1,66 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/ModalForm";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/ProxyForm";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Schedule, TasksApi } from "@goauthentik/api";
@customElement("ak-schedule-form")
export class ScheduleForm extends ModelForm<Schedule, string> {
async loadInstance(pk: string): Promise<Schedule> {
return await new TasksApi(DEFAULT_CONFIG).tasksSchedulesRetrieve({
id: pk,
});
}
getSuccessMessage(): string {
if (!this.instance) {
return "";
}
return msg("Successfully updated schedule.");
}
async send(data: Schedule): Promise<Schedule | void> {
if (!this.instance) {
return;
}
return await new TasksApi(DEFAULT_CONFIG).tasksSchedulesUpdate({
id: this.instance.id,
scheduleRequest: data,
});
}
renderForm(): TemplateResult {
return html`<form class="pf-c-form pf-m-horizontal">
<ak-text-input
name="crontab"
value=${ifDefined(this.instance?.crontab)}
label=${msg("Crontab")}
required
help=${msg("Crontab")}
></ak-text-input>
<ak-switch-input
name="paused"
label=${msg("Paused")}
?checked=${this.instance?.paused ?? false}
help=${msg("Pause this schedule")}
>
</ak-switch-input>
</form>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-schedule-form": ScheduleForm;
}
}

View File

@ -1,9 +1,9 @@
import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/providers/rac/EndpointForm";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/system-tasks/ScheduleForm";
import "@goauthentik/admin/system-tasks/TaskList";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
@ -55,9 +55,45 @@ export class ScheduleList extends Table<Schedule> {
html`<div>${item.description}</div>
<small>${item.uid.replace(new RegExp("^authentik."), "")}</small>`,
html`${item.crontab}`,
html`<div>${getRelativeTime(item.nextRun)}</div>
<small>${item.nextRun.toLocaleString()}</small>`,
html``,
html`
${item.paused
? html`Paused`
: html`
<div>${getRelativeTime(item.nextRun)}</div>
<small>${item.nextRun.toLocaleString()}</small>
`}
`,
html`<ak-action-button
class="pf-m-plain"
.apiRequest=${() => {
return new TasksApi(DEFAULT_CONFIG)
.tasksSchedulesSendCreate({
id: item.id,
})
.then(() => {
this.dispatchEvent(
new CustomEvent(EVENT_REFRESH, {
bubbles: true,
composed: true,
}),
);
});
}}
>
</ak-action-button>
<pf-tooltip position="top" content=${msg("Run scheduled task now")}>
<i class="fas fa-play" aria-hidden="true"></i>
</pf-tooltip>
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Schedule")} </span>
<ak-schedule-form slot="form" .instancePk=${item.id}> </ak-schedule-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal> `,
];
}