Skip to content

Commit 6a95d8a

Browse files
committed
feat(sandbox): add label selectors to template listing
Signed-off-by: Gordon Sim <gsim@redhat.com>
1 parent d35122e commit 6a95d8a

20 files changed

Lines changed: 239 additions & 18 deletions

File tree

.agents/skills/openshell-cli/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ openshell sandbox template create gpu-kata \
286286
--label team=runtime \
287287
--env FEATURE_FLAG=on
288288
openshell sandbox template list
289+
openshell sandbox template list --label-selector team=runtime
289290
openshell sandbox template list --all-workspaces --output json
290291
openshell sandbox template get gpu-kata
291292
openshell sandbox template delete gpu-kata

.agents/skills/openshell-cli/cli-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ List sandbox workload templates.
285285
|------|---------|-------------|
286286
| `--limit <N>` | 100 | Maximum templates |
287287
| `--offset <N>` | 0 | Pagination offset |
288+
| `--label-selector <KEY=VALUE,...>` | empty | Filter templates by labels |
288289
| `--names` | false | Print only template names |
289290
| `--all-workspaces` | false | List templates across all workspaces; requires platform-admin permissions |
290291
| `--output table|yaml|json` | `table` | Output format |

crates/openshell-cli/src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1800,6 +1800,10 @@ enum SandboxTemplateCommands {
18001800
#[arg(long, default_value_t = 0)]
18011801
offset: u32,
18021802

1803+
/// Filter templates by labels, e.g. env=prod,team=runtime.
1804+
#[arg(long)]
1805+
label_selector: Option<String>,
1806+
18031807
/// Print only template names (one per line).
18041808
#[arg(long, conflicts_with = "output")]
18051809
names: bool,
@@ -3464,6 +3468,7 @@ async fn run_async() -> Result<()> {
34643468
SandboxTemplateCommands::List {
34653469
limit,
34663470
offset,
3471+
label_selector,
34673472
names,
34683473
output,
34693474
all_workspaces,
@@ -3472,6 +3477,7 @@ async fn run_async() -> Result<()> {
34723477
endpoint,
34733478
limit,
34743479
offset,
3480+
label_selector.as_deref(),
34753481
names,
34763482
output.as_str(),
34773483
&cli.workspace,
@@ -5666,6 +5672,8 @@ mod tests {
56665672
"list",
56675673
"--names",
56685674
"--all-workspaces",
5675+
"--label-selector",
5676+
"team=runtime",
56695677
"--limit",
56705678
"25",
56715679
"--offset",
@@ -5679,6 +5687,7 @@ mod tests {
56795687
Some(SandboxCommands::Template(SandboxTemplateCommands::List {
56805688
limit,
56815689
offset,
5690+
label_selector,
56825691
names,
56835692
all_workspaces,
56845693
..
@@ -5687,6 +5696,7 @@ mod tests {
56875696
}) => {
56885697
assert_eq!(limit, 25);
56895698
assert_eq!(offset, 5);
5699+
assert_eq!(label_selector.as_deref(), Some("team=runtime"));
56905700
assert!(names);
56915701
assert!(all_workspaces);
56925702
}

crates/openshell-cli/src/run.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,7 @@ pub async fn sandbox_template_list(
25532553
server: &str,
25542554
limit: u32,
25552555
offset: u32,
2556+
label_selector: Option<&str>,
25562557
names_only: bool,
25572558
output: &str,
25582559
workspace: &str,
@@ -2570,6 +2571,7 @@ pub async fn sandbox_template_list(
25702571
workspace.to_string()
25712572
},
25722573
all_workspaces,
2574+
label_selector: label_selector.unwrap_or_default().to_string(),
25732575
})
25742576
.await
25752577
.into_diagnostic()?;

crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,6 +1684,7 @@ async fn sandbox_template_list_and_delete_send_workspace_requests() {
16841684
&server.endpoint,
16851685
25,
16861686
5,
1687+
Some("team=runtime"),
16871688
false,
16881689
"table",
16891690
"default",
@@ -1702,6 +1703,7 @@ async fn sandbox_template_list_and_delete_send_workspace_requests() {
17021703
.expect("template list request should be recorded");
17031704
assert_eq!(list_request.limit, 25);
17041705
assert_eq!(list_request.offset, 5);
1706+
assert_eq!(list_request.label_selector, "team=runtime");
17051707
assert_eq!(list_request.workspace, "default");
17061708
assert!(!list_request.all_workspaces);
17071709

crates/openshell-sdk/src/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ impl OpenShellClient {
221221
offset: opts.offset,
222222
workspace: String::new(),
223223
all_workspaces: opts.all_workspaces,
224+
label_selector: opts.label_selector.clone(),
224225
};
225226
async move { grpc.list_sandbox_templates(request).await }
226227
})
@@ -708,6 +709,7 @@ impl WorkspaceScopedClient {
708709
self.workspace.clone()
709710
},
710711
all_workspaces: opts.all_workspaces,
712+
label_selector: opts.label_selector.clone(),
711713
};
712714
async move { grpc.list_sandbox_templates(request).await }
713715
})

crates/openshell-sdk/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ pub struct SandboxTemplateListOptions {
159159
pub limit: u32,
160160
/// Offset into the result list.
161161
pub offset: u32,
162+
/// Optional label selector in `key=value,key2=value2` form.
163+
pub label_selector: String,
162164
/// List templates across all workspaces.
163165
pub all_workspaces: bool,
164166
}

crates/openshell-sdk/tests/client_mock.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,7 @@ async fn sandbox_template_crud_uses_default_workspace() {
968968
.list_sandbox_templates(SandboxTemplateListOptions {
969969
limit: 10,
970970
offset: 2,
971+
label_selector: String::new(),
971972
all_workspaces: true,
972973
})
973974
.await

crates/openshell-server/src/grpc/sandbox.rs

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -781,11 +781,24 @@ pub(super) async fn handle_list_sandbox_templates(
781781
let limit = clamp_limit(request.limit, 100, MAX_PAGE_SIZE);
782782
let templates = if request.all_workspaces {
783783
require_platform_admin(&state.admin_role, &principal)?;
784-
state
785-
.store
786-
.list_all_messages::<SandboxWorkloadTemplate>(limit, request.offset)
787-
.await
788-
.map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))?
784+
if request.label_selector.is_empty() {
785+
state
786+
.store
787+
.list_all_messages::<SandboxWorkloadTemplate>(limit, request.offset)
788+
.await
789+
.map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))?
790+
} else {
791+
crate::grpc::validation::validate_label_selector(&request.label_selector)?;
792+
state
793+
.store
794+
.list_all_messages_with_selector::<SandboxWorkloadTemplate>(
795+
&request.label_selector,
796+
limit,
797+
request.offset,
798+
)
799+
.await
800+
.map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))?
801+
}
789802
} else {
790803
let authz = authorize_workspace(
791804
&state.store,
@@ -798,11 +811,27 @@ pub(super) async fn handle_list_sandbox_templates(
798811
let workspace = super::workspace::resolve_workspace(state.store.as_ref(), &authz.workspace)
799812
.await?
800813
.name;
801-
state
802-
.store
803-
.list_messages::<SandboxWorkloadTemplate>(&workspace, limit, request.offset)
804-
.await
805-
.map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))?
814+
if request.label_selector.is_empty() {
815+
state
816+
.store
817+
.list_messages::<SandboxWorkloadTemplate>(&workspace, limit, request.offset)
818+
.await
819+
.map_err(|e| Status::internal(format!("list sandbox templates failed: {e}")))?
820+
} else {
821+
crate::grpc::validation::validate_label_selector(&request.label_selector)?;
822+
state
823+
.store
824+
.list_messages_with_selector::<SandboxWorkloadTemplate>(
825+
&workspace,
826+
&request.label_selector,
827+
limit,
828+
request.offset,
829+
)
830+
.await
831+
.map_err(|e| {
832+
Status::internal(format!("list sandbox templates with selector failed: {e}"))
833+
})?
834+
}
806835
};
807836
Ok(Response::new(ListSandboxTemplatesResponse { templates }))
808837
}
@@ -4222,6 +4251,7 @@ mod tests {
42224251
offset: 0,
42234252
workspace: "default".to_string(),
42244253
all_workspaces: false,
4254+
label_selector: String::new(),
42254255
}),
42264256
)
42274257
.await
@@ -4255,6 +4285,61 @@ mod tests {
42554285
assert_eq!(missing.code(), tonic::Code::NotFound);
42564286
}
42574287

4288+
#[tokio::test]
4289+
async fn sandbox_template_list_filters_by_label_selector() {
4290+
let state = test_server_state().await;
4291+
4292+
let mut gpu = test_workload_template("gpu-kata");
4293+
gpu.metadata
4294+
.as_mut()
4295+
.expect("metadata")
4296+
.labels
4297+
.insert("team".to_string(), "runtime".to_string());
4298+
handle_create_sandbox_template(
4299+
&state,
4300+
authed_request(CreateSandboxTemplateRequest {
4301+
template: Some(gpu),
4302+
workspace: "default".to_string(),
4303+
}),
4304+
)
4305+
.await
4306+
.expect("gpu template create should succeed");
4307+
4308+
let mut cpu = test_workload_template("cpu-base");
4309+
cpu.metadata
4310+
.as_mut()
4311+
.expect("metadata")
4312+
.labels
4313+
.insert("team".to_string(), "batch".to_string());
4314+
handle_create_sandbox_template(
4315+
&state,
4316+
authed_request(CreateSandboxTemplateRequest {
4317+
template: Some(cpu),
4318+
workspace: "default".to_string(),
4319+
}),
4320+
)
4321+
.await
4322+
.expect("cpu template create should succeed");
4323+
4324+
let listed = handle_list_sandbox_templates(
4325+
&state,
4326+
authed_request(ListSandboxTemplatesRequest {
4327+
limit: 100,
4328+
offset: 0,
4329+
workspace: "default".to_string(),
4330+
all_workspaces: false,
4331+
label_selector: "team=runtime".to_string(),
4332+
}),
4333+
)
4334+
.await
4335+
.expect("template list with label selector should succeed")
4336+
.into_inner()
4337+
.templates;
4338+
4339+
assert_eq!(listed.len(), 1);
4340+
assert_eq!(listed[0].object_name(), "gpu-kata");
4341+
}
4342+
42584343
#[tokio::test]
42594344
async fn sandbox_template_create_rejects_whitespace_name() {
42604345
let state = test_server_state().await;
@@ -4279,6 +4364,7 @@ mod tests {
42794364
offset: 0,
42804365
workspace: "default".to_string(),
42814366
all_workspaces: false,
4367+
label_selector: String::new(),
42824368
}),
42834369
)
42844370
.await
@@ -4326,6 +4412,7 @@ mod tests {
43264412
offset: 0,
43274413
workspace: "beta".to_string(),
43284414
all_workspaces: false,
4415+
label_selector: String::new(),
43294416
}),
43304417
)
43314418
.await

docs/sandboxes/manage-sandboxes.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ Inspect and manage templates:
184184
185185
```shell
186186
openshell sandbox template list
187+
openshell sandbox template list --label-selector team=runtime
187188
openshell sandbox template get gpu-kata
188189
openshell sandbox template delete gpu-kata
189190
```

0 commit comments

Comments
 (0)