Skip to content

Commit 5803237

Browse files
committed
New preflight in src/cli/install_runner.rs:
1. parse_compose_host_port() — parses both string (127.0.0.1:3000:80) and mapping ({published: 3000}) compose port entries 2. collect_compose_host_port_services() — reads the compose file and returns all (host_port, service_name) pairs 3. get_own_compose_running_ports() — runs docker compose ps --format {{.Ports}} to discover ports currently held by this project's own containers (to avoid false positives on redeploys) 4. check_local_host_port_conflicts() — TCP-binds each declared port; if a bind fails AND the port isn't owned by the project's own containers, it's flagged as a real conflict 5. LocalDeploy::deploy() — calls the check before docker compose up; if conflicts are found, it returns a clear error like: Host port conflict detected before deploy: • port 3000 (service 'status-panel-web') is already allocated on this host — find the owner with: lsof -nP -iTCP:3000 -sTCP:LISTEN Stop the conflicting process or change the port in stacker.yml, then retry.
1 parent c78352a commit 5803237

7 files changed

Lines changed: 635 additions & 30 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docker/dev/docker-compose.yml

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
version: "2.2"
2-
31
volumes:
42
stackerdb:
53
driver: local
@@ -8,30 +6,28 @@ volumes:
86
driver: local
97

108
networks:
11-
backend:
9+
trydirect_default:
1210
driver: bridge
13-
name: backend
1411
external: true
1512
trydirect-network:
1613
external: true
1714
name: trydirect-network
1815

19-
2016
services:
21-
2217
stacker:
23-
image: trydirect/stacker:0.0.8
24-
build: .
18+
image: trydirect/stacker:latest
19+
#image: trydirect/stacker:test
2520
container_name: stacker
2621
restart: always
2722
volumes:
2823
- ./stacker/files:/app/files
24+
- ./.env:/app/.env
2925
- ./configuration.yaml:/app/configuration.yaml
3026
- ./access_control.conf:/app/access_control.conf
3127
- ./migrations:/app/migrations
32-
- ./.env:/app/.env
28+
- ./stacker/ansible:/ansible/roles:ro
3329
ports:
34-
- "8000:8000"
30+
- "8001:8000"
3531
env_file:
3632
- ./.env
3733
environment:
@@ -41,33 +37,27 @@ services:
4137
stackerdb:
4238
condition: service_healthy
4339
networks:
44-
- backend
40+
- trydirect_default
4541

46-
47-
stacker_queue:
48-
image: trydirect/stacker:0.0.7
49-
container_name: stacker_queue
42+
stackermq:
43+
image: trydirect/stacker:latest
44+
#image: trydirect/stacker:test # for testing mcp
45+
container_name: stackermq
5046
restart: always
5147
volumes:
5248
- ./configuration.yaml:/app/configuration.yaml
5349
- ./.env:/app/.env
5450
environment:
5551
- RUST_LOG=debug
5652
- RUST_BACKTRACE=1
57-
- AMQP_HOST=rabbitmq
58-
- AMQP_PORT=5672
59-
- AMQP_USERNAME=guest
60-
- AMQP_PASSWORD=guest
6153
env_file:
6254
- ./.env
6355
depends_on:
6456
stackerdb:
6557
condition: service_healthy
6658
entrypoint: /app/console mq listen
6759
networks:
68-
- backend
69-
- trydirect-network
70-
60+
- trydirect_default
7161

7262
stackerdb:
7363
container_name: stackerdb
@@ -76,17 +66,17 @@ services:
7666
interval: 10s
7767
timeout: 5s
7868
retries: 5
79-
image: postgres:18.3
69+
image: postgres:16.0
8070
restart: always
8171
ports:
82-
- 5432
72+
- 5434:5432
8373
env_file:
8474
- ./.env
8575
volumes:
8676
- stackerdb:/var/lib/postgresql/data
8777
- ./postgresql.conf:/etc/postgresql/postgresql.conf
8878
networks:
89-
- backend
79+
- trydirect_default
9080

9181
stackerredis:
9282
container_name: stackerredis
@@ -105,5 +95,5 @@ services:
10595
options:
10696
max-size: "10m"
10797
tag: "container_{{.Name}}"
108-
109-
98+
networks:
99+
- trydirect_default

src/cli/config_parser.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,7 @@ pub struct ConfigBuilder {
12161216
app_path: Option<PathBuf>,
12171217
app_image: Option<String>,
12181218
app_dockerfile: Option<PathBuf>,
1219+
app_volumes: Vec<String>,
12191220
build_args: HashMap<String, String>,
12201221
services: Vec<ServiceDefinition>,
12211222
proxy: Option<ProxyConfig>,
@@ -1275,6 +1276,11 @@ impl ConfigBuilder {
12751276
self
12761277
}
12771278

1279+
pub fn app_volumes(mut self, volumes: Vec<String>) -> Self {
1280+
self.app_volumes = volumes;
1281+
self
1282+
}
1283+
12781284
pub fn build_arg<K: Into<String>, V: Into<String>>(mut self, key: K, value: V) -> Self {
12791285
self.build_args.insert(key.into(), value.into());
12801286
self
@@ -1364,7 +1370,7 @@ impl ConfigBuilder {
13641370
image: self.app_image,
13651371
build: build_config,
13661372
ports: Vec::new(),
1367-
volumes: Vec::new(),
1373+
volumes: self.app_volumes,
13681374
environment: HashMap::new(),
13691375
},
13701376
services: self.services,

src/cli/generator/compose.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ impl TryFrom<&StackerConfig> for ComposeDefinition {
106106

107107
// --- Main app service ---
108108
let app_service = build_app_service(config);
109+
for vol in &app_service.volumes {
110+
if let Some(named) = extract_named_volume(vol) {
111+
if !named_volumes.contains(&named) {
112+
named_volumes.push(named);
113+
}
114+
}
115+
}
109116
compose.services.push(app_service);
110117

111118
// --- Additional services (databases, caches, etc.) ---
@@ -736,6 +743,41 @@ mod tests {
736743
);
737744
}
738745

746+
#[test]
747+
fn app_named_volumes_appear_in_top_level_volumes_block() {
748+
let config = ConfigBuilder::new()
749+
.name("rustfs")
750+
.app_type(AppType::Custom)
751+
.app_image("rustfs/rustfs:latest")
752+
.app_volumes(vec![
753+
"rustfs_data:/data".into(),
754+
"rustfs_logs:/app/logs".into(),
755+
"./local-config:/etc/config:ro".into(), // bind mount — must NOT appear
756+
])
757+
.build()
758+
.unwrap();
759+
760+
let compose = ComposeDefinition::try_from(&config).unwrap();
761+
762+
assert!(
763+
compose.volumes.contains(&"rustfs_data".to_string()),
764+
"rustfs_data should be in top-level volumes"
765+
);
766+
assert!(
767+
compose.volumes.contains(&"rustfs_logs".to_string()),
768+
"rustfs_logs should be in top-level volumes"
769+
);
770+
assert!(
771+
!compose.volumes.contains(&"./local-config".to_string()),
772+
"bind mount should not appear in top-level volumes"
773+
);
774+
775+
let yaml = compose.render();
776+
assert!(yaml.contains("volumes:"), "top-level volumes: block must exist");
777+
assert!(yaml.contains(" rustfs_data:"), "rustfs_data entry must appear");
778+
assert!(yaml.contains(" rustfs_logs:"), "rustfs_logs entry must appear");
779+
}
780+
739781
#[test]
740782
fn test_extract_named_volume_returns_name() {
741783
assert_eq!(

0 commit comments

Comments
 (0)