mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 16:03:47 +00:00
add resume urls at the api level
This commit is contained in:
Generated
+7
@@ -3829,6 +3829,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.2.1"
|
||||
@@ -4120,6 +4126,7 @@ dependencies = [
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"urlencoding",
|
||||
"windmill-audit",
|
||||
"windmill-common",
|
||||
"windmill-parser",
|
||||
|
||||
@@ -64,4 +64,5 @@ tokio-util.workspace = true
|
||||
tokio-tar.workspace = true
|
||||
hmac.workspace = true
|
||||
cookie.workspace = true
|
||||
sha2.workspace = true
|
||||
sha2.workspace = true
|
||||
urlencoding.workspace = true
|
||||
|
||||
@@ -2887,6 +2887,43 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/w/{workspace}/jobs/resume_urls/{id}/{resume_id}:
|
||||
get:
|
||||
summary: get resume urls given a job_id, resume_id and a nonce to resume a flow
|
||||
operationId: getResumeUrls
|
||||
tags:
|
||||
- job
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/WorkspaceId"
|
||||
- $ref: "#/components/parameters/JobId"
|
||||
- name: resume_id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- name: approver
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: url endpoints
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
approvalPage:
|
||||
type: string
|
||||
resume:
|
||||
type: string
|
||||
cancel:
|
||||
type: string
|
||||
required:
|
||||
- approvalPage
|
||||
- resume
|
||||
- cancel
|
||||
|
||||
/w/{workspace}/jobs/resume/{id}/{resume_id}/{signature}:
|
||||
get:
|
||||
summary: resume a job for a suspended flow
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* LICENSE-AGPL for a copy of the license.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{FromRequest, Path, Query},
|
||||
@@ -18,6 +20,7 @@ use hyper::StatusCode;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use sql_builder::{prelude::*, quote, SqlBuilder};
|
||||
use sqlx::{query_scalar, types::Uuid, Postgres, Transaction};
|
||||
use urlencoding::encode;
|
||||
use windmill_audit::{audit_log, ActionKind};
|
||||
use windmill_common::{
|
||||
error::{self, to_anyhow, Error},
|
||||
@@ -34,6 +37,7 @@ use crate::{
|
||||
db::{UserDB, DB},
|
||||
users::Authed,
|
||||
variables::get_workspace_key,
|
||||
BaseUrl,
|
||||
};
|
||||
|
||||
pub fn workspaced_service() -> Router {
|
||||
@@ -64,6 +68,7 @@ pub fn workspaced_service() -> Router {
|
||||
"/job_signature/:job_id/:resume_id",
|
||||
get(create_job_signature),
|
||||
)
|
||||
.route("/resume_urls/:job_id/:resume_id", get(get_resume_urls))
|
||||
.route("/result_by_id/:job_id/:node_id", get(get_result_by_id))
|
||||
}
|
||||
|
||||
@@ -662,16 +667,74 @@ pub async fn create_job_signature(
|
||||
Query(approver): Query<QueryApprover>,
|
||||
) -> error::Result<String> {
|
||||
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
|
||||
create_signature(key, job_id, resume_id, approver.approver)
|
||||
}
|
||||
|
||||
fn create_signature(
|
||||
key: String,
|
||||
job_id: Uuid,
|
||||
resume_id: u32,
|
||||
approver: Option<String>,
|
||||
) -> Result<String, Error> {
|
||||
let mut mac = HmacSha256::new_from_slice(key.as_bytes()).map_err(to_anyhow)?;
|
||||
mac.update(job_id.as_bytes());
|
||||
mac.update(resume_id.to_be_bytes().as_ref());
|
||||
tracing::info!("approver: {:?}", approver.approver);
|
||||
if let Some(approver) = approver.approver {
|
||||
if let Some(approver) = approver {
|
||||
mac.update(approver.as_bytes());
|
||||
}
|
||||
Ok(hex::encode(mac.finalize().into_bytes()))
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Serialize)]
|
||||
pub struct ResumeUrls {
|
||||
approvalPage: String,
|
||||
cancel: String,
|
||||
resume: String,
|
||||
}
|
||||
|
||||
fn build_resume_url(
|
||||
op: &str,
|
||||
w_id: &str,
|
||||
job_id: &Uuid,
|
||||
resume_id: &u32,
|
||||
signature: &str,
|
||||
approver: &str,
|
||||
base_url: &str,
|
||||
) -> String {
|
||||
format!("{base_url}/api/w/{w_id}/jobs/{op}/{job_id}/{resume_id}/{signature}{approver}")
|
||||
}
|
||||
|
||||
pub async fn get_resume_urls(
|
||||
authed: Authed,
|
||||
Extension(user_db): Extension<UserDB>,
|
||||
Path((w_id, job_id, resume_id)): Path<(String, Uuid, u32)>,
|
||||
Query(approver): Query<QueryApprover>,
|
||||
Extension(base_url): Extension<Arc<BaseUrl>>,
|
||||
) -> error::JsonResult<ResumeUrls> {
|
||||
let key = get_workspace_key(&w_id, &mut user_db.begin(&authed).await?).await?;
|
||||
let signature = create_signature(key, job_id, resume_id, approver.approver.clone())?;
|
||||
let base_url = base_url.0.clone();
|
||||
let approver = approver
|
||||
.approver
|
||||
.as_ref()
|
||||
.map(|x| format!("?approver={}", encode(x)))
|
||||
.unwrap_or_else(String::new);
|
||||
let res = ResumeUrls {
|
||||
approvalPage: format!(
|
||||
"{base_url}/approve/{w_id}/{job_id}/{resume_id}/{signature}{approver}"
|
||||
),
|
||||
cancel: build_resume_url(
|
||||
"cancel", &w_id, &job_id, &resume_id, &signature, &approver, &base_url,
|
||||
),
|
||||
resume: build_resume_url(
|
||||
"resume", &w_id, &job_id, &resume_id, &signature, &approver, &base_url,
|
||||
),
|
||||
};
|
||||
|
||||
Ok(Json(res))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Job {
|
||||
|
||||
@@ -46,7 +46,7 @@ mod workspaces;
|
||||
|
||||
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
|
||||
|
||||
struct BaseUrl(String);
|
||||
pub struct BaseUrl(String);
|
||||
pub struct IsSecure(bool);
|
||||
pub struct CookieDomain(Option<String>);
|
||||
pub struct CloudHosted(bool);
|
||||
|
||||
+15
-72
@@ -1,4 +1,4 @@
|
||||
import { ResourceService, VariableService } from './windmill-api/index.ts'
|
||||
import { JobService, ResourceService, VariableService } from './windmill-api/index.ts'
|
||||
import { OpenAPI } from './windmill-api/index.ts'
|
||||
|
||||
export {
|
||||
@@ -168,87 +168,30 @@ export async function databaseUrlFromResource(path: string): Promise<string> {
|
||||
return `postgresql://${resource.user}:${resource.password}@${resource.host}:${resource.port}/${resource.dbname}?sslmode=${resource.sslmode}`
|
||||
}
|
||||
|
||||
|
||||
export interface NonceAndHmac {
|
||||
nonce: number;
|
||||
signature: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HMAC and nonce needed for approval script
|
||||
* @param workspace workspace name
|
||||
* @param jobId
|
||||
* Get URLs needed for resuming a flow after this step
|
||||
* @param approver approver name
|
||||
* @returns HMAC and nonce needed to authorize approval script actions
|
||||
* @returns approval page UI URL, resume and cancel API URLs for resumeing the flow
|
||||
*/
|
||||
export async function genNounceAndHmac(workspace: string, jobId: string, approver?: string): Promise<NonceAndHmac> {
|
||||
const nonce = Math.floor(Math.random() * 4294967295);
|
||||
const u = new URL(
|
||||
`/api/w/${workspace}/jobs/job_signature/${jobId}/${nonce}`,
|
||||
Deno.env.get("WM_BASE_URL"),
|
||||
);
|
||||
|
||||
u.searchParams.append('token', Deno.env.get("WM_TOKEN") ?? '');
|
||||
if (approver) {
|
||||
u.searchParams.append('approver', approver);
|
||||
}
|
||||
|
||||
const sig = await fetch(u.toString());
|
||||
return {
|
||||
nonce,
|
||||
signature: await sig.text()
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResumeEndpoints {
|
||||
export async function getResumeUrls(approver?: string): Promise<{
|
||||
approvalPage: string;
|
||||
resume: string;
|
||||
cancel: string;
|
||||
}> {
|
||||
const nonce = Math.floor(Math.random() * 4294967295);
|
||||
const workspace = getWorkspace()
|
||||
return await JobService.getResumeUrls({ workspace, resumeId: nonce, approver, id: Deno.env.get("WM_JOB_ID") ?? 'NO_JOB_ID' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URLs needed for approval script
|
||||
* @param approver approver name
|
||||
* @returns approval page UI URL, resume and cancel API URLs for approval script
|
||||
* @deprecated use getResumeUrls instead
|
||||
*/
|
||||
export async function getResumeEndpoints(approver?: string): Promise<ResumeEndpoints> {
|
||||
const workspace = getWorkspace()
|
||||
|
||||
const { nonce, signature } = await genNounceAndHmac(
|
||||
workspace,
|
||||
Deno.env.get("WM_JOB_ID") ?? "no_job_id",
|
||||
approver
|
||||
);
|
||||
|
||||
function getResumeUrl(op: string): string {
|
||||
const u = new URL(
|
||||
`${op}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`,
|
||||
Deno.env.get("WM_BASE_URL") + `/api/w/${workspace}/jobs/`,
|
||||
);
|
||||
if (approver) {
|
||||
u.searchParams.append('approver', approver);
|
||||
}
|
||||
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
function getApprovalPage() {
|
||||
const u = new URL(
|
||||
`/approve/${workspace}/${Deno.env.get("WM_JOB_ID")}/${nonce}/${signature}`,
|
||||
Deno.env.get("WM_BASE_URL"),
|
||||
);
|
||||
if (approver) {
|
||||
u.searchParams.append('approver', approver);
|
||||
}
|
||||
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
return {
|
||||
approvalPage: getApprovalPage(),
|
||||
resume: getResumeUrl("resume"),
|
||||
cancel: getResumeUrl("cancel"),
|
||||
};
|
||||
export function getResumeEndpoints(approver?: string): Promise<{
|
||||
approvalPage: string;
|
||||
resume: string;
|
||||
cancel: string;
|
||||
}> {
|
||||
return getResumeUrls(approver)
|
||||
}
|
||||
|
||||
export function base64ToUint8Array(data: string): Uint8Array {
|
||||
|
||||
@@ -327,6 +327,24 @@ def get_state_path() -> str:
|
||||
return state_path
|
||||
|
||||
|
||||
def get_resume_urls(approver: str | None = None) -> Dict:
|
||||
from windmill_api.api.job import get_resume_urls as get_resume_urls_api
|
||||
|
||||
workspace = get_workspace()
|
||||
client = create_client()
|
||||
job_id = os.environ.get("WM_JOB_ID") or "NO_ID"
|
||||
import random
|
||||
|
||||
nonce = random.randint(0, 1000000000)
|
||||
res = get_resume_urls_api.sync_detailed(
|
||||
workspace, job_id, nonce, client=client, approver=approver
|
||||
)
|
||||
if res.parsed is not None:
|
||||
return res.parsed.to_dict()
|
||||
else:
|
||||
raise Exception("Failed to get resume urls")
|
||||
|
||||
|
||||
def _transform_leaves(d: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {k: _transform_leaf(v) for k, v in d.items()}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user