use crate::{
GcpHttpClient, Result,
ops::osconfig::OsconfigOps,
types::osconfig::{Inventory, PatchDeployment},
};
pub struct OsConfigClient<'a> {
ops: OsconfigOps<'a>,
}
impl<'a> OsConfigClient<'a> {
pub(crate) fn new(client: &'a GcpHttpClient) -> Self {
Self {
ops: OsconfigOps::new(client),
}
}
pub async fn list_patch_deployments(&self, project: &str) -> Result<Vec<PatchDeployment>> {
let parent = format!("projects/{}", project);
let mut all = Vec::new();
let mut page_token = String::new();
loop {
let resp = self
.ops
.list_patch_deployments(&parent, "100", &page_token)
.await?;
all.extend(resp.patch_deployments);
match resp.next_page_token {
Some(tok) if !tok.is_empty() => page_token = tok,
_ => break,
}
}
Ok(all)
}
pub async fn list_inventories(&self, project: &str, view: &str) -> Result<Vec<Inventory>> {
let parent = format!("projects/{}/locations/-/instances/-", project);
let mut all = Vec::new();
let mut page_token = String::new();
loop {
let resp = self
.ops
.list_inventories(&parent, "100", &page_token, view, "")
.await?;
all.extend(resp.inventories);
match resp.next_page_token {
Some(tok) if !tok.is_empty() => page_token = tok,
_ => break,
}
}
Ok(all)
}
pub async fn list_inventories_in_zone(
&self,
project: &str,
zone: &str,
view: &str,
) -> Result<Vec<Inventory>> {
let parent = format!("projects/{}/locations/{}/instances/-", project, zone);
let mut all = Vec::new();
let mut page_token = String::new();
loop {
let resp = self
.ops
.list_inventories(&parent, "100", &page_token, view, "")
.await?;
all.extend(resp.inventories);
match resp.next_page_token {
Some(tok) if !tok.is_empty() => page_token = tok,
_ => break,
}
}
Ok(all)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
#[tokio::test]
async fn test_list_patch_deployments() {
let mut mock = crate::MockClient::new();
mock.expect_get("/v1/projects/my-project/patchDeployments?pageSize=100")
.returning_json(json!({
"patchDeployments": [
{
"name": "projects/my-project/patchDeployments/weekly-patches",
"description": "Weekly OS patching",
"state": "ACTIVE",
"createTime": "2024-01-01T00:00:00Z",
"lastExecuteTime": "2025-02-01T00:00:00Z"
}
]
}))
.times(1);
let client = crate::GcpHttpClient::from_mock(mock);
let os = client.osconfig();
let result = os.list_patch_deployments("my-project").await;
assert!(result.is_ok());
let deployments = result.unwrap();
assert_eq!(deployments.len(), 1);
assert_eq!(
deployments[0].name,
"projects/my-project/patchDeployments/weekly-patches"
);
assert_eq!(deployments[0].state.as_deref(), Some("ACTIVE"));
assert_eq!(
deployments[0].last_execute_time.as_deref(),
Some("2025-02-01T00:00:00Z")
);
}
#[tokio::test]
async fn test_list_patch_deployments_paginated() {
let mut mock = crate::MockClient::new();
mock.expect_get(
"/v1/projects/my-project/patchDeployments?pageSize=100&pageToken=tok123",
)
.returning_json(json!({
"patchDeployments": [{"name": "projects/my-project/patchDeployments/pd-2", "state": "ACTIVE"}]
}))
.times(1);
mock.expect_get("/v1/projects/my-project/patchDeployments?pageSize=100")
.returning_json(json!({
"patchDeployments": [{"name": "projects/my-project/patchDeployments/pd-1", "state": "ACTIVE"}],
"nextPageToken": "tok123"
}))
.times(1);
let client = crate::GcpHttpClient::from_mock(mock);
let os = client.osconfig();
let result = os.list_patch_deployments("my-project").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
#[tokio::test]
async fn test_list_inventories() {
let mut mock = crate::MockClient::new();
mock.expect_get(
"/v1/projects/my-project/locations/-/instances/-/inventories?pageSize=100&view=BASIC",
)
.returning_json(json!({
"inventories": [
{
"name": "projects/my-project/locations/us-central1-a/instances/vm-1/inventory",
"updateTime": "2025-02-10T12:00:00Z",
"osInfo": {
"hostname": "vm-1",
"longName": "Debian GNU/Linux 11",
"shortName": "debian",
"version": "11",
"architecture": "x86_64",
"kernelVersion": "5.10.0"
}
}
]
}))
.times(1);
let client = crate::GcpHttpClient::from_mock(mock);
let os = client.osconfig();
let result = os.list_inventories("my-project", "BASIC").await;
assert!(result.is_ok());
let inventories = result.unwrap();
assert_eq!(inventories.len(), 1);
assert!(inventories[0].name.contains("vm-1"));
assert_eq!(
inventories[0].update_time.as_deref(),
Some("2025-02-10T12:00:00Z")
);
let os_info = inventories[0].os_info.as_ref().unwrap();
assert_eq!(os_info.hostname.as_deref(), Some("vm-1"));
assert_eq!(os_info.short_name.as_deref(), Some("debian"));
}
#[tokio::test]
async fn test_list_inventories_in_zone() {
let mut mock = crate::MockClient::new();
mock.expect_get(
"/v1/projects/my-project/locations/us-central1-a/instances/-/inventories?pageSize=100&view=BASIC",
)
.returning_json(json!({
"inventories": [
{
"name": "projects/my-project/locations/us-central1-a/instances/vm-1/inventory",
"updateTime": "2025-02-10T12:00:00Z"
}
]
}))
.times(1);
let client = crate::GcpHttpClient::from_mock(mock);
let os = client.osconfig();
let result = os
.list_inventories_in_zone("my-project", "us-central1-a", "BASIC")
.await;
assert!(result.is_ok());
let inventories = result.unwrap();
assert_eq!(inventories.len(), 1);
assert!(inventories[0].name.contains("us-central1-a"));
}
}