mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
feat(forks): opt-in fork_triggers flag clones triggers/schedules disabled
Adds `workspace.fork_triggers` (default false) and a matching field on CreateWorkspaceFork. When the user opts in, fork creation also runs clone_triggers_and_schedules: every row in schedule and the ten *_trigger tables is copied to the fork with mode='disabled' / enabled=false. Listener identifiers (group_id, replication_slot_name, subscription_name, …) are copied verbatim — the runtime suffix that prevents the fork from competing with the parent ships in a follow-up PR. native_trigger is intentionally skipped: those triggers manage external webhook state we don't want duplicated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a4d8f138ae
commit
482b7c1524
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspace DROP COLUMN fork_triggers;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Tracks whether triggers and schedules were cloned (always disabled) at fork
|
||||
-- creation time. Read by the create_fork handler; reflected in the UI so users
|
||||
-- can tell which forks carry the parent's trigger/schedule definitions.
|
||||
ALTER TABLE workspace ADD COLUMN fork_triggers BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@@ -401,6 +401,12 @@ struct CreateWorkspaceFork {
|
||||
/// forked workspace's datatable config to point to the new database.
|
||||
#[serde(default)]
|
||||
forked_datatables: Vec<ForkedDatatableInfo>,
|
||||
/// When true, clone every trigger and schedule from the parent workspace
|
||||
/// (always with mode='disabled'/enabled=false). The user re-enables what
|
||||
/// they need; warnings on enable cover the parent-conflict case. When
|
||||
/// false (default), the fork starts trigger-empty — same as today.
|
||||
#[serde(default)]
|
||||
fork_triggers: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -3790,6 +3796,243 @@ async fn clone_workspace_data(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clone every trigger and schedule from the parent workspace, forcing
|
||||
/// `mode='disabled'` / `enabled=false`. Called only when the fork-creation
|
||||
/// request opts in via `fork_triggers=true`. Listener identifiers
|
||||
/// (group_id, replication_slot_name, subscription_name, …) are copied
|
||||
/// verbatim — the runtime suffix that prevents the fork from competing with
|
||||
/// the parent ships in a follow-up PR (see Phase 3).
|
||||
async fn clone_triggers_and_schedules(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
source_workspace_id: &str,
|
||||
target_workspace_id: &str,
|
||||
) -> Result<()> {
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO schedule (
|
||||
workspace_id, path, edited_by, edited_at, schedule, enabled, script_path,
|
||||
args, extra_perms, is_flow, email, error, timezone, on_failure,
|
||||
on_recovery, on_failure_times, on_failure_exact, on_failure_extra_args,
|
||||
on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry,
|
||||
summary, no_flow_overlap, tag, paused_until, on_success, on_success_extra_args,
|
||||
cron_version, description, dynamic_skip, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
$1, path, edited_by, edited_at, schedule, FALSE, script_path,
|
||||
args, extra_perms, is_flow, email, error, timezone, on_failure,
|
||||
on_recovery, on_failure_times, on_failure_exact, on_failure_extra_args,
|
||||
on_recovery_times, on_recovery_extra_args, ws_error_handler_muted, retry,
|
||||
summary, no_flow_overlap, tag, paused_until, on_success, on_success_extra_args,
|
||||
cron_version, description, dynamic_skip, permissioned_as, labels
|
||||
FROM schedule WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO http_trigger (
|
||||
path, route_path, route_path_key, script_path, is_flow, workspace_id,
|
||||
edited_by, edited_at, extra_perms, authentication_method, http_method,
|
||||
static_asset_config, is_static_website, workspaced_route, wrap_body,
|
||||
raw_string, authentication_resource_path, summary, description,
|
||||
error_handler_path, error_handler_args, retry, request_type, mode,
|
||||
permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
path, route_path, route_path_key, script_path, is_flow, $1,
|
||||
edited_by, edited_at, extra_perms, authentication_method, http_method,
|
||||
static_asset_config, is_static_website, workspaced_route, wrap_body,
|
||||
raw_string, authentication_resource_path, summary, description,
|
||||
error_handler_path, error_handler_args, retry, request_type, 'disabled'::TRIGGER_MODE,
|
||||
permissioned_as, labels
|
||||
FROM http_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO websocket_trigger (
|
||||
path, url, script_path, is_flow, workspace_id, edited_by, edited_at,
|
||||
extra_perms, server_id, last_server_ping, error, filters, initial_messages,
|
||||
url_runnable_args, can_return_message, error_handler_path, error_handler_args,
|
||||
retry, can_return_error_result, mode, permissioned_as, filter_logic, labels,
|
||||
heartbeat
|
||||
)
|
||||
SELECT
|
||||
path, url, script_path, is_flow, $1, edited_by, edited_at,
|
||||
extra_perms, NULL, NULL, NULL, filters, initial_messages,
|
||||
url_runnable_args, can_return_message, error_handler_path, error_handler_args,
|
||||
retry, can_return_error_result, 'disabled'::TRIGGER_MODE, permissioned_as, filter_logic, labels,
|
||||
heartbeat
|
||||
FROM websocket_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO kafka_trigger (
|
||||
path, kafka_resource_path, topics, group_id, script_path, is_flow,
|
||||
workspace_id, edited_by, edited_at, extra_perms, server_id,
|
||||
last_server_ping, error, error_handler_path, error_handler_args, retry,
|
||||
mode, filters, auto_offset_reset, reset_offset, auto_commit,
|
||||
permissioned_as, filter_logic, labels
|
||||
)
|
||||
SELECT
|
||||
path, kafka_resource_path, topics, group_id, script_path, is_flow,
|
||||
$1, edited_by, edited_at, extra_perms, NULL,
|
||||
NULL, NULL, error_handler_path, error_handler_args, retry,
|
||||
'disabled'::TRIGGER_MODE, filters, auto_offset_reset, reset_offset, auto_commit,
|
||||
permissioned_as, filter_logic, labels
|
||||
FROM kafka_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO nats_trigger (
|
||||
path, nats_resource_path, subjects, stream_name, consumer_name,
|
||||
use_jetstream, script_path, is_flow, workspace_id, edited_by, edited_at,
|
||||
extra_perms, server_id, last_server_ping, error, error_handler_path,
|
||||
error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
path, nats_resource_path, subjects, stream_name, consumer_name,
|
||||
use_jetstream, script_path, is_flow, $1, edited_by, edited_at,
|
||||
extra_perms, NULL, NULL, NULL, error_handler_path,
|
||||
error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM nats_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO postgres_trigger (
|
||||
path, script_path, is_flow, workspace_id, edited_by, edited_at,
|
||||
extra_perms, postgres_resource_path, error, server_id, last_server_ping,
|
||||
replication_slot_name, publication_name, error_handler_path,
|
||||
error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
path, script_path, is_flow, $1, edited_by, edited_at,
|
||||
extra_perms, postgres_resource_path, NULL, NULL, NULL,
|
||||
replication_slot_name, publication_name, error_handler_path,
|
||||
error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM postgres_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO mqtt_trigger (
|
||||
mqtt_resource_path, subscribe_topics, client_version, v5_config, v3_config,
|
||||
client_id, path, script_path, is_flow, workspace_id, edited_by, edited_at,
|
||||
extra_perms, server_id, last_server_ping, error, error_handler_path,
|
||||
error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
mqtt_resource_path, subscribe_topics, client_version, v5_config, v3_config,
|
||||
client_id, path, script_path, is_flow, $1, edited_by, edited_at,
|
||||
extra_perms, NULL, NULL, NULL, error_handler_path,
|
||||
error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM mqtt_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO sqs_trigger (
|
||||
path, queue_url, aws_resource_path, message_attributes, script_path,
|
||||
is_flow, workspace_id, edited_by, edited_at, extra_perms, error,
|
||||
server_id, last_server_ping, aws_auth_resource_type, error_handler_path,
|
||||
error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
path, queue_url, aws_resource_path, message_attributes, script_path,
|
||||
is_flow, $1, edited_by, edited_at, extra_perms, NULL,
|
||||
NULL, NULL, aws_auth_resource_type, error_handler_path,
|
||||
error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM sqs_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO gcp_trigger (
|
||||
gcp_resource_path, topic_id, subscription_id, delivery_type,
|
||||
delivery_config, path, script_path, is_flow, workspace_id, edited_by,
|
||||
edited_at, extra_perms, server_id, last_server_ping, error,
|
||||
subscription_mode, error_handler_path, error_handler_args, retry,
|
||||
auto_acknowledge_msg, ack_deadline, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
gcp_resource_path, topic_id, subscription_id, delivery_type,
|
||||
delivery_config, path, script_path, is_flow, $1, edited_by,
|
||||
edited_at, extra_perms, NULL, NULL, NULL,
|
||||
subscription_mode, error_handler_path, error_handler_args, retry,
|
||||
auto_acknowledge_msg, ack_deadline, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM gcp_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO azure_trigger (
|
||||
azure_resource_path, azure_mode, scope_resource_id, topic_name,
|
||||
subscription_name, event_type_filters, push_auth_config, path, script_path,
|
||||
is_flow, workspace_id, edited_by, email, edited_at, extra_perms, server_id,
|
||||
last_server_ping, error, mode, permissioned_as, error_handler_path,
|
||||
error_handler_args, retry, labels
|
||||
)
|
||||
SELECT
|
||||
azure_resource_path, azure_mode, scope_resource_id, topic_name,
|
||||
subscription_name, event_type_filters, push_auth_config, path, script_path,
|
||||
is_flow, $1, edited_by, email, edited_at, extra_perms, NULL,
|
||||
NULL, NULL, 'disabled'::TRIGGER_MODE, permissioned_as, error_handler_path,
|
||||
error_handler_args, retry, labels
|
||||
FROM azure_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
r#"INSERT INTO email_trigger (
|
||||
path, local_part, workspaced_local_part, script_path, is_flow,
|
||||
workspace_id, edited_by, edited_at, extra_perms, error_handler_path,
|
||||
error_handler_args, retry, mode, permissioned_as, labels
|
||||
)
|
||||
SELECT
|
||||
path, local_part, workspaced_local_part, script_path, is_flow,
|
||||
$1, edited_by, edited_at, extra_perms, error_handler_path,
|
||||
error_handler_args, retry, 'disabled'::TRIGGER_MODE, permissioned_as, labels
|
||||
FROM email_trigger WHERE workspace_id = $2"#,
|
||||
target_workspace_id,
|
||||
source_workspace_id,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_workspace_settings(
|
||||
tx: &mut Transaction<'_, Postgres>,
|
||||
source_workspace_id: &str,
|
||||
@@ -4621,12 +4864,13 @@ async fn create_workspace_fork(
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO workspace
|
||||
(id, name, owner, parent_workspace_id)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
(id, name, owner, parent_workspace_id, fork_triggers)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
forked_id,
|
||||
nw.name,
|
||||
authed.email,
|
||||
parent_workspace_id,
|
||||
nw.fork_triggers,
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -4657,6 +4901,13 @@ async fn create_workspace_fork(
|
||||
// Clone all data from the parent workspace using Rust implementation
|
||||
clone_workspace_data(&mut tx, &parent_workspace_id, &forked_id).await?;
|
||||
|
||||
// Optionally clone triggers and schedules. Always with mode='disabled' /
|
||||
// enabled=false — the user re-enables in the fork, with warnings if the
|
||||
// parent has the same path enabled (see Phase 4).
|
||||
if nw.fork_triggers {
|
||||
clone_triggers_and_schedules(&mut tx, &parent_workspace_id, &forked_id).await?;
|
||||
}
|
||||
|
||||
// Update forked datatable settings to point to new databases
|
||||
for fdt in &nw.forked_datatables {
|
||||
apply_forked_datatable(&db, &mut tx, &parent_workspace_id, &forked_id, fdt).await?;
|
||||
|
||||
@@ -25836,6 +25836,13 @@ components:
|
||||
new_dbname:
|
||||
type: string
|
||||
description: "New database name for the fork"
|
||||
fork_triggers:
|
||||
type: boolean
|
||||
description: >
|
||||
When true, every trigger and schedule from the parent workspace is
|
||||
cloned into the fork with mode='disabled' / enabled=false. The user
|
||||
re-enables in the fork. When false (default), the fork starts
|
||||
trigger-empty.
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
|
||||
Reference in New Issue
Block a user