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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions migrations/20260306120000_add_cloud_name.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_cloud_user_name;
ALTER TABLE cloud DROP COLUMN IF EXISTS name;
12 changes: 12 additions & 0 deletions migrations/20260306120000_add_cloud_name.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Add a human-friendly name to cloud credentials so users can reference them
-- by name (e.g. `stacker deploy --key my-hetzner`) instead of by provider.
ALTER TABLE cloud ADD COLUMN name VARCHAR(100);

-- Backfill existing rows: default name = "{provider}-{id}" (e.g. "htz-4")
UPDATE cloud SET name = provider || '-' || id WHERE name IS NULL;

-- Make name NOT NULL after backfill
ALTER TABLE cloud ALTER COLUMN name SET NOT NULL;

-- Unique per user: a user can't have two cloud keys with the same name
CREATE UNIQUE INDEX idx_cloud_user_name ON cloud (user_id, name);
9 changes: 9 additions & 0 deletions migrations/20260306190000_casbin_client_role_mapping.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Revert client role Casbin mappings
DELETE FROM public.casbin_rule WHERE ptype = 'g' AND v0 = 'client' AND v1 = 'group_anonymous';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/api/v1/agent/register' AND v2 = 'POST';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/api/v1/agent/commands/wait/:deployment_hash' AND v2 = 'GET';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/api/v1/agent/commands/report' AND v2 = 'POST';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/project/:id/deploy' AND v2 = 'POST';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/project/:id/deploy/:cloud_id' AND v2 = 'POST';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/project/:id/compose' AND v2 = 'GET';
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'client' AND v1 = '/project/:id/compose' AND v2 = 'POST';
44 changes: 44 additions & 0 deletions migrations/20260306190000_casbin_client_role_mapping.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Fix 403 on agent registration when using HMAC auth (client role).
-- The HMAC middleware now sets subject = "client" (previously was the numeric
-- client_id which had no Casbin mapping at all).
-- Ensure the "client" role inherits from group_anonymous (like group_user/group_admin).

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('g', 'client', 'group_anonymous', '', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

-- Safety: ensure agent register is accessible by group_anonymous
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'group_anonymous', '/api/v1/agent/register', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

-- Safety: ensure client has explicit access to agent register
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/api/v1/agent/register', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

-- Grant client access to other agent endpoints (wait, report, enqueue)
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/api/v1/agent/commands/wait/:deployment_hash', 'GET', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/api/v1/agent/commands/report', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

-- Grant client access to deploy-related endpoints that HMAC clients need
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/project/:id/deploy', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/project/:id/deploy/:cloud_id', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/project/:id/compose', 'GET', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;

INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'client', '/project/:id/compose', 'POST', '', '', '')
ON CONFLICT ON CONSTRAINT unique_key_sqlx_adapter DO NOTHING;
42 changes: 41 additions & 1 deletion src/bin/stacker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ enum StackerCommands {
/// Name of saved cloud credential to reuse (overrides deploy.cloud.key in stacker.yml)
#[arg(long, value_name = "KEY_NAME")]
key: Option<String>,
/// ID of saved cloud credential to reuse (from `stacker list clouds`)
#[arg(long, value_name = "CLOUD_ID")]
key_id: Option<i32>,
/// Name of saved server to reuse (overrides deploy.cloud.server in stacker.yml)
#[arg(long, value_name = "SERVER_NAME")]
server: Option<String>,
Expand All @@ -100,6 +103,12 @@ enum StackerCommands {
/// Disable automatic progress watching after deploy
#[arg(long)]
no_watch: bool,
/// Persist server details into stacker.yml after deploy (for redeploy)
#[arg(long)]
lock: bool,
/// Skip server pre-check; force fresh cloud provision even if deploy.server exists
#[arg(long)]
force_new: bool,
},
/// Show container logs
Logs {
Expand Down Expand Up @@ -207,6 +216,12 @@ enum ListCommands {
#[arg(long)]
json: bool,
},
/// List saved cloud credentials
Clouds {
/// Output in JSON format
#[arg(long)]
json: bool,
},
}

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -306,6 +321,16 @@ enum ConfigCommands {
#[arg(long, default_value_t = true)]
interactive: bool,
},
/// Persist deployment lock into stacker.yml (writes deploy.server from last deploy)
Lock {
#[arg(long, value_name = "FILE")]
file: Option<String>,
},
/// Remove deploy.server section from stacker.yml (allows fresh cloud provision)
Unlock {
#[arg(long, value_name = "FILE")]
file: Option<String>,
},
/// Guided setup helpers
Setup {
#[command(subcommand)]
Expand Down Expand Up @@ -500,9 +525,12 @@ fn get_command(
force_rebuild,
project,
key,
key_id,
server,
watch,
no_watch,
lock,
force_new,
} => Box::new(
stacker::console::commands::cli::deploy::DeployCommand::new(
target,
Expand All @@ -511,7 +539,10 @@ fn get_command(
force_rebuild,
)
.with_remote_overrides(project, key, server)
.with_watch(watch, no_watch),
.with_key_id(key_id)
.with_watch(watch, no_watch)
.with_lock(lock)
.with_force_new(force_new),
),
StackerCommands::Logs {
service,
Expand Down Expand Up @@ -540,6 +571,12 @@ fn get_command(
ConfigCommands::Fix { file, interactive } => Box::new(
stacker::console::commands::cli::config::ConfigFixCommand::new(file, interactive),
),
ConfigCommands::Lock { file } => Box::new(
stacker::console::commands::cli::config::ConfigLockCommand::new(file),
),
ConfigCommands::Unlock { file } => Box::new(
stacker::console::commands::cli::config::ConfigUnlockCommand::new(file),
),
ConfigCommands::Setup { command } => match command {
ConfigSetupCommands::Cloud { file } => Box::new(
stacker::console::commands::cli::config::ConfigSetupCloudCommand::new(file),
Expand Down Expand Up @@ -590,6 +627,9 @@ fn get_command(
ListCommands::SshKeys { json } => Box::new(
stacker::console::commands::cli::list::ListSshKeysCommand::new(json),
),
ListCommands::Clouds { json } => Box::new(
stacker::console::commands::cli::list::ListCloudsCommand::new(json),
),
},
StackerCommands::SshKey { command: ssh_cmd } => match ssh_cmd {
SshKeyCommands::Generate { server_id, save_to } => Box::new(
Expand Down
Loading
Loading