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
1 change: 0 additions & 1 deletion .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,6 @@ jobs:
source_ref: ${{ github.sha }}
release_version: ${{ needs.compute-versions.outputs.docs_version }}
display_name: Dev
availability: beta
publish: true
secrets:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -758,9 +758,10 @@ jobs:
uses: ./.github/workflows/sync-docs.yml
with:
operation: sync
channel: latest
channel: stable
source_ref: ${{ needs.compute-versions.outputs.source_sha }}
release_version: ${{ needs.compute-versions.outputs.semver }}
version_slug: v${{ needs.compute-versions.outputs.semver }}
display_name: Latest (v${{ needs.compute-versions.outputs.semver }})
publish: true
secrets:
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,20 @@ Attach the providers and policy required by that workload.

### See network policy in action

Every sandbox starts with **minimal outbound access**. You open additional access with a short YAML policy that the proxy enforces at the HTTP method and path level, without restarting anything.
Every sandbox starts with **minimal outbound access**. You open additional access with a network rule that the proxy enforces at the HTTP method and path level, without restarting anything.

```bash
# 1. Create a sandbox (starts with minimal outbound access)
openshell sandbox create
openshell sandbox create --name demo

# 2. Inside the sandbox — blocked
sandbox$ curl -sS https://api.github.com/zen
curl: (56) Received HTTP code 403 from proxy after CONNECT

# 3. Back on the host — apply a read-only GitHub API policy
# 3. Back on the host — add a read-only GitHub API rule
sandbox$ exit
openshell policy set demo --policy examples/sandbox-policy-quickstart/policy.yaml --wait
openshell policy update demo --rule-name github_api --binary /usr/bin/curl \
--add-endpoint api.github.com:443:read-only:rest:enforce --wait

# 4. Reconnect — GET allowed, POST blocked by L7
openshell sandbox connect demo
Expand Down
5 changes: 5 additions & 0 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ OpenShell builds these main artifacts:
| VM driver/runtime assets | `crates/openshell-driver-vm` |
| Published docs site | `docs/` rendered by Fern config in `fern/` |

Release Tag publishes the same tagged docs commit as an immutable `vX.Y.Z`
snapshot and the mutable `latest` alias. Release Dev updates `dev`. The Fern
selector pins `latest` and `dev`, then lists versioned snapshots newest first.
The current release workflows do not request availability badges.

Workload images are standard OCI images supplied by operators or users.

## Build Features
Expand Down
141 changes: 120 additions & 21 deletions crates/openshell-driver-kubernetes/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3719,7 +3719,11 @@ impl KubernetesComputeDriver {
else {
continue;
};
if !sandbox_runtime_namespace_fence_generation_matches(&fence, &object) {
let workload_may_run = sandbox_runtime_should_run(&object)
|| sandbox_runtime_bootstrap_in_progress(&object);
if workload_may_run
&& !sandbox_runtime_namespace_fence_generation_matches(&fence, &object)
{
warn!(
sandbox_id,
"namespace workload fence generation changed; suspending stale boundary"
Expand Down Expand Up @@ -3751,7 +3755,8 @@ impl KubernetesComputeDriver {
)
.await
{
SandboxRuntimeControlAvailability::Available => {}
SandboxRuntimeControlAvailability::Available
| SandboxRuntimeControlAvailability::Degraded => {}
SandboxRuntimeControlAvailability::Unavailable => {
warn!(
sandbox_id,
Expand All @@ -3777,7 +3782,8 @@ impl KubernetesComputeDriver {
)
.await
{
SandboxRuntimeControlAvailability::Available => {}
SandboxRuntimeControlAvailability::Available
| SandboxRuntimeControlAvailability::Degraded => {}
SandboxRuntimeControlAvailability::Unavailable => {
warn!(
sandbox_id,
Expand Down Expand Up @@ -3934,10 +3940,8 @@ impl KubernetesComputeDriver {
object: &DynamicObject,
availability: SandboxRuntimeControlAvailability,
) {
let state = match availability {
SandboxRuntimeControlAvailability::Available => "ready",
SandboxRuntimeControlAvailability::Unavailable => "unavailable",
SandboxRuntimeControlAvailability::Unknown => return,
let Some(state) = sandbox_runtime_readiness_state(availability) else {
return;
};
if object
.metadata
Expand Down Expand Up @@ -5164,23 +5168,27 @@ fn sandbox_from_object(namespace: &str, obj: DynamicObject) -> Result<(String, S
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SandboxRuntimeControlAvailability {
Available,
Degraded,
Unavailable,
Unknown,
}

fn sandbox_runtime_control_availability_from_pod(pod: &Pod) -> SandboxRuntimeControlAvailability {
if pod.metadata.deletion_timestamp.is_none()
&& pod
.status
.as_ref()
.and_then(|status| status.conditions.as_ref())
.is_some_and(|conditions| {
conditions
.iter()
.any(|condition| condition.type_ == "Ready" && condition.status == "True")
})
{
if pod.metadata.deletion_timestamp.is_some() {
return SandboxRuntimeControlAvailability::Unavailable;
}
let status = pod.status.as_ref();
let ready = status
.and_then(|status| status.conditions.as_ref())
.is_some_and(|conditions| {
conditions
.iter()
.any(|condition| condition.type_ == "Ready" && condition.status == "True")
});
if ready {
SandboxRuntimeControlAvailability::Available
} else if status.and_then(|status| status.phase.as_deref()) == Some("Running") {
SandboxRuntimeControlAvailability::Degraded
} else {
SandboxRuntimeControlAvailability::Unavailable
}
Expand Down Expand Up @@ -5277,16 +5285,34 @@ async fn sandbox_runtime_control_availability(
SandboxRuntimeControlAvailability::Unknown
}
};
let dependencies = [control, service, fence, supervisor_fence];
combine_sandbox_runtime_control_availability([control, service, fence, supervisor_fence])
}

fn combine_sandbox_runtime_control_availability(
dependencies: [SandboxRuntimeControlAvailability; 4],
) -> SandboxRuntimeControlAvailability {
if dependencies.contains(&SandboxRuntimeControlAvailability::Unavailable) {
SandboxRuntimeControlAvailability::Unavailable
} else if dependencies.contains(&SandboxRuntimeControlAvailability::Degraded) {
SandboxRuntimeControlAvailability::Degraded
} else if dependencies.contains(&SandboxRuntimeControlAvailability::Unknown) {
SandboxRuntimeControlAvailability::Unknown
} else {
SandboxRuntimeControlAvailability::Available
}
}

fn sandbox_runtime_readiness_state(
availability: SandboxRuntimeControlAvailability,
) -> Option<&'static str> {
match availability {
SandboxRuntimeControlAvailability::Available => Some("ready"),
SandboxRuntimeControlAvailability::Degraded
| SandboxRuntimeControlAvailability::Unavailable => Some("unavailable"),
SandboxRuntimeControlAvailability::Unknown => None,
}
}

async fn sandbox_runtime_workload_generation_matches(
client: &Client,
namespace: &str,
Expand Down Expand Up @@ -5395,8 +5421,11 @@ async fn sandbox_from_object_with_sandbox_runtime_readiness(
// A transient API read failure is not evidence that the running
// boundary disappeared. Preserve the CR's published readiness for
// Unknown and let periodic reconciliation retry.
if dependencies == SandboxRuntimeControlAvailability::Unavailable
|| workload_generation == SandboxRuntimeControlAvailability::Unavailable
if matches!(
dependencies,
SandboxRuntimeControlAvailability::Unavailable
| SandboxRuntimeControlAvailability::Degraded
) || workload_generation == SandboxRuntimeControlAvailability::Unavailable
|| supervisor_generation == SandboxRuntimeControlAvailability::Unavailable
{
mark_sandbox_runtime_control_unavailable(&mut sandbox);
Expand Down Expand Up @@ -11373,6 +11402,76 @@ mod tests {
);
}

#[test]
fn sandbox_runtime_degraded_supervisor_is_not_masked_by_unknown_dependency() {
use SandboxRuntimeControlAvailability::{Available, Degraded, Unavailable, Unknown};

let combined =
combine_sandbox_runtime_control_availability([Degraded, Unknown, Available, Available]);
assert_eq!(combined, Degraded);
assert_eq!(
sandbox_runtime_readiness_state(combined),
Some("unavailable")
);

assert_eq!(
combine_sandbox_runtime_control_availability([
Degraded,
Unknown,
Unavailable,
Available
]),
Unavailable
);
let unknown = combine_sandbox_runtime_control_availability([
Available, Unknown, Available, Available,
]);
assert_eq!(unknown, Unknown);
assert_eq!(sandbox_runtime_readiness_state(unknown), None);
assert_eq!(
combine_sandbox_runtime_control_availability([Available; 4]),
Available
);
}

#[test]
fn sandbox_runtime_unready_running_supervisor_is_degraded_not_unavailable() {
let pod_with = |status: serde_json::Value| Pod {
status: Some(serde_json::from_value(status).expect("valid Pod status")),
..Default::default()
};

let reconnecting = pod_with(serde_json::json!({
"phase": "Running",
"conditions": [{"type": "Ready", "status": "False"}]
}));
assert_eq!(
sandbox_runtime_control_availability_from_pod(&reconnecting),
SandboxRuntimeControlAvailability::Degraded
);

for phase in ["Failed", "Succeeded", "Pending"] {
let pod = pod_with(serde_json::json!({
"phase": phase,
"conditions": [{"type": "Ready", "status": "False"}]
}));
assert_eq!(
sandbox_runtime_control_availability_from_pod(&pod),
SandboxRuntimeControlAvailability::Unavailable,
"phase {phase}"
);
}

let mut deleting = reconnecting;
deleting.metadata.deletion_timestamp = Some(
k8s_openapi::apimachinery::pkg::apis::meta::v1::Time(k8s_openapi::chrono::Utc::now()),
);
assert_eq!(
sandbox_runtime_control_availability_from_pod(&deleting),
SandboxRuntimeControlAvailability::Unavailable
);
}

#[test]
fn sandbox_runtime_readiness_transitions_bump_the_watched_cr() {
let unavailable = sandbox_runtime_readiness_transition_patch("42", "unavailable");
Expand Down
Loading
Loading