Skip to content
Merged
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
64 changes: 64 additions & 0 deletions clients/typescript/src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/control/locations/{location_id}/targets/{target}/jobs": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listPersonJobs"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/control/locations/{location_id}/targets/{target}/maintenance": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1134,6 +1150,17 @@ export interface components {
failed: components["schemas"]["Table"][];
failure?: string | null;
};
/** @description A job or group of the current published snapshot. Settings are left out of person reads. */
JobRecord: {
concurrency_groups: string[];
description: string;
job_key: string;
/** @description `job` or `group`; a group's selection is the union of its member jobs' selections. */
kind: components["schemas"]["RunKind"];
selection: string[];
/** Format: uuid */
snapshot_id: string;
};
/** @description A trusted operator names a published job; execution inputs are never supplied by the caller. */
JobRunRequest: {
job_key: string;
Expand Down Expand Up @@ -2885,6 +2912,43 @@ export interface operations {
};
};
};
listPersonJobs: {
parameters: {
query?: never;
header?: never;
path: {
location_id: string;
target: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["JobRecord"][];
};
};
/** @description Session rejected */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorBody"];
};
};
};
};
readPersonMaintenance: {
parameters: {
query?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ pub(crate) fn person_routes() -> Router<ControlStore> {
"/api/control/locations/{location_id}/targets/{target}/runs/{run_id}/nodes",
get(crate::api::_helpers::resources::person_reads::run_nodes),
)
.route(
"/api/control/locations/{location_id}/targets/{target}/jobs",
get(crate::api::_helpers::resources::person_reads::jobs),
)
.route(
"/api/control/locations/{location_id}/targets/{target}/triggers",
get(crate::api::_helpers::resources::person_reads::triggers),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Person reads of run nodes, triggers and ticks; refusal precedes every lookup, so unknown objects look forbidden.
//! Person reads of run nodes, jobs, triggers and ticks; refusal precedes every lookup, so unknown objects look forbidden.

use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
Expand All @@ -8,8 +8,9 @@ use uuid::Uuid;
use crate::api::errors::ApiError;
use crate::api::models::ReadQuery;
use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole};
use crate::declarations::main::list_person_jobs::list_person_jobs;
use crate::declarations::main::list_person_triggers::list_person_triggers;
use crate::declarations::models::TriggerRecord;
use crate::declarations::models::{JobRecord, TriggerRecord};
use crate::runs::main::list_person_run_nodes::list_person_run_nodes;
use crate::runs::models::RunNodeRecord;
use crate::scheduler::main::list_person_ticks::list_person_ticks;
Expand Down Expand Up @@ -39,6 +40,27 @@ pub(crate) async fn run_nodes(
.ok_or_else(|| not_found("run not found"))
}

#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/jobs", operation_id = "listPersonJobs",
params(("location_id" = Uuid, Path), ("target" = String, Path)),
responses((status = 200, body = Vec<JobRecord>), (status = 401, description = "Session rejected"),
(status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))]
pub(crate) async fn jobs(
(State(store), Extension(context)): (State<ControlStore>, Extension<AuditContext>),
Extension(default_role): Extension<DefaultRole>,
Path((location_id, target)): Path<(Uuid, String)>,
Extension(AuthenticatedPerson(principal, _)): Extension<AuthenticatedPerson>,
) -> Result<Json<Vec<JobRecord>>, ApiError> {
Ok(Json(
list_person_jobs(
&store,
(&context, principal),
(location_id, &target),
default_role,
)
.await?,
))
}

#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/triggers", operation_id = "listPersonTriggers",
params(("location_id" = Uuid, Path), ("target" = String, Path)),
responses((status = 200, body = Vec<TriggerRecord>), (status = 401, description = "Session rejected"),
Expand Down
1 change: 1 addition & 0 deletions crates/sqlbuild-orchestration/src/api/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::api::_helpers::resources::{
person_operations::change_trigger,
person_operations::recent_runs,
person_reads::run_nodes,
person_reads::jobs,
person_reads::triggers,
person_reads::ticks,
person_operations::catalog,
Expand Down
29 changes: 27 additions & 2 deletions crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Trigger reads inside a caller's transaction, so authorization and results share one view.
//! Job and trigger reads inside a caller's transaction, so authorization and results share one view.

use uuid::Uuid;

use crate::declarations::models::TriggerRecord;
use crate::declarations::models::{JobRecord, StoredJob, TriggerRecord};
use crate::store::errors::ControlError;
use crate::store::main::query_records::query_records;
use crate::store::models::{ControlTransaction, Parameter};
Expand All @@ -17,3 +17,28 @@ pub(crate) async fn triggers_in(
WHERE t.location_id=$1 AND t.target=$2 AND t.removed_in_snapshot_id IS NULL ORDER BY t.name",
vec![Parameter::Id(location_id), Parameter::Text(target.into())]).await
}

pub(crate) async fn jobs_in(
transaction: ControlTransaction,
(location_id, target): (Uuid, &str),
) -> Result<(ControlTransaction, Vec<JobRecord>), ControlError> {
let (transaction, stored): (_, Vec<StoredJob>) = query_records(
transaction,
"SELECT job_key, kind, snapshot_id, definition_json FROM sqb_jobs \
WHERE location_id=$1 AND target=$2 ORDER BY job_key",
vec![Parameter::Id(location_id), Parameter::Text(target.into())],
)
.await?;
let jobs: Vec<JobRecord> = stored
.into_iter()
.map(|job| JobRecord {
job_key: job.job_key,
kind: job.kind,
description: job.definition_json.description,
selection: job.definition_json.selection,
concurrency_groups: job.definition_json.concurrency_groups,
snapshot_id: job.snapshot_id,
})
.collect();
Ok((transaction, jobs))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! A person lists jobs only with view on every current node of the target, like triggers.

use uuid::Uuid;

use crate::authorization::main::authorize_listing::authorize_listing;
use crate::credentials::models::{AuditContext, DefaultRole};
use crate::declarations::_helpers::reads::jobs_in;
use crate::declarations::models::JobRecord;
use crate::store::errors::ControlError;
use crate::store::main::{begin::begin, commit::commit};
use crate::store::models::{ControlStore, TransactionMode};

pub async fn list_person_jobs(
store: &ControlStore,
(context, credentials): (&AuditContext, Uuid),
(location_id, target): (Uuid, &str),
default_role: DefaultRole,
) -> Result<Vec<JobRecord>, ControlError> {
let (transaction, _) = authorize_listing(
begin(store, TransactionMode::Write).await?,
(context, credentials),
(location_id, target),
default_role,
)
.await?;
let (transaction, jobs) = jobs_in(transaction, (location_id, target)).await?;
commit(transaction).await?;
Ok(jobs)
}
1 change: 1 addition & 0 deletions crates/sqlbuild-orchestration/src/declarations/main/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod change_person_trigger;
pub mod change_trigger;
pub(crate) mod frozen_definitions;
pub mod group_dependencies;
pub mod list_person_jobs;
pub mod list_person_triggers;
pub mod list_trigger_events;
pub mod list_triggers;
Expand Down
21 changes: 21 additions & 0 deletions crates/sqlbuild-orchestration/src/declarations/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,27 @@ pub enum TriggerDeclaration {
Sensor(SensorDefinition),
}

/// A job or group of the current published snapshot. Settings are left out of person reads.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct JobRecord {
pub job_key: String,
/// `job` or `group`; a group's selection is the union of its member jobs' selections.
pub kind: crate::runs::models::RunKind,
pub description: String,
pub selection: BTreeSet<String>,
pub concurrency_groups: BTreeSet<String>,
pub snapshot_id: uuid::Uuid,
}

/// A `sqb_jobs` row as stored.
#[derive(Clone, Debug, Deserialize)]
pub(crate) struct StoredJob {
pub job_key: String,
pub kind: crate::runs::models::RunKind,
pub snapshot_id: uuid::Uuid,
pub definition_json: JobDefinition,
}

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
pub struct TriggerRecord {
pub trigger_id: uuid::Uuid,
Expand Down
3 changes: 2 additions & 1 deletion crates/sqlbuild-orchestration/tests/api/test_denials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use crate::store::tests::store_helpers::{close_test_store, open_test_store, run}
use crate::store::tests::store_test_types::Backend;

/// Location-scoped reads and the action each needs.
const READS: [(&str, &str); 10] = [
const READS: [(&str, &str); 11] = [
("runs", "view"),
("jobs", "view"),
("triggers", "view"),
("changes", "view"),
("catalog?schema=orders", "view"),
Expand Down
33 changes: 33 additions & 0 deletions crates/sqlbuild-orchestration/tests/api/test_person_sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
let mut input = snapshot(location);
input.documents.get_mut("triggers").expect("triggers")["health_orders"]["when"] =
json!({"kind":"custom", "function":"orders_ready", "code_hash":"orders_v1"});
input.documents.get_mut("jobs").expect("jobs")["orders"]["concurrency_groups"] =
json!(["warehouse_heavy"]);
input.documents.insert(
"groups".into(),
json!({"nightly": {"description": "Nightly orders", "selection": ["orders"], "jobs": {"orders": []}}}),
);
let sql_input: Value = serde_json::from_str(
&std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
Expand Down Expand Up @@ -279,6 +285,7 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
assert!(!page.to_string().contains("select 1 as id"));
let known_reads = [
format!("{metadata}/{}/nodes", seed_run.run_id),
format!("/api/control/locations/{location}/targets/development/jobs"),
format!("/api/control/locations/{location}/targets/development/triggers"),
format!(
"/api/control/locations/{location}/targets/development/triggers/{}/ticks",
Expand Down Expand Up @@ -312,6 +319,32 @@ fn given_browser_session_roles_when_work_is_requested_then_scope_revocation_and_
"{read_path}"
);
}
let jobs: Value = request(
&provider.client,
session,
(
Method::GET,
&format!("/api/control/locations/{location}/targets/development/jobs"),
),
(&Value::Null, &session.csrf),
)
.await
.json()
.await
.expect("jobs response");
let listed_jobs: Vec<Value> =
jobs.as_array().into_iter().flatten().cloned().collect();
let expected_jobs = [
json!({"job_key": "nightly", "kind": "group", "description": "Nightly orders",
"selection": ["orders"], "concurrency_groups": [], "snapshot_id": published.snapshot_id}),
json!({"job_key": "orders", "kind": "job", "description": "Refresh orders",
"selection": ["orders"], "concurrency_groups": ["warehouse_heavy"], "snapshot_id": published.snapshot_id}),
];
assert_eq!(
listed_jobs,
expected_jobs[..expected_jobs.len() * usize::from(jobs.is_array())],
"jobs list declared metadata and never settings"
);
for read_path in &unknown_reads {
let missing = request(
&provider.client,
Expand Down
2 changes: 1 addition & 1 deletion fensu.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,7 @@ paths = ["crates/sqlbuild-orchestration/src/api/_helpers/resources/administrator
reason = "Each function is one utoipa-annotated axum route: OpenAPI needs one annotated handler per operation, and each body only checks the caller's scope and calls one store operation. Scope checks and store work live in their shared owners."

[[dupes.allowlist]]
paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"]
paths = ["crates/sqlbuild-orchestration/src/authorization/main/read_security_audit.rs", "crates/sqlbuild-orchestration/src/credentials/_helpers/active_session.rs", "crates/sqlbuild-orchestration/src/declarations/_helpers/reads.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_jobs.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_person_triggers.rs", "crates/sqlbuild-orchestration/src/declarations/main/list_triggers.rs", "crates/sqlbuild-orchestration/src/platform/main/read_location_work.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_runs.rs", "crates/sqlbuild-orchestration/src/runs/main/list_person_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_run_nodes.rs", "crates/sqlbuild-orchestration/src/runs/main/list_runs.rs", "crates/sqlbuild-orchestration/src/runs/_helpers/lifecycle/reads.rs", "crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_person_ticks.rs", "crates/sqlbuild-orchestration/src/store/main/read_records.rs", "crates/sqlbuild-orchestration/src/platform/main/location_work.rs", "crates/sqlbuild-orchestration/src/scheduler/main/list_ticks.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/attempts.rs", "crates/sqlbuild-orchestration/src/sensors/_helpers/facts.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_current_record.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_documents.rs", "crates/sqlbuild-orchestration/src/snapshots/main/read_snapshot_nodes.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_catalog.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_check.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_checks.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_maintenance.rs", "crates/sqlbuild-orchestration/src/warehouse_history/main/read_person_microbatch_coverage.rs", "crates/sqlbuild-orchestration/src/warehouse_metadata/main/read_person_statistics.rs"]
reason = "Each is one control-store read with its own SQL and record type. Transactions, person authorization and decoding go through the shared begin_authorized, query_records, read_records and query_bodies helpers; what remains alike is the call shape."

[[dupes.allowlist]]
Expand Down
2 changes: 2 additions & 0 deletions ui/src/lib/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export interface ApiClient extends PlatformApi {
listJobs(scope: Scope): Promise<Job[]>;
getJob(scope: Scope, key: string): Promise<JobDetail | null>;
launchJob(scope: Scope, key: string, options?: JobLaunchOptions): Promise<Run>;
/** Launches with `--full-refresh`, rebuilding the job's tables from scratch. */
launchJobFromScratch(scope: Scope, key: string, options?: JobLaunchOptions): Promise<Run>;

listTriggers(scope: Scope): Promise<Trigger[]>;
getTrigger(scope: Scope, id: string): Promise<TriggerDetail | null>;
Expand Down
Loading
Loading