From a2a54cc33b6d81518d04529b43b74de73a7a79cf Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 21:58:56 -0400 Subject: [PATCH 01/23] fix(bootstrap): replace em dash with ASCII in SG description --- operator/src/bootstrap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/bootstrap.rs b/operator/src/bootstrap.rs index ac3270bce..cbaccc789 100644 --- a/operator/src/bootstrap.rs +++ b/operator/src/bootstrap.rs @@ -355,7 +355,7 @@ async fn create(config: &aws_config::SdkConfig, imports: ImportOptions) -> Resul _ => { let resp = ec2.create_security_group() .group_name(SG_NAME) - .description("OAB agent containers — managed by oabctl bootstrap") + .description("OAB agent containers - managed by oabctl bootstrap") .vpc_id(&vid) .send().await .context("failed to create security group")?; From 010635fed2ce784d8e13c4cefdc051b83257aaaa Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:06:56 -0400 Subject: [PATCH 02/23] ci(oabctl): add Rust build cache --- .github/workflows/build-oabctl.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-oabctl.yml b/.github/workflows/build-oabctl.yml index 558fc2973..3e167fc5a 100644 --- a/.github/workflows/build-oabctl.yml +++ b/.github/workflows/build-oabctl.yml @@ -26,6 +26,10 @@ jobs: with: targets: ${{ inputs.target == 'linux-aarch64' && 'aarch64-unknown-linux-gnu' || '' }} + - uses: Swatinem/rust-cache@v2 + with: + workspaces: operator + - name: Install cross-compilation tools if: inputs.target == 'linux-aarch64' run: | From 0d0f35b1d2a3d7c84008ebe473d07699d989e67c Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:30:42 -0400 Subject: [PATCH 03/23] fix(create): use GHCR image URIs and add all backends --- operator/src/create.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/operator/src/create.rs b/operator/src/create.rs index 77241286a..9fd25989b 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -5,12 +5,17 @@ use aws_sdk_secretsmanager::Client as SmClient; use std::io::{self, Write}; const BACKENDS: &[(&str, &str)] = &[ - ("kiro", "public.ecr.aws/oablab/kiro"), - ("claude-code", "public.ecr.aws/oablab/claude-code"), - ("codex", "public.ecr.aws/oablab/codex"), - ("gemini", "public.ecr.aws/oablab/gemini"), - ("copilot", "public.ecr.aws/oablab/copilot"), - ("opencode", "public.ecr.aws/oablab/opencode"), + ("kiro", "ghcr.io/openabdev/openab"), + ("claude-code", "ghcr.io/openabdev/openab-claude"), + ("codex", "ghcr.io/openabdev/openab-codex"), + ("gemini", "ghcr.io/openabdev/openab-gemini"), + ("copilot", "ghcr.io/openabdev/openab-copilot"), + ("opencode", "ghcr.io/openabdev/openab-opencode"), + ("hermes", "ghcr.io/openabdev/openab-hermes"), + ("grok", "ghcr.io/openabdev/openab-grok"), + ("cursor", "ghcr.io/openabdev/openab-cursor"), + ("mimocode", "ghcr.io/openabdev/openab-mimocode"), + ("antigravity", "ghcr.io/openabdev/openab-antigravity"), ]; const CHANNELS: &[&str] = &["stable", "beta"]; From 9021bdf1d1d14d5442a7d79c0f5ba6df80f0200b Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:32:52 -0400 Subject: [PATCH 04/23] feat(create): add CPU/memory sizing selection with Fargate validation --- operator/src/create.rs | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/operator/src/create.rs b/operator/src/create.rs index 9fd25989b..6f1e7104c 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -20,6 +20,14 @@ const BACKENDS: &[(&str, &str)] = &[ const CHANNELS: &[&str] = &["stable", "beta"]; +const VALID_FARGATE_SIZING: &[(u32, &[u32])] = &[ + (256, &[512, 1024, 2048]), + (512, &[1024, 2048, 3072, 4096]), + (1024, &[2048, 3072, 4096, 5120, 6144, 7168, 8192]), + (2048, &[4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384]), + (4096, &[8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720]), +]; + pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, auto_apply: bool) -> Result<()> { eprintln!("🤖 Creating agent: {name}\n"); @@ -66,7 +74,19 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au let cap = prompt_select("Capacity provider", &["FARGATE_SPOT (cost-optimized)", "FARGATE (on-demand)"])?; let capacity_provider = if cap.starts_with("FARGATE_SPOT") { "FARGATE_SPOT" } else { "FARGATE" }; - // 6. VPC + // 6. CPU/Memory sizing + let cpu_options: Vec = VALID_FARGATE_SIZING.iter().map(|(c, _)| c.to_string()).collect(); + let cpu_labels: Vec<&str> = cpu_options.iter().map(|s| s.as_str()).collect(); + let cpu_choice = prompt_select("CPU (units)", &cpu_labels)?; + let cpu: u32 = cpu_choice.parse().unwrap(); + + let mem_values = VALID_FARGATE_SIZING.iter().find(|(c, _)| *c == cpu).unwrap().1; + let mem_options: Vec = mem_values.iter().map(|m| m.to_string()).collect(); + let mem_labels: Vec<&str> = mem_options.iter().map(|s| s.as_str()).collect(); + let mem_choice = prompt_select("Memory (MiB)", &mem_labels)?; + let memory: u32 = mem_choice.parse().unwrap(); + + // 7. VPC let ec2 = Ec2Client::new(config); let vpcs = list_vpcs(&ec2).await?; if vpcs.is_empty() { @@ -76,7 +96,7 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au let vpc_choice = prompt_select("VPC", &vpc_labels)?; let vpc = vpcs.iter().find(|v| v.label == vpc_choice).unwrap(); - // 7. Subnets (auto-select: private+NAT > private > public, 2-3 AZ) + // 8. Subnets (auto-select: private+NAT > private > public, 2-3 AZ) let subnets = select_subnets(&ec2, &vpc.id).await?; eprintln!(" Subnets (auto-selected):"); for s in &subnets { @@ -123,7 +143,7 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au std::fs::write(format!("{dir}/config.toml"), &config_toml)?; let subnet_ids: Vec = subnets.iter().map(|s| s.id.clone()).collect(); - let manifest_yaml = generate_manifest(name, namespace, &image, &config_from, capacity_provider, &subnet_ids, &sg_id); + let manifest_yaml = generate_manifest(name, namespace, &image, &config_from, capacity_provider, &subnet_ids, &sg_id, cpu, memory); std::fs::write(format!("{dir}/manifest.yaml"), &manifest_yaml)?; // ─── Summary ─────────────────────────────────────────────────────────── @@ -131,7 +151,7 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au eprintln!("Summary:"); eprintln!(" Agent: {name}"); eprintln!(" Image: {image}"); - eprintln!(" CPU/Mem: 256 / 512"); + eprintln!(" CPU/Mem: {} / {}", cpu, memory); eprintln!(" Runtime: ECS {capacity_provider}"); eprintln!(" Subnets: {}", subnet_ids.join(", ")); eprintln!(" SG: {sg_id}"); @@ -359,7 +379,7 @@ usercron_path = "cronjob.toml" ) } -fn generate_manifest(name: &str, namespace: &str, image: &str, config_from: &str, cap: &str, subnets: &[String], sg: &str) -> String { +fn generate_manifest(name: &str, namespace: &str, image: &str, config_from: &str, cap: &str, subnets: &[String], sg: &str, cpu: u32, memory: u32) -> String { let subnets_yaml = subnets.iter().map(|s| format!("\"{}\"", s)).collect::>().join(", "); format!( r#"apiVersion: oab.dev/v2 @@ -370,8 +390,8 @@ metadata: spec: image: {image} resources: - cpu: "256" - memory: "512" + cpu: "{cpu}" + memory: "{memory}" configFrom: {config_from} runtime: type: ecs From 46eda534c399abb4d33c5aa8527f87e76a04b89f Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:36:49 -0400 Subject: [PATCH 05/23] refactor(create): always create dedicated SG, remove selection --- operator/src/create.rs | 45 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/operator/src/create.rs b/operator/src/create.rs index 6f1e7104c..d1227ad55 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -104,27 +104,16 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au } eprintln!(); - // 8. Security group - let sgs = list_security_groups(&ec2, &vpc.id).await?; - let mut sg_labels: Vec = vec!["Create new (oab-{name})".to_string()]; - sg_labels.extend(sgs.iter().map(|s| format!("{} ({})", s.id, s.name))); - let sg_labels_ref: Vec<&str> = sg_labels.iter().map(|s| s.as_str()).collect(); - let sg_choice = prompt_select("Security group", &sg_labels_ref)?; - - let sg_id = if sg_choice.starts_with("Create new") { - let sg_name = format!("oab-{name}"); - let resp = ec2.create_security_group() - .group_name(&sg_name) - .description(format!("OAB agent {name}")) - .vpc_id(&vpc.id) - .send().await - .context("failed to create security group")?; - let id = resp.group_id().unwrap_or_default().to_string(); - eprintln!(" → Created security group: {id}\n"); - id - } else { - sgs.iter().find(|s| sg_choice.contains(&s.id)).unwrap().id.clone() - }; + // 8. Security group (always create a dedicated one) + let sg_name = format!("oab-{name}"); + let resp = ec2.create_security_group() + .group_name(&sg_name) + .description(format!("OAB agent {name}")) + .vpc_id(&vpc.id) + .send().await + .context("failed to create security group")?; + let sg_id = resp.group_id().unwrap_or_default().to_string(); + eprintln!(" → Created security group: {sg_id} ({sg_name})\n"); // ─── Generate config.toml ────────────────────────────────────────────── let config_toml = generate_config(backend, name, namespace, stt_enabled); @@ -307,20 +296,6 @@ async fn select_subnets(ec2: &Ec2Client, vpc_id: &str) -> Result Ok(selected) } -struct SgInfo { id: String, name: String } - -async fn list_security_groups(ec2: &Ec2Client, vpc_id: &str) -> Result> { - let resp = ec2.describe_security_groups() - .filters(aws_sdk_ec2::types::Filter::builder().name("vpc-id").values(vpc_id).build()) - .send().await?; - Ok(resp.security_groups().iter().map(|sg| { - SgInfo { - id: sg.group_id().unwrap_or_default().to_string(), - name: sg.group_name().unwrap_or_default().to_string(), - } - }).collect()) -} - fn generate_config(_backend: &str, name: &str, namespace: &str, stt_enabled: bool) -> String { let stt_section = if stt_enabled { r#"[stt] From 017e36a55ecdc268fd9697304e8e76196261ea16 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:45:18 -0400 Subject: [PATCH 06/23] fix(apply): download S3 config at container start via command override --- operator/src/apply.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 794370179..758cbab2e 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -182,13 +182,21 @@ async fn apply_ecs( .collect(); // 4. Register task definition - let container = ContainerDefinition::builder() + let mut container_builder = ContainerDefinition::builder() .name("openab") .image(&m.spec.image) .essential(true) .set_environment(Some(env_vars)) - .set_secrets(if secrets.is_empty() { None } else { Some(secrets) }) - .build(); + .set_secrets(if secrets.is_empty() { None } else { Some(secrets) }); + + if !m.spec.config_from.is_empty() { + container_builder = container_builder + .entry_point("sh") + .entry_point("-c") + .command("aws s3 cp $CONFIG_S3_PATH /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); + } + + let container = container_builder.build(); let task_def = ecs .register_task_definition() From e9595c3360873c137a0b4f1fe3c654ee79c0fbdd Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 22:57:51 -0400 Subject: [PATCH 07/23] fix(apply): inject config as base64 env var instead of S3 download --- operator/Cargo.toml | 1 + operator/src/apply.rs | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/operator/Cargo.toml b/operator/Cargo.toml index 3550bb313..e9f9c7512 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -27,5 +27,6 @@ serde_yaml = "0.9" tokio = { version = "1.40", features = ["full"] } toml = "0.8" anyhow = "1.0" +base64 = "0.22" dirs = "6" rpassword = "7" diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 758cbab2e..63a5df6a6 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -164,13 +164,27 @@ async fn apply_ecs( KeyValuePair::builder().name("NAMESPACE").value(&m.metadata.namespace).build(), KeyValuePair::builder().name("NAME").value(&m.metadata.name).build(), ]; - if !m.spec.config_from.is_empty() { - env_vars.push(KeyValuePair::builder().name("CONFIG_S3_PATH").value(&m.spec.config_from).build()); - } if let Some(ref bootstrap) = m.spec.bootstrap_from { env_vars.push(KeyValuePair::builder().name("BOOTSTRAP_FROM").value(bootstrap).build()); } + // Read and embed config.toml as base64 env var + let has_config = !m.spec.config_from.is_empty(); + if has_config { + // Resolve config content: if S3 path, download; otherwise treat as local + let config_content = if let Some(s3_path) = m.spec.config_from.strip_prefix("s3://") { + let (bucket, key) = s3_path.split_once('/').context("invalid configFrom S3 URI")?; + let resp = s3.get_object().bucket(bucket).key(key).send().await + .context("failed to download config from S3")?; + resp.body.collect().await?.into_bytes().to_vec() + } else { + std::fs::read(&m.spec.config_from).context("failed to read local config file")? + }; + use base64::Engine; + let b64 = base64::engine::general_purpose::STANDARD.encode(&config_content); + env_vars.push(KeyValuePair::builder().name("CONFIG_B64").value(&b64).build()); + } + // 3. Build secrets from map let secrets: Vec = m .spec @@ -189,11 +203,11 @@ async fn apply_ecs( .set_environment(Some(env_vars)) .set_secrets(if secrets.is_empty() { None } else { Some(secrets) }); - if !m.spec.config_from.is_empty() { + if has_config { container_builder = container_builder .entry_point("sh") .entry_point("-c") - .command("aws s3 cp $CONFIG_S3_PATH /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); + .command("echo $CONFIG_B64 | base64 -d > /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); } let container = container_builder.build(); From 130746aab3b3336950a379693437e1177095355b Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:02:03 -0400 Subject: [PATCH 08/23] fix(apply): add awslogs log configuration to container --- operator/src/apply.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 63a5df6a6..75e6518f8 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -3,7 +3,7 @@ use crate::manifest::{OABFleetManifest, OABServiceManifest, RawManifest, Runtime use anyhow::{Context, Result}; use aws_sdk_ecs::types::{ AssignPublicIp, AwsVpcConfiguration, CapacityProviderStrategyItem, ContainerDefinition, - KeyValuePair, NetworkConfiguration, Secret, + KeyValuePair, LogConfiguration, LogDriver, NetworkConfiguration, Secret, }; use aws_sdk_s3::primitives::ByteStream; use std::path::Path; @@ -196,12 +196,20 @@ async fn apply_ecs( .collect(); // 4. Register task definition + let log_config = LogConfiguration::builder() + .log_driver(LogDriver::Awslogs) + .options("awslogs-group", "/oab/agents") + .options("awslogs-region", aws_config.region().map(|r| r.as_ref()).unwrap_or("us-east-1")) + .options("awslogs-stream-prefix", &service_name) + .build(); + let mut container_builder = ContainerDefinition::builder() .name("openab") .image(&m.spec.image) .essential(true) .set_environment(Some(env_vars)) - .set_secrets(if secrets.is_empty() { None } else { Some(secrets) }); + .set_secrets(if secrets.is_empty() { None } else { Some(secrets) }) + .log_configuration(log_config); if has_config { container_builder = container_builder From 01daf63552fd1de9923567c7dbf7fccc4c47ac65 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:03:04 -0400 Subject: [PATCH 09/23] fix(apply): fix log config build errors --- operator/src/apply.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 75e6518f8..0a8541eef 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -199,9 +199,10 @@ async fn apply_ecs( let log_config = LogConfiguration::builder() .log_driver(LogDriver::Awslogs) .options("awslogs-group", "/oab/agents") - .options("awslogs-region", aws_config.region().map(|r| r.as_ref()).unwrap_or("us-east-1")) + .options("awslogs-region", config.region().map(|r| r.as_ref()).unwrap_or("us-east-1")) .options("awslogs-stream-prefix", &service_name) - .build(); + .build() + .context("failed to build log configuration")?; let mut container_builder = ContainerDefinition::builder() .name("openab") From bfb0f7f98612b55f56ed711b1f53bec1a857aa93 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:06:35 -0400 Subject: [PATCH 10/23] fix(apply): add execution/task role ARNs and runtime platform --- operator/src/apply.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 0a8541eef..c64fe0afa 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -221,14 +221,30 @@ async fn apply_ecs( let container = container_builder.build(); + // Resolve account ID for role ARNs + let sts = aws_sdk_sts::Client::new(config); + let account_id = sts.get_caller_identity().send().await? + .account().unwrap_or_default().to_string(); + let region = config.region().map(|r| r.as_ref()).unwrap_or("us-east-1"); + let execution_role = format!("arn:aws:iam::{account_id}:role/oab-task-execution"); + let task_role = format!("arn:aws:iam::{account_id}:role/oab-task-role"); + let task_def = ecs .register_task_definition() .family(&service_name) + .execution_role_arn(&execution_role) + .task_role_arn(&task_role) .requires_compatibilities(aws_sdk_ecs::types::Compatibility::Fargate) .network_mode(aws_sdk_ecs::types::NetworkMode::Awsvpc) .cpu(&m.spec.resources.cpu) .memory(&m.spec.resources.memory) .container_definitions(container) + .runtime_platform( + aws_sdk_ecs::types::RuntimePlatform::builder() + .operating_system_family(aws_sdk_ecs::types::OsFamily::Linux) + .cpu_architecture(aws_sdk_ecs::types::CpuArchitecture::X8664) + .build() + ) .send() .await .context("failed to register task definition")?; From 92cdc64157ca16b24ae757f205fca663150f346e Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:09:43 -0400 Subject: [PATCH 11/23] fix(apply): mkdir -p /etc/openab before writing config --- operator/src/apply.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index c64fe0afa..bcc821105 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -216,7 +216,7 @@ async fn apply_ecs( container_builder = container_builder .entry_point("sh") .entry_point("-c") - .command("echo $CONFIG_B64 | base64 -d > /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); + .command("mkdir -p /etc/openab && echo $CONFIG_B64 | base64 -d > /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); } let container = container_builder.build(); From 23dce45368004d83fafa0bb52b87d372696f213a Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:13:06 -0400 Subject: [PATCH 12/23] fix(apply): use $HOME/.config/openab for config (non-root containers) --- operator/src/apply.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index bcc821105..dbec915d0 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -216,7 +216,7 @@ async fn apply_ecs( container_builder = container_builder .entry_point("sh") .entry_point("-c") - .command("mkdir -p /etc/openab && echo $CONFIG_B64 | base64 -d > /etc/openab/config.toml && exec openab run -c /etc/openab/config.toml"); + .command("mkdir -p $HOME/.config/openab && echo $CONFIG_B64 | base64 -d > $HOME/.config/openab/config.toml && exec openab run -c $HOME/.config/openab/config.toml"); } let container = container_builder.build(); @@ -225,7 +225,6 @@ async fn apply_ecs( let sts = aws_sdk_sts::Client::new(config); let account_id = sts.get_caller_identity().send().await? .account().unwrap_or_default().to_string(); - let region = config.region().map(|r| r.as_ref()).unwrap_or("us-east-1"); let execution_role = format!("arn:aws:iam::{account_id}:role/oab-task-execution"); let task_role = format!("arn:aws:iam::{account_id}:role/oab-task-role"); From 20e82858684eee3d650f0491d168a27815edc403 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:20:28 -0400 Subject: [PATCH 13/23] feat(apply): enable ECS Exec + use ecsctl wait_for_stable --- operator/src/apply.rs | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index dbec915d0..0eb0dd9d9 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -291,6 +291,7 @@ async fn apply_ecs( .service(&service_name) .task_definition(&task_def_arn) .network_configuration(network_config) + .enable_execute_command(true) .send() .await .context("failed to update ECS service")?; @@ -306,6 +307,7 @@ async fn apply_ecs( .service_name(&service_name) .task_definition(&task_def_arn) .desired_count(1) + .enable_execute_command(true) .capacity_provider_strategy(cap_strategy) .network_configuration(network_config) .send() @@ -319,30 +321,8 @@ async fn apply_ecs( if wait { eprintln!(" ⏳ Waiting for {} to stabilize...", m.metadata.name); - wait_for_stable(ecs, "oab", &service_name).await?; - eprintln!(" ✓ {} is stable", m.metadata.name); + ecsctl::apply::wait_for_stable(ecs, "oab", &service_name).await?; } Ok(()) } - -async fn wait_for_stable(ecs: &aws_sdk_ecs::Client, cluster: &str, service: &str) -> Result<()> { - for _ in 0..60 { - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - let resp = ecs.describe_services() - .cluster(cluster) - .services(service) - .send().await?; - if let Some(svc) = resp.services().first() { - let deployments = svc.deployments(); - if deployments.len() == 1 { - if let Some(d) = deployments.first() { - if d.running_count() == d.desired_count() && d.rollout_state() == Some(&aws_sdk_ecs::types::DeploymentRolloutState::Completed) { - return Ok(()); - } - } - } - } - } - anyhow::bail!("timed out waiting for service to stabilize (5 min)") -} From 653a12cdf32492284a5ee1a0e6eef436415367fc Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:23:22 -0400 Subject: [PATCH 14/23] feat(apply): register ecsctl alias for oabctl exec --- operator/src/apply.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 0eb0dd9d9..8216b7e9f 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -319,6 +319,10 @@ async fn apply_ecs( ); } + // Register alias: agent name → cluster/service/container + let alias_target = format!("oab/{}/openab", service_name); + ecsctl::alias::set(&m.metadata.name, &alias_target).await?; + if wait { eprintln!(" ⏳ Waiting for {} to stabilize...", m.metadata.name); ecsctl::apply::wait_for_stable(ecs, "oab", &service_name).await?; From 56f65a1a9e952056fef5fda82d746e237730f1f8 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:29:51 -0400 Subject: [PATCH 15/23] feat(exec): resolve agent name directly from ECS, no ecsctl aliases --- operator/src/apply.rs | 4 ---- operator/src/main.rs | 29 +++++++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 8216b7e9f..0eb0dd9d9 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -319,10 +319,6 @@ async fn apply_ecs( ); } - // Register alias: agent name → cluster/service/container - let alias_target = format!("oab/{}/openab", service_name); - ecsctl::alias::set(&m.metadata.name, &alias_target).await?; - if wait { eprintln!(" ⏳ Waiting for {} to stabilize...", m.metadata.name); ecsctl::apply::wait_for_stable(ecs, "oab", &service_name).await?; diff --git a/operator/src/main.rs b/operator/src/main.rs index c391e04e0..b86e04fe5 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -6,6 +6,7 @@ mod create; mod get; mod delete; +use anyhow::Context; use clap::{Parser, Subcommand}; #[derive(Parser)] @@ -130,11 +131,10 @@ async fn main() -> anyhow::Result<()> { delete::run(&config, &resource, &name, &cluster, &namespace).await } Commands::Exec { agent, command } => { - let resolved = ecsctl::alias::resolve(&config, &agent).await?; + let resolved = resolve_agent(&config, &agent).await?; let cmd = if command.is_empty() { None } else { - // Join args with single-quote escaping to prevent shell interpretation let joined = command.iter().map(|a| { format!("'{}'", a.replace('\'', "'\\''")) }).collect::>().join(" "); @@ -189,3 +189,28 @@ async fn main() -> anyhow::Result<()> { } } } + +/// Resolve agent name to cluster/task_id/container for ECS Exec. +/// Looks up service "oab-prod-" in cluster "oab". +async fn resolve_agent(config: &aws_config::SdkConfig, name: &str) -> anyhow::Result { + // If already in cluster/task/container format, pass through + if name.contains('/') { + return Ok(name.to_string()); + } + + let ecs = aws_sdk_ecs::Client::new(config); + let service_name = format!("oab-prod-{name}"); + + let tasks = ecs.list_tasks() + .cluster("oab") + .service_name(&service_name) + .desired_status(aws_sdk_ecs::types::DesiredStatus::Running) + .send().await + .context(format!("failed to list tasks for {service_name}"))?; + + let task_arn = tasks.task_arns().first() + .context(format!("no running tasks for agent '{name}'"))?; + + let task_id = task_arn.rsplit('/').next().unwrap_or(task_arn); + Ok(format!("oab/{task_id}/openab")) +} From 6b1ff06640fd62814974c772a1b6a74f24741cb1 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:51:06 -0400 Subject: [PATCH 16/23] fix: default cluster to 'oab', restore Exec command struct --- operator/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/operator/src/main.rs b/operator/src/main.rs index b86e04fe5..2c3b5de6a 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -48,7 +48,7 @@ enum Commands { /// Optional resource name name: Option, /// ECS cluster name - #[arg(long, default_value = "default")] + #[arg(long, default_value = "oab")] cluster: String, }, /// Delete an OAB service @@ -58,15 +58,15 @@ enum Commands { /// Resource name name: String, /// ECS cluster name - #[arg(long, default_value = "default")] + #[arg(long, default_value = "oab")] cluster: String, /// Namespace #[arg(long, default_value = "prod")] namespace: String, }, - /// Execute a command in an agent container (via ecsctl) + /// Execute a command in an agent container Exec { - /// Agent name (alias) + /// Agent name agent: String, /// Command to run (default: /bin/sh). Use -- to separate args. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] From 406287d71a8aef498d246f4464b182b4f0ac5069 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Fri, 19 Jun 2026 23:56:27 -0400 Subject: [PATCH 17/23] fix(create): reuse existing SG if duplicate --- operator/src/create.rs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/operator/src/create.rs b/operator/src/create.rs index d1227ad55..c957c7dbb 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -106,14 +106,31 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au // 8. Security group (always create a dedicated one) let sg_name = format!("oab-{name}"); - let resp = ec2.create_security_group() + let sg_id = match ec2.create_security_group() .group_name(&sg_name) .description(format!("OAB agent {name}")) .vpc_id(&vpc.id) .send().await - .context("failed to create security group")?; - let sg_id = resp.group_id().unwrap_or_default().to_string(); - eprintln!(" → Created security group: {sg_id} ({sg_name})\n"); + { + Ok(resp) => { + let id = resp.group_id().unwrap_or_default().to_string(); + eprintln!(" → Created security group: {id} ({sg_name})\n"); + id + } + Err(_) => { + // SG already exists — look it up + let existing = ec2.describe_security_groups() + .filters(aws_sdk_ec2::types::Filter::builder().name("group-name").values(&sg_name).build()) + .filters(aws_sdk_ec2::types::Filter::builder().name("vpc-id").values(&vpc.id).build()) + .send().await?; + let id = existing.security_groups().first() + .and_then(|sg| sg.group_id()) + .context("SG exists but could not be found")? + .to_string(); + eprintln!(" → Using existing security group: {id} ({sg_name})\n"); + id + } + }; // ─── Generate config.toml ────────────────────────────────────────────── let config_toml = generate_config(backend, name, namespace, stt_enabled); From bc7d99c3a4887407e1c2a82455b6878ccf4950f2 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 20 Jun 2026 00:09:01 -0400 Subject: [PATCH 18/23] fix: allow clippy too_many_arguments on generate_manifest --- operator/src/create.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/operator/src/create.rs b/operator/src/create.rs index c957c7dbb..3405a4f79 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -371,6 +371,7 @@ usercron_path = "cronjob.toml" ) } +#[allow(clippy::too_many_arguments)] fn generate_manifest(name: &str, namespace: &str, image: &str, config_from: &str, cap: &str, subnets: &[String], sg: &str, cpu: u32, memory: u32) -> String { let subnets_yaml = subnets.iter().map(|s| format!("\"{}\"", s)).collect::>().join(", "); format!( From b325bed12f2a45e3f77a7033fdc5ec7a8fd9b006 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Sat, 20 Jun 2026 04:20:29 +0000 Subject: [PATCH 19/23] fix(review): address PR review findings - F1: SG creation now matches InvalidGroup.Duplicate specifically, propagates other errors - F2: CONFIG_B64 size check (8KB limit) with clear error message - F3: IAM role pre-flight validation before register_task_definition - F4: Replace too_many_arguments with ManifestParams struct --- operator/src/apply.rs | 9 +++++++++ operator/src/create.rs | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index 0eb0dd9d9..d2639a99d 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -182,6 +182,9 @@ async fn apply_ecs( }; use base64::Engine; let b64 = base64::engine::general_purpose::STANDARD.encode(&config_content); + if b64.len() > 8192 { + anyhow::bail!("config.toml too large for env injection ({} bytes encoded, max 8192). Use S3 sidecar pattern instead.", b64.len()); + } env_vars.push(KeyValuePair::builder().name("CONFIG_B64").value(&b64).build()); } @@ -228,6 +231,12 @@ async fn apply_ecs( let execution_role = format!("arn:aws:iam::{account_id}:role/oab-task-execution"); let task_role = format!("arn:aws:iam::{account_id}:role/oab-task-role"); + let iam = aws_sdk_iam::Client::new(config); + iam.get_role().role_name("oab-task-execution").send().await + .context("IAM role 'oab-task-execution' not found — run `oabctl bootstrap` first")?; + iam.get_role().role_name("oab-task-role").send().await + .context("IAM role 'oab-task-role' not found — run `oabctl bootstrap` first")?; + let task_def = ecs .register_task_definition() .family(&service_name) diff --git a/operator/src/create.rs b/operator/src/create.rs index 3405a4f79..5657d69f3 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -117,7 +117,13 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au eprintln!(" → Created security group: {id} ({sg_name})\n"); id } - Err(_) => { + Err(e) => { + let is_duplicate = e.as_service_error() + .map(|se| se.code() == Some("InvalidGroup.Duplicate")) + .unwrap_or(false); + if !is_duplicate { + return Err(anyhow::anyhow!("failed to create security group: {e}")); + } // SG already exists — look it up let existing = ec2.describe_security_groups() .filters(aws_sdk_ec2::types::Filter::builder().name("group-name").values(&sg_name).build()) @@ -149,7 +155,10 @@ pub async fn run(config: &aws_config::SdkConfig, name: &str, namespace: &str, au std::fs::write(format!("{dir}/config.toml"), &config_toml)?; let subnet_ids: Vec = subnets.iter().map(|s| s.id.clone()).collect(); - let manifest_yaml = generate_manifest(name, namespace, &image, &config_from, capacity_provider, &subnet_ids, &sg_id, cpu, memory); + let manifest_yaml = generate_manifest(&ManifestParams { + name, namespace, image: &image, config_from: &config_from, + cap: capacity_provider, subnets: &subnet_ids, sg: &sg_id, cpu, memory, + }); std::fs::write(format!("{dir}/manifest.yaml"), &manifest_yaml)?; // ─── Summary ─────────────────────────────────────────────────────────── @@ -371,9 +380,20 @@ usercron_path = "cronjob.toml" ) } -#[allow(clippy::too_many_arguments)] -fn generate_manifest(name: &str, namespace: &str, image: &str, config_from: &str, cap: &str, subnets: &[String], sg: &str, cpu: u32, memory: u32) -> String { - let subnets_yaml = subnets.iter().map(|s| format!("\"{}\"", s)).collect::>().join(", "); +struct ManifestParams<'a> { + name: &'a str, + namespace: &'a str, + image: &'a str, + config_from: &'a str, + cap: &'a str, + subnets: &'a [String], + sg: &'a str, + cpu: u32, + memory: u32, +} + +fn generate_manifest(p: &ManifestParams) -> String { + let subnets_yaml = p.subnets.iter().map(|s| format!("\"{}\"", s)).collect::>().join(", "); format!( r#"apiVersion: oab.dev/v2 kind: OABService @@ -392,7 +412,15 @@ spec: networking: subnets: [{subnets_yaml}] securityGroups: ["{sg}"] -"# +"#, + name = p.name, + namespace = p.namespace, + image = p.image, + cpu = p.cpu, + memory = p.memory, + config_from = p.config_from, + cap = p.cap, + sg = p.sg, ) } From 7b2d4aa97fe0b7d14c3568fdb29d16dcb4101854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Sat, 20 Jun 2026 14:31:25 +0000 Subject: [PATCH 20/23] fix(review): add ProvideErrorMetadata import, document arch and role assumptions --- operator/src/apply.rs | 2 ++ operator/src/create.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index d2639a99d..e8fb10bcf 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -225,6 +225,7 @@ async fn apply_ecs( let container = container_builder.build(); // Resolve account ID for role ARNs + // NOTE: Role names must match those created by `oabctl bootstrap` let sts = aws_sdk_sts::Client::new(config); let account_id = sts.get_caller_identity().send().await? .account().unwrap_or_default().to_string(); @@ -250,6 +251,7 @@ async fn apply_ecs( .runtime_platform( aws_sdk_ecs::types::RuntimePlatform::builder() .operating_system_family(aws_sdk_ecs::types::OsFamily::Linux) + // TODO: make configurable via manifest spec.resources.arch for ARM64/Graviton .cpu_architecture(aws_sdk_ecs::types::CpuArchitecture::X8664) .build() ) diff --git a/operator/src/create.rs b/operator/src/create.rs index 5657d69f3..5ac231cae 100644 --- a/operator/src/create.rs +++ b/operator/src/create.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use aws_sdk_ec2::Client as Ec2Client; +use aws_sdk_ec2::error::ProvideErrorMetadata; use aws_sdk_s3::Client as S3Client; use aws_sdk_secretsmanager::Client as SmClient; use std::io::{self, Write}; From 97d1875897bfa6ae62b0a988dd0ba8a4571b48f5 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 22 Jun 2026 18:05:50 +0000 Subject: [PATCH 21/23] fix(review): address PR review findings - Fix cp/sync broken by alias removal: use resolve_remote_path with dynamic ECS task lookup instead of ecsctl::alias::resolve_remote - Add --cluster/--namespace to Exec, Cp, Sync commands (default: oab/prod) - Update resolve_agent to accept cluster/namespace parameters - Handle iam:GetRole AccessDenied gracefully (warn + proceed) --- operator/src/apply.rs | 29 ++++++++++++++++++---- operator/src/main.rs | 56 +++++++++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index e8fb10bcf..da12de37b 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -233,10 +233,8 @@ async fn apply_ecs( let task_role = format!("arn:aws:iam::{account_id}:role/oab-task-role"); let iam = aws_sdk_iam::Client::new(config); - iam.get_role().role_name("oab-task-execution").send().await - .context("IAM role 'oab-task-execution' not found — run `oabctl bootstrap` first")?; - iam.get_role().role_name("oab-task-role").send().await - .context("IAM role 'oab-task-role' not found — run `oabctl bootstrap` first")?; + check_iam_role(&iam, "oab-task-execution").await?; + check_iam_role(&iam, "oab-task-role").await?; let task_def = ecs .register_task_definition() @@ -337,3 +335,26 @@ async fn apply_ecs( Ok(()) } + +/// Check IAM role existence. If AccessDenied, warn and proceed (caller may only have iam:PassRole). +/// Only fail hard on NoSuchEntity. +async fn check_iam_role(iam: &aws_sdk_iam::Client, role_name: &str) -> Result<()> { + use aws_sdk_iam::error::ProvideErrorMetadata; + match iam.get_role().role_name(role_name).send().await { + Ok(_) => Ok(()), + Err(e) => { + let code = e.as_service_error() + .and_then(|se| se.code()) + .unwrap_or_default(); + if code == "AccessDenied" || code == "AccessDeniedException" { + eprintln!(" ⚠ Cannot verify role '{}' (AccessDenied) — proceeding anyway", role_name); + Ok(()) + } else { + Err(anyhow::anyhow!( + "IAM role '{}' not found — run `oabctl bootstrap` first ({})", + role_name, code + )) + } + } + } +} \ No newline at end of file diff --git a/operator/src/main.rs b/operator/src/main.rs index 2c3b5de6a..a3c147187 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -68,6 +68,12 @@ enum Commands { Exec { /// Agent name agent: String, + /// ECS cluster name + #[arg(long, default_value = "oab")] + cluster: String, + /// Namespace (used in service name: oab-{namespace}-{agent}) + #[arg(long, default_value = "prod")] + namespace: String, /// Command to run (default: /bin/sh). Use -- to separate args. #[arg(trailing_var_arg = true, allow_hyphen_values = true)] command: Vec, @@ -78,6 +84,12 @@ enum Commands { src: String, /// Destination path (local or agent:/path) dst: String, + /// ECS cluster name + #[arg(long, default_value = "oab")] + cluster: String, + /// Namespace (used in service name: oab-{namespace}-{agent}) + #[arg(long, default_value = "prod")] + namespace: String, }, /// Sync directories between local machine and agent containers (via ecsctl) Sync { @@ -85,6 +97,12 @@ enum Commands { src: String, /// Destination: agent:/path or local dir dst: String, + /// ECS cluster name + #[arg(long, default_value = "oab")] + cluster: String, + /// Namespace (used in service name: oab-{namespace}-{agent}) + #[arg(long, default_value = "prod")] + namespace: String, }, /// Bootstrap OAB infrastructure (cluster, IAM roles, S3, security group) Bootstrap { @@ -130,8 +148,8 @@ async fn main() -> anyhow::Result<()> { Commands::Delete { resource, name, cluster, namespace } => { delete::run(&config, &resource, &name, &cluster, &namespace).await } - Commands::Exec { agent, command } => { - let resolved = resolve_agent(&config, &agent).await?; + Commands::Exec { agent, cluster, namespace, command } => { + let resolved = resolve_agent(&config, &agent, &cluster, &namespace).await?; let cmd = if command.is_empty() { None } else { @@ -142,17 +160,17 @@ async fn main() -> anyhow::Result<()> { }; ecsctl::exec::run(&config, &resolved, cmd.as_deref()).await } - Commands::Cp { src, dst } => { - let src = ecsctl::alias::resolve_remote(&config, &src).await?; - let dst = ecsctl::alias::resolve_remote(&config, &dst).await?; + Commands::Cp { src, dst, cluster, namespace } => { + let src = resolve_remote_path(&config, &src, &cluster, &namespace).await?; + let dst = resolve_remote_path(&config, &dst, &cluster, &namespace).await?; eprintln!("⇄ Copying {} → {} ...", src, dst); ecsctl::cp::run(&config, &src, &dst, None, 60).await?; eprintln!("✓ Done"); Ok(()) } - Commands::Sync { src, dst } => { - let src = ecsctl::alias::resolve_remote(&config, &src).await?; - let dst = ecsctl::alias::resolve_remote(&config, &dst).await?; + Commands::Sync { src, dst, cluster, namespace } => { + let src = resolve_remote_path(&config, &src, &cluster, &namespace).await?; + let dst = resolve_remote_path(&config, &dst, &cluster, &namespace).await?; let src_remote = ecsctl::cp::is_remote(&src); let dst_remote = ecsctl::cp::is_remote(&dst); eprintln!("⇄ Syncing {} → {} ...", src, dst); @@ -191,18 +209,18 @@ async fn main() -> anyhow::Result<()> { } /// Resolve agent name to cluster/task_id/container for ECS Exec. -/// Looks up service "oab-prod-" in cluster "oab". -async fn resolve_agent(config: &aws_config::SdkConfig, name: &str) -> anyhow::Result { +/// Looks up service "{cluster}-{namespace}-{name}" in the given cluster. +async fn resolve_agent(config: &aws_config::SdkConfig, name: &str, cluster: &str, namespace: &str) -> anyhow::Result { // If already in cluster/task/container format, pass through if name.contains('/') { return Ok(name.to_string()); } let ecs = aws_sdk_ecs::Client::new(config); - let service_name = format!("oab-prod-{name}"); + let service_name = format!("{cluster}-{namespace}-{name}"); let tasks = ecs.list_tasks() - .cluster("oab") + .cluster(cluster) .service_name(&service_name) .desired_status(aws_sdk_ecs::types::DesiredStatus::Running) .send().await @@ -212,5 +230,17 @@ async fn resolve_agent(config: &aws_config::SdkConfig, name: &str) -> anyhow::Re .context(format!("no running tasks for agent '{name}'"))?; let task_id = task_arn.rsplit('/').next().unwrap_or(task_arn); - Ok(format!("oab/{task_id}/openab")) + Ok(format!("{cluster}/{task_id}/openab")) +} + +/// Resolve remote path "agent:/path" using dynamic ECS task lookup. +/// Local paths are returned unchanged. +async fn resolve_remote_path(config: &aws_config::SdkConfig, path: &str, cluster: &str, namespace: &str) -> anyhow::Result { + if !path.contains(':') { + return Ok(path.to_string()); + } + let (agent, remote_path) = path.split_once(':') + .context("invalid remote path format")?; + let resolved = resolve_agent(config, agent, cluster, namespace).await?; + Ok(format!("{resolved}:{remote_path}")) } From 7892b3254d0616ace907e0984391cfded05a2e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Tue, 23 Jun 2026 11:20:03 +0000 Subject: [PATCH 22/23] fix(operator): add [workspace] to isolate from root workspace Cargo discovers the root Cargo.toml as workspace root and fails because operator is not listed as a member. Adding an empty [workspace] table makes operator a standalone package. --- operator/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/operator/Cargo.toml b/operator/Cargo.toml index e9f9c7512..71f414cd0 100644 --- a/operator/Cargo.toml +++ b/operator/Cargo.toml @@ -30,3 +30,5 @@ anyhow = "1.0" base64 = "0.22" dirs = "6" rpassword = "7" + +[workspace] \ No newline at end of file From ce49f808d2c7cf6fe57c8ff5e65cfbb412bd5b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E6=B8=A1=E6=B3=95=E5=B8=AB?= Date: Tue, 23 Jun 2026 12:28:43 +0000 Subject: [PATCH 23/23] fix(operator): use config-based cluster name and consistent service prefix - resolve_agent: use 'oab-{namespace}-{name}' to match ecs_service_name() - apply_ecs: load cluster from OabConfig instead of hardcoding 'oab' --- operator/src/apply.rs | 11 +++++++---- operator/src/main.rs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/operator/src/apply.rs b/operator/src/apply.rs index da12de37b..51797f948 100644 --- a/operator/src/apply.rs +++ b/operator/src/apply.rs @@ -124,6 +124,9 @@ async fn apply_ecs( }; let service_name = m.ecs_service_name(); + let cluster = crate::config::OabConfig::load() + .map(|c| c.defaults.cluster) + .unwrap_or_else(|_| "oab".to_string()); let bucket = if let Some(b) = crate::config::OabConfig::load().ok().and_then(|c| c.bucket()) { b } else { @@ -283,7 +286,7 @@ async fn apply_ecs( // Check if service exists let existing = ecs .describe_services() - .cluster("oab") + .cluster(&cluster) .services(&service_name) .send() .await; @@ -296,7 +299,7 @@ async fn apply_ecs( if service_active { ecs.update_service() - .cluster("oab") + .cluster(&cluster) .service(&service_name) .task_definition(&task_def_arn) .network_configuration(network_config) @@ -312,7 +315,7 @@ async fn apply_ecs( .build()?; ecs.create_service() - .cluster("oab") + .cluster(&cluster) .service_name(&service_name) .task_definition(&task_def_arn) .desired_count(1) @@ -330,7 +333,7 @@ async fn apply_ecs( if wait { eprintln!(" ⏳ Waiting for {} to stabilize...", m.metadata.name); - ecsctl::apply::wait_for_stable(ecs, "oab", &service_name).await?; + ecsctl::apply::wait_for_stable(ecs, &cluster, &service_name).await?; } Ok(()) diff --git a/operator/src/main.rs b/operator/src/main.rs index a3c147187..3dc6d6cbd 100644 --- a/operator/src/main.rs +++ b/operator/src/main.rs @@ -217,7 +217,7 @@ async fn resolve_agent(config: &aws_config::SdkConfig, name: &str, cluster: &str } let ecs = aws_sdk_ecs::Client::new(config); - let service_name = format!("{cluster}-{namespace}-{name}"); + let service_name = format!("oab-{namespace}-{name}"); let tasks = ecs.list_tasks() .cluster(cluster)