Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
439 changes: 439 additions & 0 deletions crates/nemoclaw-provider/tests/contract/kubernetes_lifecycle.rs

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions crates/nemoclaw-provider/tests/contract/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Contract tests: each nemoclaw type through pinned OpenTofu against fake
//! Docker engines, gateways, and model servers. `every_type_has_a_contract_test`
//! requires one for every type the provider serves.
//! Docker engines, gateways, model servers, and Kubernetes APIs.
//! `every_type_has_a_contract_test` requires one for every type the provider
//! serves.

#[path = "../../../test-support/http.rs"]
mod http_fixture;
/// The in-memory Kubernetes API shared with the SDK tests, which reaches its
/// HTTP server as `transport`.
#[path = "../../../test-support/kube_api.rs"]
mod kube_api;
use http_fixture as transport;
/// Fabric catalog fixtures shared with the end-to-end tests.
#[path = "../../../nemoclaw-e2e/src/image_runtime.rs"]
#[allow(dead_code)]
Expand All @@ -19,6 +25,7 @@ mod gateway_readiness;
mod gateway_storage;
mod inference_capabilities;
mod kubernetes_hcl;
mod kubernetes_lifecycle;
mod provider_protocol;
mod runtime_image;
mod service_capacity;
Expand Down Expand Up @@ -75,6 +82,7 @@ fn every_type_has_a_contract_test() {
include_str!("gateway_storage.rs"),
include_str!("inference_capabilities.rs"),
include_str!("kubernetes_hcl.rs"),
include_str!("kubernetes_lifecycle.rs"),
include_str!("provider_protocol.rs"),
include_str!("runtime_image.rs"),
include_str!("service_capacity.rs"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::sync::{
atomic::{AtomicUsize, Ordering},
};

#[path = "../../../../tests/support/kube_api.rs"]
#[path = "../../../../../test-support/kube_api.rs"]
mod kube_api;
#[path = "../../../../../test-support/http.rs"]
mod transport;
Expand Down
2 changes: 1 addition & 1 deletion crates/nemoclaw-sdk/tests/kubernetes_connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! port forward and the client credentials OpenShell requires.
#![cfg(unix)]

use crate::kube_api::{Objects, client};
use crate::{kube_api::Objects, kube_client::client};
use base64::{Engine, engine::general_purpose::STANDARD};
use nemoclaw_sdk::{
Error,
Expand Down
2 changes: 1 addition & 1 deletion crates/nemoclaw-sdk/tests/kubernetes_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Kubernetes preparation and observation around the native Helm release.
#![cfg(unix)]

use crate::kube_api::{Objects, client};
use crate::{kube_api::Objects, kube_client::client};
use nemoclaw_sdk::{
ObservationError,
kubernetes::{
Expand Down
28 changes: 27 additions & 1 deletion crates/nemoclaw-sdk/tests/kubernetes_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
//! Storage install: the namespace, prerequisite check and credential key.

use crate::kube_api::{Objects, client};
use crate::{kube_api::Objects, kube_client::client};
use nemoclaw_sdk::{
ObservationError,
kubernetes::{
Expand Down Expand Up @@ -259,3 +259,29 @@ async fn a_dropped_connection_during_the_storage_class_list_is_a_transport_failu
Err(ObservationError::Transport)
);
}

/// The in-memory API reports an absent object as missing, even one whose name
/// ends in `s` like a collection's.
#[tokio::test]
async fn the_in_memory_api_reports_absent_objects_as_missing() {
use nemoclaw_sdk::kubernetes::cluster::Owned;
let objects = cluster(true);
let fixture = objects.serve().await;
let cluster = Cluster::new(client(&fixture), OWNER, "generation-1");
for (kind, namespace, name) in [
("Namespace", "", "agents"),
("Secret", "agents", "credentials"),
] {
let owned = Owned {
api_version: "v1".into(),
kind: kind.into(),
namespace: namespace.into(),
name: name.into(),
uid: String::new(),
};
assert!(
cluster.get(&owned).await.unwrap().is_none(),
"{kind} {name}"
);
}
}
4 changes: 3 additions & 1 deletion crates/nemoclaw-sdk/tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

#[path = "support/examples.rs"]
mod examples;
#[path = "support/kube_api.rs"]
#[path = "../../test-support/kube_api.rs"]
mod kube_api;
#[path = "support/kube_client.rs"]
mod kube_client;
#[path = "support/provider_scope.rs"]
mod provider_scope;
#[path = "support/config.rs"]
Expand Down
11 changes: 11 additions & 0 deletions crates/nemoclaw-sdk/tests/support/kube_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! A Kubernetes client for the in-memory API server in `kube_api`.

use super::transport::Fixture;

/// A client for a fixture started by `Objects::serve`.
pub fn client(fixture: &Fixture) -> kube::Client {
let _ = rustls::crypto::ring::default_provider().install_default();
kube::Client::try_from(kube::Config::new(fixture.endpoint.parse().unwrap())).unwrap()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! An in-memory Kubernetes API server for SDK tests.
//! An in-memory Kubernetes API server, shared through private source modules
//! by SDK and provider tests. Includers also include `http.rs` as
//! `transport`; this module needs no Kubernetes client crate.
//!
//! It stores objects by path and answers get, list, create (assigning a UID)
//! and delete with a UID precondition. That is enough for the SDK's cluster
Expand Down Expand Up @@ -61,6 +63,22 @@ fn path(object: &Value) -> String {
)
}

/// Whether `path` names a collection rather than one object: after the
/// `/api/VERSION` or `/apis/GROUP/VERSION` prefix, a collection is
/// `PLURAL` or `namespaces/NAMESPACE/PLURAL`.
fn is_collection(path: &str) -> bool {
let segments: Vec<_> = path.trim_start_matches('/').split('/').collect();
let prefix = if segments.first() == Some(&"apis") {
3
} else {
2
};
matches!(
segments.get(prefix..).unwrap_or_default(),
[_] | ["namespaces", _, _]
)
}

fn status(code: u16, reason: &str) -> Option<(u16, Vec<u8>)> {
Some((
code,
Expand Down Expand Up @@ -116,12 +134,7 @@ impl Objects {
})
.map(|(_, object)| object.clone())
.collect();
if items.is_empty()
&& path
.rsplit('/')
.next()
.is_some_and(|last| !last.ends_with('s'))
{
if items.is_empty() && !is_collection(path) {
return status(404, "NotFound");
}
Some((
Expand Down Expand Up @@ -176,9 +189,3 @@ impl Objects {
.await
}
}

/// A client for a fixture started by `Objects::serve`.
pub fn client(fixture: &Fixture) -> kube::Client {
let _ = rustls::crypto::ring::default_provider().install_default();
kube::Client::try_from(kube::Config::new(fixture.endpoint.parse().unwrap())).unwrap()
}
6 changes: 3 additions & 3 deletions docs/contributing/integration-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ The mixed-search export case checks shared registrations, unused definitions, ex

## NemoClaw Provider Contract

The `nemoclaw-provider` crate's `contract` tests run its types through OpenTofu against fake Docker engines, gateways, and model servers:
The `nemoclaw-provider` crate's `contract` tests run its types through OpenTofu against fake Docker engines, gateways, model servers, and Kubernetes APIs:

```sh
NEMOCLAW_TEST_TOFU=/absolute/path/to/tofu \
Expand All @@ -86,8 +86,8 @@ NEMOCLAW_TEST_BUNDLE=/absolute/path/to/bundle \
```

They cover service storage, Kubernetes planning, runtime images and runtime contracts, gateway readiness, and engine and hardware discovery.
`every_type_has_a_contract_test` requires a contract test for every type the provider serves.
Its `ELSEWHERE` list names the types still tested only in `nemoclaw-e2e` and why: the managed gateway, service capacity, and service readiness tests need that crate's fixture binaries, gateway storage needs Docker, and inference capabilities are read only through SDK deployments.
`kubernetes_lifecycle` applies, refreshes, and tears down the Kubernetes resources against the in-memory API in `crates/test-support/kube_api.rs`, which the SDK tests share; the test writes and deletes the objects Helm would, and the live Kind suite runs the chart itself.
`every_type_has_a_contract_test` requires a contract test for every type the provider serves, or an `ELSEWHERE` entry naming where it is tested instead.

## OpenShell Provider Contract

Expand Down
Loading