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
31 changes: 14 additions & 17 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
SQLX_OFFLINE: true
steps:
- name: Checkout sources
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}

Expand All @@ -47,7 +47,7 @@ jobs:
components: rustfmt, clippy

- name: Cache cargo registry
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: docker-registry-${{ hashFiles('**/Cargo.lock') }}
Expand All @@ -56,7 +56,7 @@ jobs:
docker-

- name: Cache cargo index
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: docker-index-${{ hashFiles('**/Cargo.lock') }}
Expand All @@ -69,7 +69,7 @@ jobs:
head -c16 /dev/urandom > src/secret.key

- name: Cache cargo build
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: target
key: docker-build-${{ hashFiles('**/Cargo.lock') }}
Expand Down Expand Up @@ -124,7 +124,7 @@ jobs:

- name: Archive production artifacts
if: ${{ hashFiles('web/package.json') != '' }}
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: dist-without-markdown
path: |
Expand All @@ -141,13 +141,14 @@ jobs:
cp target/release/server app/stacker/server
if [ -d web/dist ]; then cp -a web/dist/. app/stacker; fi
cp Dockerfile app/Dockerfile
cp access_control.conf.dist app/access_control.conf.dist
cd app
touch .env
tar -czvf ../app.tar.gz .
cd ..

- name: Upload app archive for Docker job
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: artifact-linux-docker
path: app.tar.gz
Expand All @@ -157,16 +158,10 @@ jobs:
runs-on: ubuntu-latest
needs: cicd-docker
steps:
- name: Download app archive
uses: actions/download-artifact@v7
- name: Checkout sources
uses: actions/checkout@v4
with:
name: artifact-linux-docker

- name: Extract app archive
run: tar -zxvf app.tar.gz

- name: Display structure of downloaded files
run: ls -R
ref: ${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}

-
name: Set up QEMU
Expand All @@ -192,7 +187,8 @@ jobs:
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.docker_tags.outputs.tags }}

stackerdb-docker:
Expand All @@ -201,7 +197,7 @@ jobs:
needs: cicd-docker
steps:
- name: Checkout sources
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
-
Expand Down Expand Up @@ -231,3 +227,4 @@ jobs:
file: ./stackerdb/Dockerfile
push: true
tags: ${{ steps.stackerdb_tags.outputs.tags }}

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM public.casbin_rule WHERE ptype = 'p' AND v0 = 'group_user' AND v1 = '/api/templates/:id/reviews' AND v2 = 'GET';
2 changes: 2 additions & 0 deletions migrations/20260325100000_casbin_template_reviews_rule.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'group_user', '/api/templates/:id/reviews', 'GET', '', '', '') ON CONFLICT DO NOTHING;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DELETE FROM public.casbin_rule
WHERE ptype = 'p'
AND v0 = 'group_admin'
AND v1 = '/admin/project/:id/compose'
AND v2 = 'GET';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Allow group_admin (and roles inheriting it, like root) to access the admin
-- project compose endpoint. The existing rule only grants access to the
-- admin_service JWT role, but OAuth-based access (User Service client
-- credentials) authenticates as the client owner whose role is "root".
-- root inherits group_admin, so adding the policy here covers both paths.
INSERT INTO public.casbin_rule (ptype, v0, v1, v2, v3, v4, v5)
VALUES ('p', 'group_admin', '/admin/project/:id/compose', 'GET', '', '', '')
ON CONFLICT DO NOTHING;
31 changes: 31 additions & 0 deletions src/bin/stacker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,27 @@ enum MarketplaceCommands {
#[arg(long)]
json: bool,
},
/// Submit current stack to the marketplace for review
Submit {
/// Path to stacker.yml (default: ./stacker.yml)
#[arg(long, value_name = "FILE")]
file: Option<String>,
/// Stack version (default: from stacker.yml or "1.0.0")
#[arg(long)]
version: Option<String>,
/// Short description for marketplace listing
#[arg(long)]
description: Option<String>,
/// Category code (e.g. ai-agents, data-pipelines, saas-starter)
#[arg(long)]
category: Option<String>,
/// Pricing: free, one_time, subscription (default: free)
#[arg(long, value_name = "TYPE")]
plan_type: Option<String>,
/// Price amount (required if plan_type is not free)
#[arg(long)]
price: Option<f64>,
},
}

#[derive(Debug, Subcommand)]
Expand Down Expand Up @@ -1186,6 +1207,16 @@ fn get_command(
name, json,
),
),
MarketplaceCommands::Submit {
file,
version,
description,
category,
plan_type,
price,
} => Box::new(stacker::console::commands::cli::submit::SubmitCommand::new(
file, version, description, category, plan_type, price,
)),
},
// Completion is handled in main() before this function is called.
StackerCommands::Completion { .. } => unreachable!(),
Expand Down
6 changes: 6 additions & 0 deletions src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub enum CliError {
EnvFileNotFound { path: std::path::PathBuf },
SecretKeyNotFound { key: String },

// Marketplace errors
MarketplaceFailed(String),

// Agent errors
AgentNotFound { deployment_hash: String },
AgentOffline { deployment_hash: String },
Expand Down Expand Up @@ -148,6 +151,9 @@ impl fmt::Display for CliError {
Self::SecretKeyNotFound { key } => {
write!(f, "Secret key not found: {key}")
}
Self::MarketplaceFailed(msg) => {
write!(f, "Marketplace error: {msg}")
}
Self::AgentNotFound { deployment_hash } => {
write!(
f,
Expand Down
90 changes: 28 additions & 62 deletions src/cli/stacker_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1361,25 +1361,19 @@ impl StackerClient {
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Stacker server unreachable: {}", e),
})?;
.map_err(|e| CliError::MarketplaceFailed(format!("Stacker server unreachable: {}", e)))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("GET /api/templates/mine failed ({}): {}", status, body),
});
return Err(CliError::MarketplaceFailed(format!(
"GET /api/templates/mine failed ({}): {}",
status, body
)));
}

let api: ApiResponse<MarketplaceTemplateInfo> = resp.json().await.map_err(|e| {
CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Invalid response from Stacker server: {}", e),
}
CliError::MarketplaceFailed(format!("Invalid response from Stacker server: {}", e))
})?;

Ok(api.list.unwrap_or_default())
Expand All @@ -1390,46 +1384,29 @@ impl StackerClient {
&self,
template_id: &str,
) -> Result<Vec<MarketplaceReviewInfo>, CliError> {
let url = format!("{}/api/admin/templates/{}", self.base_url, template_id);
let url = format!("{}/api/templates/{}/reviews", self.base_url, template_id);
let resp = self
.http
.get(&url)
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Stacker server unreachable: {}", e),
})?;
.map_err(|e| CliError::MarketplaceFailed(format!("Stacker server unreachable: {}", e)))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!(
"GET /api/admin/templates/{} failed ({}): {}",
template_id, status, body
),
});
return Err(CliError::MarketplaceFailed(format!(
"GET /api/templates/{}/reviews failed ({}): {}",
template_id, status, body
)));
}

let api: ApiResponse<serde_json::Value> = resp.json().await.map_err(|e| {
CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Invalid response from Stacker server: {}", e),
}
let api: ApiResponse<MarketplaceReviewInfo> = resp.json().await.map_err(|e| {
CliError::MarketplaceFailed(format!("Invalid response from Stacker server: {}", e))
})?;

let item = api.item.unwrap_or(serde_json::json!({}));
let reviews: Vec<MarketplaceReviewInfo> = serde_json::from_value(
item.get("reviews")
.cloned()
.unwrap_or(serde_json::json!([])),
)
.unwrap_or_default();

Ok(reviews)
Ok(api.list.unwrap_or_default())
}

/// Create or update a marketplace template (POST /api/templates).
Expand All @@ -1445,31 +1422,23 @@ impl StackerClient {
.json(&body)
.send()
.await
.map_err(|e| CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("create template: {}", e),
})?;
.map_err(|e| CliError::MarketplaceFailed(format!("create template: {}", e)))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Create template failed ({}): {}", status, body),
});
return Err(CliError::MarketplaceFailed(format!(
"Create template failed ({}): {}",
status, body
)));
}

let api: ApiResponse<MarketplaceTemplateInfo> = resp.json().await.map_err(|e| {
CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("create template response: {}", e),
}
CliError::MarketplaceFailed(format!("create template response: {}", e))
})?;

api.item.ok_or_else(|| CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: "No template in response".to_string(),
})
api.item
.ok_or_else(|| CliError::MarketplaceFailed("No template in response".to_string()))
}

/// Submit a template for marketplace review.
Expand All @@ -1481,18 +1450,15 @@ impl StackerClient {
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Stacker server unreachable: {}", e),
})?;
.map_err(|e| CliError::MarketplaceFailed(format!("Stacker server unreachable: {}", e)))?;

if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(CliError::DeployFailed {
target: crate::cli::config_parser::DeployTarget::Cloud,
reason: format!("Submit failed ({}): {}", status, body),
});
return Err(CliError::MarketplaceFailed(format!(
"Submit failed ({}): {}",
status, body
)));
}

Ok(())
Expand Down
7 changes: 4 additions & 3 deletions src/connectors/dockerhub_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ impl RedisCache {
ConnectorError::Internal(format!("Invalid Redis URL for Docker Hub cache: {}", err))
})?;

let connection = ConnectionManager::new(client).await.map_err(|err| {
ConnectorError::ServiceUnavailable(format!("Redis unavailable: {}", err))
})?;
let connection = tokio::time::timeout(Duration::from_secs(3), ConnectionManager::new(client))
.await
.map_err(|_| ConnectorError::ServiceUnavailable("Redis connection timed out".to_string()))?
.map_err(|err| ConnectorError::ServiceUnavailable(format!("Redis unavailable: {}", err)))?;

Ok(Self {
connection: Arc::new(Mutex::new(connection)),
Expand Down
Loading
Loading