Skip to main content

systemprompt_cli/commands/cloud/dockerfile/
validation.rs

1use anyhow::{Result, bail};
2use std::collections::HashSet;
3use std::path::Path;
4
5use systemprompt_cloud::constants::container;
6use systemprompt_loader::ExtensionLoader;
7use systemprompt_models::ServicesConfig;
8
9pub fn get_required_mcp_copy_lines(
10    project_root: &Path,
11    services_config: &ServicesConfig,
12) -> Vec<String> {
13    ExtensionLoader::get_production_mcp_binary_names(project_root, services_config)
14        .iter()
15        .map(|bin| format!("COPY target/release/{} {}/", bin, container::BIN))
16        .collect()
17}
18
19fn extract_mcp_binary_names_from_dockerfile(dockerfile_content: &str) -> Vec<String> {
20    dockerfile_content
21        .lines()
22        .filter_map(|line| {
23            let trimmed = line.trim();
24            if !trimmed.starts_with("COPY target/release/systemprompt-") {
25                return None;
26            }
27            let after_copy = trimmed.strip_prefix("COPY target/release/")?;
28            let binary_name = after_copy.split_whitespace().next()?;
29            if binary_name.starts_with("systemprompt-") && binary_name != "systemprompt-*" {
30                Some(binary_name.to_string())
31            } else {
32                None
33            }
34        })
35        .collect()
36}
37
38pub fn validate_dockerfile_has_mcp_binaries(
39    dockerfile_content: &str,
40    project_root: &Path,
41    services_config: &ServicesConfig,
42) -> Vec<String> {
43    let has_wildcard = dockerfile_content.contains("target/release/systemprompt-*");
44    if has_wildcard {
45        return Vec::new();
46    }
47
48    ExtensionLoader::get_production_mcp_binary_names(project_root, services_config)
49        .into_iter()
50        .filter(|binary| {
51            let expected_pattern = format!("target/release/{}", binary);
52            !dockerfile_content.contains(&expected_pattern)
53        })
54        .collect()
55}
56
57pub fn validate_dockerfile_has_no_stale_binaries(
58    dockerfile_content: &str,
59    project_root: &Path,
60    services_config: &ServicesConfig,
61) -> Vec<String> {
62    let has_wildcard = dockerfile_content.contains("target/release/systemprompt-*");
63    if has_wildcard {
64        return Vec::new();
65    }
66
67    let dockerfile_binaries = extract_mcp_binary_names_from_dockerfile(dockerfile_content);
68    let current_binaries: HashSet<String> =
69        ExtensionLoader::get_production_mcp_binary_names(project_root, services_config)
70            .into_iter()
71            .collect();
72
73    dockerfile_binaries
74        .into_iter()
75        .filter(|binary| !current_binaries.contains(binary))
76        .collect()
77}
78
79pub fn validate_profile_dockerfile(
80    dockerfile_path: &Path,
81    project_root: &Path,
82    services_config: &ServicesConfig,
83) -> Result<()> {
84    if !dockerfile_path.exists() {
85        bail!(
86            "Dockerfile not found at {}\n\nCreate a profile first with: systemprompt cloud \
87             profile create",
88            dockerfile_path.display()
89        );
90    }
91
92    let content = std::fs::read_to_string(dockerfile_path)?;
93    let missing = validate_dockerfile_has_mcp_binaries(&content, project_root, services_config);
94    let stale = validate_dockerfile_has_no_stale_binaries(&content, project_root, services_config);
95
96    match (missing.is_empty(), stale.is_empty()) {
97        (true, true) => Ok(()),
98        (false, true) => {
99            bail!(
100                "Dockerfile at {} is missing COPY commands for MCP binaries:\n\n{}\n\nAdd these \
101                 lines:\n\n{}",
102                dockerfile_path.display(),
103                missing.join(", "),
104                get_required_mcp_copy_lines(project_root, services_config).join("\n")
105            );
106        },
107        (true, false) => {
108            bail!(
109                "Dockerfile at {} has COPY commands for dev-only or removed \
110                 binaries:\n\n{}\n\nRemove these lines or regenerate with: systemprompt cloud \
111                 profile create",
112                dockerfile_path.display(),
113                stale.join(", ")
114            );
115        },
116        (false, false) => {
117            bail!(
118                "Dockerfile at {} has issues:\n\nMissing binaries: {}\nDev-only/stale binaries: \
119                 {}\n\nRegenerate with: systemprompt cloud profile create",
120                dockerfile_path.display(),
121                missing.join(", "),
122                stale.join(", ")
123            );
124        },
125    }
126}