Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions vicky/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,58 @@ It will return `null`, if successful.
}
}
```

## Task Templates

### List All Task Templates

`GET /api/v1/task-templates` returns all task templates.

### Create A Task Template

`POST /api/v1/task-templates` creates a template with variables.

```json
{
"name": "deploy-service",
"display_name_template": "Deploy {{service}} to {{environment}}",
"flake_ref": {
"flake": "https://something.cat/deployments/{{service}}/flake.nix",
"args": ["--env={{environment}}"]
},
"locks": [
{
"name": "deploy/{{service}}",
"type": "WRITE"
}
],
"features": ["nix"],
"group": "{{environment}}",
"variables": [
{
"name": "service",
"default_value": null,
"description": "Some service name"
},
{
"name": "environment",
"default_value": "production",
"description": "Deployment environment"
}
]
}
```

### Instantiate A Task Template

`POST /api/v1/task-templates/<UUID>/instantiate` renders a task and adds it to the scheduler.

```json
{
"needs_confirmation": false,
"variables": {
"service": "vicky",
"environment": "staging"
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS task_template_variables;
DROP TABLE IF EXISTS task_template_locks;
DROP TABLE IF EXISTS task_templates;
41 changes: 41 additions & 0 deletions vicky/migrations/2026-02-11-120001-0000_task_templates/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CREATE TABLE task_templates
(
id UUID PRIMARY KEY,
name VARCHAR NOT NULL UNIQUE,
display_name_template VARCHAR NOT NULL,
flake_ref_uri_template VARCHAR NOT NULL,
flake_ref_args_template TEXT[] NOT NULL,
features TEXT[] NOT NULL,
"group" VARCHAR,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

CREATE TABLE task_template_locks
(
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
task_template_id UUID NOT NULL,
name_template VARCHAR NOT NULL,
type "LockKind_Type" NOT NULL,
CONSTRAINT fk_task_template
FOREIGN KEY (task_template_id)
REFERENCES task_templates (id)
ON DELETE CASCADE
);

CREATE TABLE task_template_variables
(
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
task_template_id UUID NOT NULL,
name VARCHAR NOT NULL,
default_value VARCHAR,
description VARCHAR,
CONSTRAINT fk_task_template_var
FOREIGN KEY (task_template_id)
REFERENCES task_templates (id)
ON DELETE CASCADE,
CONSTRAINT task_template_variable_unique
UNIQUE (task_template_id, name)
);

CREATE INDEX task_template_locks_template_id_idx ON task_template_locks (task_template_id);
CREATE INDEX task_template_variables_template_id_idx ON task_template_variables (task_template_id);
19 changes: 16 additions & 3 deletions vicky/src/bin/vicky/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ use crate::locks::{
locks_get_active, locks_get_detailed_poisoned, locks_get_poisoned, locks_unlock,
};
use crate::startup::Result;
use crate::task_templates::{
task_templates_add, task_templates_get, task_templates_get_specific, task_templates_instantiate,
};
use crate::tasks::{
tasks_add, tasks_claim, tasks_confirm, tasks_count, tasks_download_logs, tasks_finish,
tasks_get, tasks_get_logs, tasks_get_specific, tasks_heartbeat, tasks_put_logs, tasks_cancel
tasks_add, tasks_cancel, tasks_claim, tasks_confirm, tasks_count, tasks_download_logs,
tasks_finish, tasks_get, tasks_get_logs, tasks_get_specific, tasks_heartbeat, tasks_put_logs,
};
use crate::user::get_user;
use crate::webconfig::get_web_config;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use errors::AppError;
use jwtk::jwk::RemoteJwksVerifier;
use log::{LevelFilter, error, trace, warn, info};
use log::{LevelFilter, error, info, trace, warn};
use rocket::fairing::AdHoc;
use rocket::{Build, Ignite, Rocket, routes};
use snafu::ResultExt;
Expand All @@ -32,6 +35,7 @@ mod errors;
mod events;
mod locks;
mod startup;
mod task_templates;
mod tasks;
mod user;
mod webconfig;
Expand Down Expand Up @@ -202,6 +206,15 @@ async fn build_web_api(
tasks_cancel
],
)
.mount(
"/api/v1/task-templates",
routes![
task_templates_get,
task_templates_get_specific,
task_templates_add,
task_templates_instantiate
],
)
.mount(
"/api/v1/locks",
routes![
Expand Down
120 changes: 120 additions & 0 deletions vicky/src/bin/vicky/task_templates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use crate::auth::AnyAuthGuard;
use crate::errors::AppError;
use crate::events::GlobalEvent;
use diesel::result::DatabaseErrorKind;
use diesel::result::Error::DatabaseError;
use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::{State, get, post};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::broadcast;
use uuid::Uuid;
use vickylib::database::entities::task::FlakeRef;
use vickylib::database::entities::task_template::{
TaskTemplateError, TaskTemplateLock, TaskTemplateVariable,
};
use vickylib::database::entities::{Database, Task, TaskTemplate};
use vickylib::errors::VickyError;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoTaskTemplateNew {
name: String,
display_name_template: String,
flake_ref: FlakeRef,
#[serde(default)]
locks: Vec<TaskTemplateLock>,
#[serde(default)]
features: Vec<String>,
group: Option<String>,
#[serde(default)]
variables: Vec<TaskTemplateVariable>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RoTaskTemplateInstantiate {
#[serde(default)]
needs_confirmation: bool,
#[serde(default)]
variables: HashMap<String, String>,
}

fn template_error_fail(err: TaskTemplateError) -> AppError {
log::warn!("invalid task template: {err}");
AppError::HttpError(Status::BadRequest)
}

#[get("/")]
pub async fn task_templates_get(
db: Database,
_auth: AnyAuthGuard,
) -> Result<Json<Vec<TaskTemplate>>, AppError> {
let templates = db.get_all_task_templates().await?;
Ok(Json(templates))
}

#[get("/<id>")]
pub async fn task_templates_get_specific(
id: Uuid,
db: Database,
_auth: AnyAuthGuard,
) -> Result<Json<Option<TaskTemplate>>, AppError> {
let template = db.get_task_template(id).await?;
Ok(Json(template))
}

#[post("/", format = "json", data = "<task_template>")]
pub async fn task_templates_add(
task_template: Json<RoTaskTemplateNew>,
db: Database,
_auth: AnyAuthGuard,
) -> Result<Json<TaskTemplate>, AppError> {
let task_template = task_template.into_inner();

let task_template = TaskTemplate {
id: Uuid::new_v4(),
name: task_template.name,
display_name_template: task_template.display_name_template,
flake_ref: task_template.flake_ref,
locks: task_template.locks,
features: task_template.features,
group: task_template.group,
variables: task_template.variables,
created_at: chrono::Utc::now(),
};

task_template.validate().map_err(template_error_fail)?;

match db.put_task_template(task_template.clone()).await {
Ok(_) => Ok(Json(task_template)),
Err(VickyError::Diesel {
source: DatabaseError(DatabaseErrorKind::UniqueViolation, _),
}) => Err(AppError::HttpError(Status::Conflict)),
Err(err) => Err(err.into()),
}
}

#[post("/<id>/instantiate", format = "json", data = "<request>")]
pub async fn task_templates_instantiate(
id: Uuid,
request: Json<RoTaskTemplateInstantiate>,
db: Database,
global_events: &State<broadcast::Sender<GlobalEvent>>,
_auth: AnyAuthGuard,
) -> Result<Json<Task>, AppError> {
let template = db
.get_task_template(id)
.await?
.ok_or(AppError::HttpError(Status::NotFound))?;

let request = request.into_inner();

let task = template
.instantiate(request.variables, request.needs_confirmation)
.map_err(template_error_fail)?;

db.put_task(task.clone()).await?;
global_events.send(GlobalEvent::TaskAdd)?;

Ok(Json(task))
}
12 changes: 12 additions & 0 deletions vicky/src/lib/database/entities/mod.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
pub mod lock;
pub mod task;
pub mod task_template;
pub mod user;

use crate::database::entities::lock::PoisonedLock;
use crate::database::entities::lock::db_impl::LockDatabase;
use crate::database::entities::task::TaskStatus;
use crate::database::entities::task::db_impl::TaskDatabase;
use crate::database::entities::task_template::db_impl::TaskTemplateDatabase;
use crate::database::entities::user::User;
use crate::database::entities::user::db_impl::UserDatabase;
use crate::errors::VickyError;
Expand All @@ -15,6 +17,7 @@ use delegate::delegate;
pub use lock::{Lock, LockKind};
use rocket_sync_db_pools::{ConnectionPool, database};
pub use task::Task;
pub use task_template::TaskTemplate;
use uuid::Uuid;

#[database("postgres_db")]
Expand Down Expand Up @@ -53,6 +56,15 @@ impl Database {
pub async fn timeout_task(&self, task_id: Uuid) -> Result<usize, VickyError>;
}

#[await(false)]
#[expr(self.run(move |conn| $).await)]
#[through(TaskTemplateDatabase)]
to conn {
pub async fn get_all_task_templates(&self) -> Result<Vec<TaskTemplate>, VickyError>;
pub async fn get_task_template(&self, task_template_id: Uuid) -> Result<Option<TaskTemplate>, VickyError>;
pub async fn put_task_template(&self, task_template: TaskTemplate) -> Result<usize, VickyError>;
}

#[await(false)]
#[expr(self.run(move |conn| $).await)]
#[through(LockDatabase)]
Expand Down
Loading
Loading