mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
feat(sdk/go): add Go SDK foundation, types, and sandbox client (A) (#2271)
* feat(sdk/go): add Go SDK foundation, types, and sandbox client (A) Add the Go SDK module with the full API contract and a working sandbox client as the first vertical slice. All other resource clients are present as stubs returning Unimplemented errors, to be replaced with real implementations in subsequent PRs. Contents: - Module setup (go.mod, Makefile, mise.toml) - All domain types (types/ package) - Full ClientInterface with all sub-client accessors - Shared infrastructure (errors, auth, gRPC connection, logging) - Sandbox client with converter and tests (fully functional) - Stub clients for remaining resources (exec, file, health, provider, profile, config, refresh, policy, service, ssh, tcp) Part of the Go SDK decomposition plan (#2270). Implements #2044. * fix(sdk/go): address review feedback on PR #2271 - Make scheme parsing drive transport selection: http:// uses plaintext gRPC, https:// or no scheme uses TLS. Add regression tests. - Add Resources and DriverConfig fields to SandboxTemplate and update both converter directions (SandboxFromProto/SandboxSpecToProto). - Regenerate proto bindings from current canonical proto sources to eliminate drift (SigV4/MCP fields, params matchers, reserved fields). - Run gofmt/goimports on all handwritten Go files. Signed-off-by: Roland Huß <rhuss@redhat.com> * fix(sdk/go): address principal engineer review findings - Remove dead boolCount function that would fail golangci-lint (#1) - Emit EventAdded for the first watch event instead of EventModified, matching k8s watch semantics (#7) - Add mutex locking to all mock server methods that access the shared sandboxes map, fixing latent race conditions (#12) - Skip HealthCheck integration test that calls an unimplemented stub (#13) - Scope doc.go examples: mark sections for sub-clients not yet available in this PR with "available in a future release" (#4) - Document Config.Timeout/RetryPolicy/Logger and WatchOptions fields as reserved for future use (#2, #6) Signed-off-by: Roland Huß <rhuss@redhat.com> * refactor(sdk/go): migrate mise config to centralized task include Move Go SDK mise configuration from standalone sdk/go/mise.toml into the project's centralized pattern: - Add Go tools (go, golangci-lint, protoc-gen-go, protoc-gen-go-grpc) to root mise.toml [tools] section - Create tasks/go.toml with all SDK tasks using go: namespace prefix and dir=sdk/go for working directory - Update sdk/go/Makefile to reference namespaced task names - Update proto:sync default path for monorepo layout Addresses review feedback from drew on PR #2271 regarding mise convention alignment. Signed-off-by: Roland Huß <rhuss@redhat.com> * refactor(sdk/go): remove UPSTREAM_VERSION standalone repo artifact Remove sdk/go/proto/UPSTREAM_VERSION file and its exclusion from proto:check. This was a leftover from the standalone repo prototype. In a monorepo, proto drift is detectable via git diff between sdk/go/proto/ and proto/ directly. Signed-off-by: Roland Huß <rhuss@redhat.com> * refactor(sdk/go): switch proto generation from protoc to buf Replace raw protoc invocations with buf for Go SDK proto code generation, aligning with the TS SDK approach (PR #2122). - Add repo-level buf.yaml declaring proto/ as the buf module with lint and breaking change detection config - Add sdk/go/buf.gen.yaml configuring buf to generate Go code directly from root proto/ (no more vendored .proto copies) - Delete vendored .proto source files from sdk/go/proto/ - Rewrite go:proto:gen and go:proto:check mise tasks to use buf - Remove go:proto:sync and go:proto:clean tasks (no longer needed) - Add proto target to sdk/go/Makefile - Add buf 1.72.0 to root mise.toml tool dependencies - Include options.proto in generation (was stripped from vendored copies) - Regenerate all .pb.go files via the new buf pipeline Signed-off-by: Roland Huß <rhuss@redhat.com> * test(sdk/go): add proto-converter field coverage detection Use protobuf reflection to enumerate all fields on key proto messages (SandboxSpec, SandboxTemplate, SandboxStatus, SandboxCondition, SandboxPolicy) and compare against explicit handled/skipped sets in the converter tests. Unhandled fields produce warnings (t.Log), not failures, so proto contributors are not forced to fix SDK converters in the same PR. Stale entries in the handled set (removed proto fields) do fail, since they indicate the converter references something that no longer exists. A follow-up CI workflow will create GitHub issues when converter drift lands on main. Signed-off-by: Roland Huß <rhuss@redhat.com> * fix(sdk/go): bump Go to 1.26 and fix errcheck lint violations The upstream go.mod now has `toolchain go1.26.4`, which requires Go 1.26 to build golangci-lint. Bump the mise.toml Go version from 1.25 to 1.26 and wrap deferred Close() calls in test helpers to satisfy errcheck. Assisted-By: 🤖 Claude Code * feat(sdk/go): add ObjectMeta fields (annotations, workspace, deletion_timestamp) Add three new proto ObjectMeta fields to Sandbox and Provider domain types: Annotations (map), Workspace (string), and DeletionTimestamp (*time.Time). Update converters in both directions, deep-copy maps at the proto/SDK boundary, and add TimeFromMillisPtr/MillisFromTimePtr helper functions. Assisted-By: 🤖 Claude Code * chore(sdk/go): regenerate proto bindings after rebase Pick up workspace fields from upstream PR #2445 (Wire authorization into workspace model). All request messages now include workspace parameter in the generated Go bindings. Assisted-By: 🤖 Claude Code * feat(sdk/go): add workspace scoping to all RPC interfaces Add workspace parameter to every sandbox-scoped RPC method across all interfaces (Sandbox, Exec, File, Service, SSH, TCP, Config, Policy, Provider, Profile, Refresh). The workspace string is passed as the second parameter after ctx, following the convention workspace then resource-name. Key changes: - SandboxInterface: all 10 methods gain workspace parameter - sandbox_client.go: passes Workspace field in every proto request - ListOptions: add AllWorkspaces field for cross-workspace queries - All stub interfaces updated to match new signatures - All sandbox client tests updated with "default" workspace Assisted-By: 🤖 Claude Code * chore(sdk/go): remove coverage.out from tracking Assisted-By: 🤖 Claude Code * fix(sdk/go): address review feedback from mrunalp - Add RefreshStrategyAWSStsAssumeRole to match proto enum value 6, fulfilling the "all domain types upfront" contract - Wrap context.DeadlineExceeded and context.Canceled in StatusError so IsDeadlineExceeded() and IsCancelled() helpers work correctly - Return error from mapToStruct/SandboxSpecToProto instead of silently discarding structpb.NewStruct failures on invalid template maps Signed-off-by: Roland Huss <rhuss@redhat.com> * fix(sdk/go): address remaining review items - Wire go:ci into root ci task so SDK is tested in repository CI - Fix gofmt formatting on converter files - Add goimports to mise.toml tools - Add coverage.out to .gitignore - Add Go SDK section to AGENTS.md and CONTRIBUTING.md - Add regression tests for context-error wrapping (IsDeadlineExceeded, IsCancelled) and invalid template map rejection - Remove panic from SandboxToProto, return error instead Signed-off-by: Roland Huss <rhuss@redhat.com> * fix(sdk/go): pin goimports version and update lockfile Pin goimports to 0.48.0 instead of "latest" and regenerate mise.lock to include the new entry. Signed-off-by: Roland Huss <rhuss@redhat.com> * fix(sdk/go): TLS.Insecure means skip-verify, not plaintext Align TLS.Insecure semantics with the Rust SDK: Insecure: true now uses TLS with InsecureSkipVerify (skip cert verification) instead of switching to plaintext. Only the http:// scheme triggers plaintext. This fixes token auth against dev/k3d gateways: StaticToken and RefreshableToken require transport security, which real TLS (even with InsecureSkipVerify) satisfies, but plaintext does not. For http:// + token auth (dev gateways without TLS), wrap the auth provider to override RequireTransportSecurity, matching the Rust SDK's behavior where http:// accepts any auth mode. Transport decision table (matches Rust SDK crates/openshell-sdk): http:// + any TLS config -> plaintext (TLS config ignored) https:// + Insecure: true -> TLS, skip cert verify https:// + Insecure: false -> TLS, full verification no scheme -> same as https:// Signed-off-by: Roland Huss <rhuss@redhat.com> * feat(sdk/go): add missing policy proto fields Add 6 previously silently dropped fields to the network policy types and converters, preventing security-relevant data loss on round-trip: NetworkEndpoint fields 19-23: - CredentialSigning: SigV4 re-signing mode - SigningService: AWS service name for SigV4 - SigningRegion: AWS region override for SigV4 - JsonRpcMaxBodyBytes: JSON-RPC body inspection limit - Mcp: MCP-specific policy options (new McpOptions type) L7Allow and L7DenyRule field 9: - Params: MCP params matcher map for tools/call filtering New type McpOptions with StrictToolNames and AllowAllKnownMcpMethods optional booleans matching the proto definitions. Signed-off-by: Roland Huss <rhuss@redhat.com> * fix(sdk/go): enforce coverage test and extend to policy messages Change coverage_test.go from t.Logf (silent) to t.Errorf so that unhandled proto fields fail the test immediately. Add coverage tests for NetworkEndpoint (23 fields), L7Allow (8 fields), L7DenyRule (8 fields), and McpOptions (2 fields). Any new proto field that is not in the handled set or explicitly skipped now breaks the build, closing the silent-drift gap. Signed-off-by: Roland Huss <rhuss@redhat.com> * ci(sdk/go): add Go SDK job to branch-checks workflow Add a Go SDK job to branch-checks.yml that runs mise run go:ci (lint, build, test, proto-check, docs-check) on every PR. This ensures the SDK is tested in CI, not just locally. Signed-off-by: Roland Huss <rhuss@redhat.com> * fix(sdk/go): address should-fix review items #6 Fix broken godoc examples: add workspace parameter to all method calls in doc.go that were broken after workspace scoping. #7 Add Err field to Event[T]: Watch error events now carry the underlying error instead of discarding it. #8 Separate Unauthenticated from PermissionDenied: add ErrorUnauthenticated code and IsUnauthenticated() helper. gRPC Unauthenticated (401) now maps to its own code instead of collapsing into PermissionDenied (403). #9 Add Unwrap to StatusError: replace dead Details field with Cause error field. StatusError.Unwrap() returns Cause, enabling errors.Is/As unwrapping. FromGRPCError and contextError both populate Cause. Signed-off-by: Roland Huss <rhuss@redhat.com> * ci(sdk/go): add go:format:check to CI pipeline Add gofmt format verification to go:ci. Catches unformatted Go files before they reach the PR. Fix formatting on coverage_test.go. Signed-off-by: Roland Huss <rhuss@redhat.com> * chore(sdk/go): remove Makefile in favor of mise tasks All build, lint, test, and proto-gen tasks are already defined in tasks/go.toml and invoked via mise. The Makefile was a leftover that duplicated this and raised questions in review. Signed-off-by: Roland Huß <rhuss@redhat.com> * feat(sdk/go): sync proto bindings and add credential handle support Regenerate Go proto bindings after rebase to pick up new CredentialHandle message and Provider.credential_handles and profile_workspace fields from upstream. Add domain types, converter support, and proto field coverage tests for Provider and CredentialHandle. Signed-off-by: Roland Huß <rhuss@redhat.com> * fix(sdk/go): reject plaintext auth leak and fix watch error handling Reject http:// addresses when the auth provider requires transport security instead of silently stripping the requirement. Remove the insecureAuthWrapper that overrode RequireTransportSecurity. Fix watch stream error handling: use blocking send for terminal errors so they are never silently dropped when the channel is full, and wrap mid-stream errors with converter.FromGRPCError so SDK error helpers like IsUnavailable work on watch Event.Err. Signed-off-by: Roland Huß <rhuss@redhat.com> * fix(sdk/go): address review findings from multi-agent code review - WaitReady now detects SandboxDeleting phase and returns immediately instead of polling indefinitely - Watch goroutine defers streamCancel() to prevent context leaks - Fix StopOnTerminal=false test to keep stream open (was wrong-reason pass due to stream ending, not StopOnTerminal logic) - Add EventDeleted test covering the Deleting phase branch - Add provider converter unit tests for CredentialHandle round-trip, nil handling, and empty maps Signed-off-by: Roland Huß <rhuss@redhat.com> --------- Signed-off-by: Roland Huß <rhuss@redhat.com> Signed-off-by: Roland Huss <rhuss@redhat.com>
This commit is contained in:
@@ -218,6 +218,25 @@ jobs:
|
||||
- name: Test
|
||||
run: mise run test:python
|
||||
|
||||
go:
|
||||
name: Go SDK
|
||||
needs: pr_metadata
|
||||
if: needs.pr_metadata.outputs.should_run == 'true'
|
||||
runs-on: linux-amd64-cpu8
|
||||
container:
|
||||
image: ghcr.io/nvidia/openshell/ci:latest
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Install tools
|
||||
run: mise install --locked
|
||||
|
||||
- name: Lint, build, test, proto-check
|
||||
run: mise run go:ci
|
||||
|
||||
markdown:
|
||||
name: Markdown
|
||||
needs: pr_metadata
|
||||
|
||||
@@ -64,6 +64,7 @@ pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
coverage.out
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
|
||||
@@ -204,6 +204,15 @@ ocsf_emit!(event);
|
||||
- `mise run e2e` — End-to-end tests against a running gateway. Run for infrastructure, sandbox, or policy changes.
|
||||
- `mise run ci` — Full local CI (lint + compile/type checks + tests). Run before opening a PR.
|
||||
|
||||
## Go SDK (`sdk/go/`)
|
||||
|
||||
- The Go SDK lives in `sdk/go/` with module path `github.com/NVIDIA/OpenShell/sdk/go`.
|
||||
- Run `mise run go:ci` for the full SDK CI pipeline (lint, build, test, proto-check, docs-check).
|
||||
- Proto bindings are generated with `mise run go:proto:gen` from the `.proto` files in `proto/`.
|
||||
- Domain types in `sdk/go/openshell/v1/types/` must not import proto packages.
|
||||
- Converters in `sdk/go/openshell/v1/internal/converter/` deep-copy slices and maps at boundaries.
|
||||
- Tests use bufconn for in-process gRPC and testify for assertions.
|
||||
|
||||
## Python
|
||||
|
||||
- Always use `uv` for Python commands (e.g., `uv pip install`, `uv run`, `uv venv`)
|
||||
|
||||
@@ -434,6 +434,7 @@ Bazel does not yet cover `mise run gateway`, `mise run sandbox`, `mise run e2e`,
|
||||
| --------------- | --------------------------------------------- |
|
||||
| `crates/` | Rust crates |
|
||||
| `python/` | Python SDK and bindings |
|
||||
| `sdk/go/` | Go SDK (types, gRPC clients, converters) |
|
||||
| `proto/` | Protocol buffer definitions |
|
||||
| `tasks/` | `mise` task definitions and build scripts |
|
||||
| `deploy/` | Dockerfiles, Helm chart, Kubernetes manifests |
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Repo-level buf module. Declares proto/ as the single module so buf generate,
|
||||
# buf lint, buf breaking, and the editor LSP all resolve imports the same way.
|
||||
# Code generation lives with each consumer (see sdk/go/buf.gen.yaml,
|
||||
# sdk/typescript/buf.gen.yaml); this file owns the module boundary and proto
|
||||
# validation policy.
|
||||
version: v2
|
||||
modules:
|
||||
- path: proto
|
||||
lint:
|
||||
use:
|
||||
- STANDARD
|
||||
except:
|
||||
# Flat proto/ layout: all files live in one directory with nested
|
||||
# packages (openshell.v1, openshell.sandbox.v1, ...). Adopting these
|
||||
# would require restructuring the tree into openshell/<svc>/v1/ and
|
||||
# updating every Rust/Python/TS codegen path and import.
|
||||
- DIRECTORY_SAME_PACKAGE
|
||||
- PACKAGE_DIRECTORY_MATCH
|
||||
# Established API shape: services are unsuffixed (OpenShell, not
|
||||
# OpenShellService) and RPCs reuse shared request/response messages with
|
||||
# short names. Renaming these is a breaking change across the codebase.
|
||||
- RPC_REQUEST_RESPONSE_UNIQUE
|
||||
- RPC_REQUEST_STANDARD_NAME
|
||||
- RPC_RESPONSE_STANDARD_NAME
|
||||
- SERVICE_SUFFIX
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
@@ -1,5 +1,27 @@
|
||||
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
|
||||
|
||||
[[tools.buf]]
|
||||
version = "1.72.0"
|
||||
backend = "aqua:bufbuild/buf"
|
||||
|
||||
[tools.buf."platforms.linux-arm64"]
|
||||
checksum = "sha256:7641bd7e06a37a54cbb8c789f53465899def96196ab5c08057432f781a15d517"
|
||||
url = "https://github.com/bufbuild/buf/releases/download/v1.72.0/buf-Linux-aarch64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/bufbuild/buf/releases/assets/480772526"
|
||||
provenance = "minisign"
|
||||
|
||||
[tools.buf."platforms.linux-x64"]
|
||||
checksum = "sha256:a9c6186cf6fcf062b247345e1b7b12c26f580c1b2a4bbf4d3fe080abf85ceee8"
|
||||
url = "https://github.com/bufbuild/buf/releases/download/v1.72.0/buf-Linux-x86_64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/bufbuild/buf/releases/assets/480772583"
|
||||
provenance = "minisign"
|
||||
|
||||
[tools.buf."platforms.macos-arm64"]
|
||||
checksum = "sha256:be040ae0ca381103dfda68a36738695c4db3e48de8e91412acdc3d991f39b91e"
|
||||
url = "https://github.com/bufbuild/buf/releases/download/v1.72.0/buf-Darwin-arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/bufbuild/buf/releases/assets/480772487"
|
||||
provenance = "minisign"
|
||||
|
||||
[[tools."github:EmbarkStudios/cargo-about"]]
|
||||
version = "0.8.4"
|
||||
backend = "github:EmbarkStudios/cargo-about"
|
||||
@@ -104,6 +126,38 @@ checksum = "sha256:29caf036bdbb4e6f07afea31706b6f386cb5a4db9a46a3a8b462b9b78157e
|
||||
url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.22.3/cargo-zigbuild-aarch64-apple-darwin.tar.xz"
|
||||
url_api = "https://api.github.com/repos/rust-cross/cargo-zigbuild/releases/assets/405676922"
|
||||
|
||||
[[tools.go]]
|
||||
version = "1.26.5"
|
||||
backend = "core:go"
|
||||
|
||||
[tools.go."platforms.linux-arm64"]
|
||||
checksum = "sha256:fe4789e92b1f33358680864bbe8704289e7bb5fc207d80623c308935bd696d49"
|
||||
url = "https://dl.google.com/go/go1.26.5.linux-arm64.tar.gz"
|
||||
|
||||
[tools.go."platforms.linux-x64"]
|
||||
checksum = "sha256:5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053"
|
||||
url = "https://dl.google.com/go/go1.26.5.linux-amd64.tar.gz"
|
||||
|
||||
[tools.go."platforms.macos-arm64"]
|
||||
checksum = "sha256:efb87ff28af9a188d0536ef5d42e63dd52ba8263cd7344a993cc48dd11dedb6a"
|
||||
url = "https://dl.google.com/go/go1.26.5.darwin-arm64.tar.gz"
|
||||
|
||||
[[tools."go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint"]]
|
||||
version = "2.12.2"
|
||||
backend = "go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
|
||||
|
||||
[[tools."go:golang.org/x/tools/cmd/goimports"]]
|
||||
version = "0.48.0"
|
||||
backend = "go:golang.org/x/tools/cmd/goimports"
|
||||
|
||||
[[tools."go:google.golang.org/grpc/cmd/protoc-gen-go-grpc"]]
|
||||
version = "1.6.2"
|
||||
backend = "go:google.golang.org/grpc/cmd/protoc-gen-go-grpc"
|
||||
|
||||
[[tools."go:google.golang.org/protobuf/cmd/protoc-gen-go"]]
|
||||
version = "1.36.11"
|
||||
backend = "go:google.golang.org/protobuf/cmd/protoc-gen-go"
|
||||
|
||||
[[tools.helm]]
|
||||
version = "4.2.0"
|
||||
backend = "aqua:helm/helm"
|
||||
@@ -127,14 +181,17 @@ backend = "aqua:norwoodj/helm-docs"
|
||||
[tools.helm-docs."platforms.linux-arm64"]
|
||||
checksum = "sha256:c3787212332386dcd122debef7848feb165aa701467ae3e3442df7638f3ac4e4"
|
||||
url = "https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/norwoodj/helm-docs/releases/assets/178327216"
|
||||
|
||||
[tools.helm-docs."platforms.linux-x64"]
|
||||
checksum = "sha256:a8cf72ada34fad93285ba2a452b38bdc5bd52cc9a571236244ec31022928d6cc"
|
||||
url = "https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_x86_64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/norwoodj/helm-docs/releases/assets/178327210"
|
||||
|
||||
[tools.helm-docs."platforms.macos-arm64"]
|
||||
checksum = "sha256:2d8399db5b33d240d5f8985241bcf5483563150b968e3229823822979f3e4b8b"
|
||||
url = "https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Darwin_arm64.tar.gz"
|
||||
url_api = "https://api.github.com/repos/norwoodj/helm-docs/releases/assets/178327215"
|
||||
|
||||
[[tools.k3d]]
|
||||
version = "5.8.3"
|
||||
@@ -143,14 +200,17 @@ backend = "aqua:k3d-io/k3d"
|
||||
[tools.k3d."platforms.linux-arm64"]
|
||||
checksum = "sha256:0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216"
|
||||
url = "https://github.com/k3d-io/k3d/releases/download/v5.8.3/k3d-linux-arm64"
|
||||
url_api = "https://api.github.com/repos/k3d-io/k3d/releases/assets/229450023"
|
||||
|
||||
[tools.k3d."platforms.linux-x64"]
|
||||
checksum = "sha256:dbaa79a76ace7f4ca230a1ff41dc7d8a5036a8ad0309e9c54f9bf3836dbe853e"
|
||||
url = "https://github.com/k3d-io/k3d/releases/download/v5.8.3/k3d-linux-amd64"
|
||||
url_api = "https://api.github.com/repos/k3d-io/k3d/releases/assets/229450045"
|
||||
|
||||
[tools.k3d."platforms.macos-arm64"]
|
||||
checksum = "sha256:8da468daa7dc7cf7cdd4735f90a9bb05179fa27858250f62e3d8cdf5b5ca0698"
|
||||
url = "https://github.com/k3d-io/k3d/releases/download/v5.8.3/k3d-darwin-arm64"
|
||||
url_api = "https://api.github.com/repos/k3d-io/k3d/releases/assets/229450067"
|
||||
|
||||
[[tools.kubectl]]
|
||||
version = "1.36.1"
|
||||
@@ -195,14 +255,17 @@ backend = "aqua:protocolbuffers/protobuf/protoc"
|
||||
[tools.protoc."platforms.linux-arm64"]
|
||||
checksum = "sha256:2594ff4fcae8cb57310d394d0961b236190ad9c5efbfdf1f597ea471d424fe79"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-aarch_64.zip"
|
||||
url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/350795076"
|
||||
|
||||
[tools.protoc."platforms.linux-x64"]
|
||||
checksum = "sha256:48785a926e73ffa3f68e2f22b14e7b849620c7a1d36809ac9249a5495e280323"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-linux-x86_64.zip"
|
||||
url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/350795083"
|
||||
|
||||
[tools.protoc."platforms.macos-arm64"]
|
||||
checksum = "sha256:b9576b5fa1a1ef3fe13a8c91d9d8204b46545759bea5ae155cd6ba2ea4cdaeed"
|
||||
url = "https://github.com/protocolbuffers/protobuf/releases/download/v29.6/protoc-29.6-osx-aarch_64.zip"
|
||||
url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/350795082"
|
||||
|
||||
[[tools.python]]
|
||||
version = "3.14.5"
|
||||
@@ -251,16 +314,19 @@ backend = "aqua:astral-sh/uv"
|
||||
[tools.uv."platforms.linux-arm64"]
|
||||
checksum = "sha256:55bd1c1c10ec8b95a8c184f5e18b566703c6ab105f0fc118aaa4d748aabf28e4"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491942"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.uv."platforms.linux-x64"]
|
||||
checksum = "sha256:adccf40b5d1939a5e0093081ec2307ea24235adf7c2d96b122c561fa37711c46"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-x86_64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491998"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[tools.uv."platforms.macos-arm64"]
|
||||
checksum = "sha256:ae738b5661a900579ec621d3918c0ef17bdec0da2a8a6d8b161137cd15f25414"
|
||||
url = "https://github.com/astral-sh/uv/releases/download/0.10.12/uv-aarch64-apple-darwin.tar.gz"
|
||||
url_api = "https://api.github.com/repos/astral-sh/uv/releases/assets/377491929"
|
||||
provenance = "github-attestations"
|
||||
|
||||
[[tools.zig]]
|
||||
|
||||
@@ -25,6 +25,12 @@ node = "24.15.0"
|
||||
kubectl = "1.36.1"
|
||||
uv = "0.10.12"
|
||||
protoc = "29.6"
|
||||
go = "1.26"
|
||||
"go:github.com/golangci/golangci-lint/v2/cmd/golangci-lint" = "2.12"
|
||||
"go:google.golang.org/protobuf/cmd/protoc-gen-go" = "1.36.11"
|
||||
"go:google.golang.org/grpc/cmd/protoc-gen-go-grpc" = "1.6.2"
|
||||
"go:golang.org/x/tools/cmd/goimports" = "0.48.0"
|
||||
buf = "1.72.0"
|
||||
helm = "4.2.0"
|
||||
helm-docs = "1.14.2"
|
||||
skaffold = "2.20.0"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Code generation for the Go SDK. The proto module boundary and validation
|
||||
# policy live in the repo-level buf.yaml; this template only drives generation.
|
||||
# buf compiles the module with its own compiler and runs protoc-gen-go /
|
||||
# protoc-gen-go-grpc from mise-managed binaries. Limited to the client-surface
|
||||
# closure (openshell, datamodel, sandbox, options); well-known types resolve
|
||||
# through google.golang.org/protobuf and are not generated.
|
||||
version: v2
|
||||
|
||||
inputs:
|
||||
- directory: ../../proto
|
||||
paths:
|
||||
- ../../proto/openshell.proto
|
||||
- ../../proto/datamodel.proto
|
||||
- ../../proto/sandbox.proto
|
||||
- ../../proto/options.proto
|
||||
|
||||
plugins:
|
||||
- local: protoc-gen-go
|
||||
out: .
|
||||
opt:
|
||||
- module=github.com/NVIDIA/OpenShell/sdk/go
|
||||
- Mopenshell.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1
|
||||
- Mdatamodel.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1
|
||||
- Msandbox.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1
|
||||
- Moptions.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1
|
||||
- local: protoc-gen-go-grpc
|
||||
out: .
|
||||
opt:
|
||||
- module=github.com/NVIDIA/OpenShell/sdk/go
|
||||
- Mopenshell.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1
|
||||
- Mdatamodel.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1
|
||||
- Msandbox.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1
|
||||
- Moptions.proto=github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1
|
||||
@@ -0,0 +1,22 @@
|
||||
module github.com/NVIDIA/OpenShell/sdk/go
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.26.4
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
google.golang.org/grpc v1.80.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// AuthProvider supplies per-RPC credentials. It implements the
|
||||
// grpc credentials.PerRPCCredentials interface.
|
||||
type AuthProvider = types.AuthProvider
|
||||
|
||||
type noAuth struct{}
|
||||
|
||||
// NoAuth returns an AuthProvider that sends no credentials.
|
||||
func NoAuth() AuthProvider {
|
||||
return &noAuth{}
|
||||
}
|
||||
|
||||
func (n *noAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n *noAuth) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type staticToken struct {
|
||||
token string
|
||||
}
|
||||
|
||||
// StaticToken returns an AuthProvider that sends a fixed Bearer token.
|
||||
func StaticToken(token string) AuthProvider {
|
||||
return &staticToken{token: token}
|
||||
}
|
||||
|
||||
func (s *staticToken) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
"authorization": "Bearer " + s.token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *staticToken) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// extraHeadersAuth wraps a base AuthProvider with additional static headers
|
||||
// that are merged into every GetRequestMetadata call. Extra headers take
|
||||
// precedence over base headers on key collision (case-insensitive).
|
||||
type extraHeadersAuth struct {
|
||||
base types.AuthProvider
|
||||
headers map[string]string // keys already lowercase, empty values filtered out
|
||||
}
|
||||
|
||||
// WithExtraHeaders wraps base with additional per-RPC headers. Keys are
|
||||
// normalized to lowercase per HTTP/2 (RFC 9113). Empty-string values are
|
||||
// silently dropped. The headers map is deep-copied at construction time,
|
||||
// so later mutations to the caller's map have no effect.
|
||||
//
|
||||
// Returns an error if base is nil or if headers is nil, empty, or contains
|
||||
// only empty-string values.
|
||||
func WithExtraHeaders(base AuthProvider, headers map[string]string) (AuthProvider, error) {
|
||||
if base == nil {
|
||||
return nil, errors.New("base auth provider must not be nil")
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil, errors.New("headers must not be nil or empty")
|
||||
}
|
||||
|
||||
// Deep-copy and normalize: lowercase keys, skip empty values.
|
||||
normalized := make(map[string]string, len(headers))
|
||||
for k, v := range headers {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
normalized[strings.ToLower(k)] = v
|
||||
}
|
||||
|
||||
if len(normalized) == 0 {
|
||||
return nil, errors.New("headers must contain at least one non-empty value")
|
||||
}
|
||||
|
||||
return &extraHeadersAuth{
|
||||
base: base,
|
||||
headers: normalized,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRequestMetadata merges base metadata with extra headers. Extra headers
|
||||
// win on key collision because they are applied after the base metadata.
|
||||
func (e *extraHeadersAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||
baseMD, err := e.base.GetRequestMetadata(ctx, uri...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start with base metadata (may be nil for NoAuth).
|
||||
// Normalize base keys to lowercase for case-insensitive collision.
|
||||
merged := make(map[string]string, len(baseMD)+len(e.headers))
|
||||
for k, v := range baseMD {
|
||||
merged[strings.ToLower(k)] = v
|
||||
}
|
||||
// Extra headers overwrite base on collision.
|
||||
maps.Copy(merged, e.headers)
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// RequireTransportSecurity delegates to the base auth provider.
|
||||
func (e *extraHeadersAuth) RequireTransportSecurity() bool {
|
||||
return e.base.RequireTransportSecurity()
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWithExtraHeaders_NilBase(t *testing.T) {
|
||||
_, err := WithExtraHeaders(nil, map[string]string{"x-key": "val"})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "base")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_NilHeaders(t *testing.T) {
|
||||
_, err := WithExtraHeaders(NoAuth(), nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "headers")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_EmptyHeaders(t *testing.T) {
|
||||
_, err := WithExtraHeaders(NoAuth(), map[string]string{})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "headers")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_AllEmptyValues(t *testing.T) {
|
||||
// All values are empty strings, so after filtering, headers map is empty.
|
||||
_, err := WithExtraHeaders(NoAuth(), map[string]string{"x-key": ""})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "headers")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_MergesWithBase(t *testing.T) {
|
||||
base := StaticToken("my-token")
|
||||
auth, err := WithExtraHeaders(base, map[string]string{
|
||||
"x-proxy-key": "proxy-secret",
|
||||
"x-tenant-id": "acme-corp",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Bearer my-token", md["authorization"])
|
||||
assert.Equal(t, "proxy-secret", md["x-proxy-key"])
|
||||
assert.Equal(t, "acme-corp", md["x-tenant-id"])
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_ExtraPrecedenceOnCollision(t *testing.T) {
|
||||
base := StaticToken("my-token")
|
||||
auth, err := WithExtraHeaders(base, map[string]string{
|
||||
"authorization": "Custom override-token",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Extra header wins over base.
|
||||
assert.Equal(t, "Custom override-token", md["authorization"])
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_CaseInsensitiveCollision(t *testing.T) {
|
||||
base := StaticToken("my-token")
|
||||
// "Authorization" with uppercase should still override "authorization".
|
||||
auth, err := WithExtraHeaders(base, map[string]string{
|
||||
"Authorization": "Custom override-token",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "Custom override-token", md["authorization"])
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_EmptyValueSkipped(t *testing.T) {
|
||||
base := StaticToken("my-token")
|
||||
auth, err := WithExtraHeaders(base, map[string]string{
|
||||
"x-proxy-key": "proxy-secret",
|
||||
"x-empty": "", // Should be silently skipped.
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "proxy-secret", md["x-proxy-key"])
|
||||
_, hasEmpty := md["x-empty"]
|
||||
assert.False(t, hasEmpty, "empty-string header values should be skipped")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_RequireTransportSecurity_Delegates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
base AuthProvider
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "delegates to NoAuth (false)",
|
||||
base: NoAuth(),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "delegates to StaticToken (true)",
|
||||
base: StaticToken("tok"),
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
auth, err := WithExtraHeaders(tt.base, map[string]string{"x-key": "val"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, auth.RequireTransportSecurity())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_WithNoAuth(t *testing.T) {
|
||||
auth, err := WithExtraHeaders(NoAuth(), map[string]string{
|
||||
"x-proxy-key": "proxy-secret",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// NoAuth returns nil metadata, extra headers should still appear.
|
||||
assert.Equal(t, "proxy-secret", md["x-proxy-key"])
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_BaseError_Propagated(t *testing.T) {
|
||||
base := &errAuth{err: errors.New("auth failure")}
|
||||
auth, err := WithExtraHeaders(base, map[string]string{"x-key": "val"})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = auth.GetRequestMetadata(context.Background())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "auth failure")
|
||||
}
|
||||
|
||||
func TestWithExtraHeaders_DeepCopiesHeaders(t *testing.T) {
|
||||
original := map[string]string{
|
||||
"x-key": "original-value",
|
||||
}
|
||||
auth, err := WithExtraHeaders(NoAuth(), original)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Mutate the original map after construction.
|
||||
original["x-key"] = "mutated-value"
|
||||
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// The wrapper should use the value at construction time, not the mutated value.
|
||||
assert.Equal(t, "original-value", md["x-key"])
|
||||
}
|
||||
|
||||
// errAuth is a test helper that always returns an error.
|
||||
type errAuth struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
return nil, e.err
|
||||
}
|
||||
|
||||
func (e *errAuth) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
const defaultLeeway = 10 * time.Second
|
||||
|
||||
var errNilTokenSource = errors.New("openshell: TokenSource must not be nil")
|
||||
|
||||
// RefreshOption configures the behavior of RefreshableToken.
|
||||
type RefreshOption func(*refreshConfig)
|
||||
|
||||
type refreshConfig struct {
|
||||
leeway time.Duration
|
||||
logger types.Logger
|
||||
}
|
||||
|
||||
func defaultRefreshConfig() refreshConfig {
|
||||
return refreshConfig{
|
||||
leeway: defaultLeeway,
|
||||
}
|
||||
}
|
||||
|
||||
// WithLeeway sets the duration before token expiry at which a proactive
|
||||
// refresh is triggered. Default is 10 seconds.
|
||||
func WithLeeway(d time.Duration) RefreshOption {
|
||||
return func(c *refreshConfig) {
|
||||
if d < 0 {
|
||||
d = 0
|
||||
}
|
||||
c.leeway = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogger sets the logger used for stale-token fallback warnings.
|
||||
// When not set, warnings are silently dropped.
|
||||
func WithLogger(l types.Logger) RefreshOption {
|
||||
return func(c *refreshConfig) {
|
||||
c.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
type refreshableAuth struct {
|
||||
source oauth2.TokenSource
|
||||
mu sync.RWMutex
|
||||
tok *oauth2.Token
|
||||
leeway time.Duration
|
||||
logger types.Logger
|
||||
}
|
||||
|
||||
func (r *refreshableAuth) isTokenValid() bool {
|
||||
if r.tok == nil {
|
||||
return false
|
||||
}
|
||||
if r.tok.Expiry.IsZero() {
|
||||
return true
|
||||
}
|
||||
return time.Now().Before(r.tok.Expiry.Add(-r.leeway))
|
||||
}
|
||||
|
||||
func (r *refreshableAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
// Fast path: RLock, return cached token if valid.
|
||||
r.mu.RLock()
|
||||
if r.isTokenValid() {
|
||||
tok := r.tok.AccessToken
|
||||
r.mu.RUnlock()
|
||||
return map[string]string{"authorization": "Bearer " + tok}, nil
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Slow path: Lock, re-check, fetch if still stale.
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.isTokenValid() {
|
||||
return map[string]string{"authorization": "Bearer " + r.tok.AccessToken}, nil
|
||||
}
|
||||
|
||||
newTok, err := r.source.Token()
|
||||
if err != nil {
|
||||
if r.tok != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Error(err, "token refresh failed, using cached token")
|
||||
}
|
||||
return map[string]string{"authorization": "Bearer " + r.tok.AccessToken}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if newTok == nil {
|
||||
if r.tok != nil {
|
||||
if r.logger != nil {
|
||||
r.logger.Error(errors.New("token source returned nil token"), "token refresh returned nil, using cached token")
|
||||
}
|
||||
return map[string]string{"authorization": "Bearer " + r.tok.AccessToken}, nil
|
||||
}
|
||||
return nil, errors.New("openshell: token source returned nil token")
|
||||
}
|
||||
|
||||
r.tok = newTok
|
||||
return map[string]string{"authorization": "Bearer " + r.tok.AccessToken}, nil
|
||||
}
|
||||
|
||||
func (r *refreshableAuth) RequireTransportSecurity() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// RefreshableToken returns an AuthProvider that caches tokens from src
|
||||
// and refreshes them before expiry. Concurrent callers share a single
|
||||
// refresh call (coalesced via RWMutex double-checked locking).
|
||||
func RefreshableToken(src oauth2.TokenSource, opts ...RefreshOption) (AuthProvider, error) {
|
||||
if src == nil {
|
||||
return nil, errNilTokenSource
|
||||
}
|
||||
|
||||
cfg := defaultRefreshConfig()
|
||||
for _, o := range opts {
|
||||
o(&cfg)
|
||||
}
|
||||
|
||||
return &refreshableAuth{
|
||||
source: src,
|
||||
leeway: cfg.leeway,
|
||||
logger: cfg.logger,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// mockTokenSource implements oauth2.TokenSource for testing.
|
||||
type mockTokenSource struct {
|
||||
mu sync.Mutex
|
||||
tokenFunc func() (*oauth2.Token, error)
|
||||
callCount int
|
||||
}
|
||||
|
||||
func (m *mockTokenSource) Token() (*oauth2.Token, error) {
|
||||
m.mu.Lock()
|
||||
m.callCount++
|
||||
m.mu.Unlock()
|
||||
return m.tokenFunc()
|
||||
}
|
||||
|
||||
func (m *mockTokenSource) calls() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.callCount
|
||||
}
|
||||
|
||||
// --- Phase 2 tests: constructor validation ---
|
||||
|
||||
func TestRefreshableToken_NilSource(t *testing.T) {
|
||||
_, err := RefreshableToken(nil)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "openshell: TokenSource must not be nil", err.Error())
|
||||
}
|
||||
|
||||
func TestRefreshableToken_ValidSource(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "tok", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, provider)
|
||||
}
|
||||
|
||||
// --- Phase 3 / US1 tests: automatic token refresh ---
|
||||
|
||||
func TestGetRequestMetadata_FirstCallFetchesToken(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "fresh-token", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer fresh-token", md["authorization"])
|
||||
assert.Equal(t, 1, src.calls())
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_CachedTokenNoExtraCall(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "cached-token", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer cached-token", md["authorization"])
|
||||
assert.Equal(t, 1, src.calls(), "second call should use cache, not invoke TokenSource")
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_RefreshesExpiredToken(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
if n == 1 {
|
||||
return &oauth2.Token{AccessToken: "old", Expiry: time.Now().Add(-time.Minute)}, nil
|
||||
}
|
||||
return &oauth2.Token{AccessToken: "new", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src, WithLeeway(0))
|
||||
require.NoError(t, err)
|
||||
|
||||
// First call gets the expired token, which is immediately stale.
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer old", md["authorization"])
|
||||
|
||||
// Second call should trigger a refresh since the cached token is expired.
|
||||
md, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer new", md["authorization"])
|
||||
assert.Equal(t, 2, src.calls())
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_ConcurrentSingleFlight(t *testing.T) {
|
||||
var fetchCount atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
fetchCount.Add(1)
|
||||
time.Sleep(10 * time.Millisecond) // simulate slow token fetch
|
||||
return &oauth2.Token{AccessToken: "shared-token", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
const goroutines = 1000
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(goroutines)
|
||||
results := make([]string, goroutines)
|
||||
errs := make([]error, goroutines)
|
||||
|
||||
for i := range goroutines {
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
md, e := provider.GetRequestMetadata(context.Background())
|
||||
errs[idx] = e
|
||||
if md != nil {
|
||||
results[idx] = md["authorization"]
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i := range goroutines {
|
||||
require.NoError(t, errs[i], "goroutine %d failed", i)
|
||||
assert.Equal(t, "Bearer shared-token", results[i], "goroutine %d got wrong token", i)
|
||||
}
|
||||
assert.Equal(t, int32(1), fetchCount.Load(), "expected exactly 1 TokenSource.Token() call, got %d", fetchCount.Load())
|
||||
}
|
||||
|
||||
func TestRefreshableAuth_RequireTransportSecurity(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "t"}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, provider.RequireTransportSecurity())
|
||||
}
|
||||
|
||||
// --- Phase 4 / US2 tests: graceful degradation ---
|
||||
|
||||
func TestGetRequestMetadata_RefreshFailureReturnsStaleCachedToken(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
if n == 1 {
|
||||
return &oauth2.Token{AccessToken: "stale", Expiry: time.Now().Add(-time.Minute)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("idp unavailable")
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src, WithLeeway(0))
|
||||
require.NoError(t, err)
|
||||
|
||||
// First call succeeds but returns already-expired token.
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Second call: refresh fails, should return stale token.
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer stale", md["authorization"])
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_RefreshFailureLogsWarning(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
if n == 1 {
|
||||
return &oauth2.Token{AccessToken: "stale", Expiry: time.Now().Add(-time.Minute)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("idp unavailable")
|
||||
},
|
||||
}
|
||||
|
||||
logger := &captureLogger{}
|
||||
provider, err := RefreshableToken(src, WithLeeway(0), WithLogger(logger))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _ = provider.GetRequestMetadata(context.Background())
|
||||
_, _ = provider.GetRequestMetadata(context.Background())
|
||||
|
||||
require.Len(t, logger.errors, 1)
|
||||
assert.Contains(t, logger.errors[0].msg, "token refresh failed")
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_RefreshFailureNoCachedTokenReturnsError(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return nil, fmt.Errorf("idp unavailable")
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "idp unavailable")
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_RefreshFailureNoLoggerNoPanic(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
if n == 1 {
|
||||
return &oauth2.Token{AccessToken: "stale", Expiry: time.Now().Add(-time.Minute)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("idp unavailable")
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src, WithLeeway(0))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _ = provider.GetRequestMetadata(context.Background())
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
_, _ = provider.GetRequestMetadata(context.Background())
|
||||
})
|
||||
}
|
||||
|
||||
// --- Phase 5 / US3 tests: configurable leeway ---
|
||||
|
||||
func TestGetRequestMetadata_DefaultLeewayTriggersRefresh(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
return &oauth2.Token{
|
||||
AccessToken: fmt.Sprintf("token-%d", n),
|
||||
Expiry: time.Now().Add(5 * time.Second), // within default 10s leeway
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src) // default 10s leeway
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Token expires in 5s, which is within 10s leeway, so next call should refresh.
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer token-2", md["authorization"])
|
||||
assert.Equal(t, 2, src.calls())
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_CustomLeewayTriggersRefresh(t *testing.T) {
|
||||
var callNum atomic.Int32
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
n := callNum.Add(1)
|
||||
return &oauth2.Token{
|
||||
AccessToken: fmt.Sprintf("token-%d", n),
|
||||
Expiry: time.Now().Add(25 * time.Second), // within custom 30s leeway
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src, WithLeeway(30*time.Second))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Token expires in 25s, which is within 30s leeway, so next call should refresh.
|
||||
md, err := provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer token-2", md["authorization"])
|
||||
assert.Equal(t, 2, src.calls())
|
||||
}
|
||||
|
||||
func TestGetRequestMetadata_ZeroExpiryNeverRefreshes(t *testing.T) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "forever-token"}, nil // zero Expiry
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Call multiple times; should never refresh since expiry is zero.
|
||||
for range 10 {
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, 1, src.calls(), "zero-expiry token should never be refreshed")
|
||||
}
|
||||
|
||||
// --- benchmarks ---
|
||||
|
||||
func BenchmarkGetRequestMetadata_CachedToken(b *testing.B) {
|
||||
src := &mockTokenSource{
|
||||
tokenFunc: func() (*oauth2.Token, error) {
|
||||
return &oauth2.Token{AccessToken: "bench-token", Expiry: time.Now().Add(time.Hour)}, nil
|
||||
},
|
||||
}
|
||||
provider, err := RefreshableToken(src)
|
||||
require.NoError(b, err)
|
||||
|
||||
// Prime the cache.
|
||||
_, err = provider.GetRequestMetadata(context.Background())
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for range b.N {
|
||||
_, _ = provider.GetRequestMetadata(context.Background())
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
type logEntry struct {
|
||||
err error
|
||||
msg string
|
||||
}
|
||||
|
||||
type captureLogger struct {
|
||||
mu sync.Mutex
|
||||
debugs []string
|
||||
infos []string
|
||||
errors []logEntry
|
||||
}
|
||||
|
||||
func (l *captureLogger) Debug(msg string, _ ...any) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.debugs = append(l.debugs, msg)
|
||||
}
|
||||
|
||||
func (l *captureLogger) Info(msg string, _ ...any) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.infos = append(l.infos, msg)
|
||||
}
|
||||
|
||||
func (l *captureLogger) Error(err error, msg string, _ ...any) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
l.errors = append(l.errors, logEntry{err: err, msg: msg})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNoAuth_GetRequestMetadata(t *testing.T) {
|
||||
auth := NoAuth()
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, md)
|
||||
}
|
||||
|
||||
func TestNoAuth_RequireTransportSecurity(t *testing.T) {
|
||||
auth := NoAuth()
|
||||
assert.False(t, auth.RequireTransportSecurity())
|
||||
}
|
||||
|
||||
func TestStaticToken_GetRequestMetadata(t *testing.T) {
|
||||
auth := StaticToken("my-secret-token")
|
||||
md, err := auth.GetRequestMetadata(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Bearer my-secret-token", md["authorization"])
|
||||
}
|
||||
|
||||
func TestStaticToken_RequireTransportSecurity(t *testing.T) {
|
||||
auth := StaticToken("token")
|
||||
assert.True(t, auth.RequireTransportSecurity())
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
|
||||
internalgrpc "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/grpc"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Config holds all settings needed to create a Client.
|
||||
type Config = types.Config
|
||||
|
||||
// ClientInterface defines the top-level SDK surface.
|
||||
type ClientInterface interface {
|
||||
Sandboxes() SandboxInterface
|
||||
Providers() ProviderInterface
|
||||
Services() ServiceInterface
|
||||
Exec() ExecInterface
|
||||
Files() FileInterface
|
||||
Health() HealthInterface
|
||||
SSH() SSHInterface
|
||||
TCP() TCPInterface
|
||||
Config() ConfigInterface
|
||||
Policy() PolicyInterface
|
||||
Close() error
|
||||
}
|
||||
|
||||
// SandboxInterface is defined in sandbox.go
|
||||
|
||||
// ProviderInterface is defined in provider.go
|
||||
|
||||
// ExecInterface is defined in exec.go
|
||||
|
||||
// FileInterface is defined in file.go
|
||||
|
||||
// Client implements ClientInterface. It holds a gRPC connection and provides
|
||||
// sub-client accessors following the Kubernetes client-go pattern.
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
config Config
|
||||
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
|
||||
sandboxes SandboxInterface
|
||||
providers ProviderInterface
|
||||
services ServiceInterface
|
||||
exec ExecInterface
|
||||
files FileInterface
|
||||
health HealthInterface
|
||||
ssh SSHInterface
|
||||
tcp TCPInterface
|
||||
cfg ConfigInterface
|
||||
policy PolicyInterface
|
||||
}
|
||||
|
||||
// NewClient creates a new SDK client connected to the given gateway.
|
||||
func NewClient(cfg Config) (*Client, error) {
|
||||
if cfg.Address == "" {
|
||||
return nil, &StatusError{Code: ErrorInvalidArgument, Message: "address must not be empty"}
|
||||
}
|
||||
|
||||
if cfg.Auth == nil {
|
||||
cfg.Auth = NoAuth()
|
||||
}
|
||||
|
||||
var tlsParams *internalgrpc.TLSParams
|
||||
if cfg.TLS != nil {
|
||||
tlsParams = &internalgrpc.TLSParams{
|
||||
CertFile: cfg.TLS.CertFile,
|
||||
KeyFile: cfg.TLS.KeyFile,
|
||||
CAFile: cfg.TLS.CAFile,
|
||||
Insecure: cfg.TLS.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := internalgrpc.NewConnection(cfg.Address, tlsParams, cfg.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
conn: conn,
|
||||
config: cfg,
|
||||
}
|
||||
|
||||
c.sandboxes = newSandboxClient(conn)
|
||||
c.providers = &stubProviders{}
|
||||
c.services = &stubServices{}
|
||||
c.exec = &stubExec{}
|
||||
c.files = &stubFiles{}
|
||||
c.health = &stubHealth{}
|
||||
c.ssh = &stubSSH{}
|
||||
c.tcp = &stubTCP{}
|
||||
c.cfg = &stubConfig{}
|
||||
c.policy = &stubPolicy{}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Sandboxes returns the sandbox sub-client.
|
||||
func (c *Client) Sandboxes() SandboxInterface { return c.sandboxes }
|
||||
|
||||
// Providers returns the provider sub-client.
|
||||
func (c *Client) Providers() ProviderInterface { return c.providers }
|
||||
|
||||
// Services returns the service sub-client.
|
||||
func (c *Client) Services() ServiceInterface { return c.services }
|
||||
|
||||
// Exec returns the exec sub-client.
|
||||
func (c *Client) Exec() ExecInterface { return c.exec }
|
||||
|
||||
// Files returns the file sub-client.
|
||||
func (c *Client) Files() FileInterface { return c.files }
|
||||
|
||||
// Health returns the health sub-client.
|
||||
func (c *Client) Health() HealthInterface { return c.health }
|
||||
|
||||
// SSH returns the SSH session sub-client.
|
||||
func (c *Client) SSH() SSHInterface { return c.ssh }
|
||||
|
||||
// TCP returns the TCP port forwarding sub-client.
|
||||
func (c *Client) TCP() TCPInterface { return c.tcp }
|
||||
|
||||
// Config returns the configuration sub-client.
|
||||
func (c *Client) Config() ConfigInterface { return c.cfg }
|
||||
|
||||
// Policy returns the policy management sub-client.
|
||||
func (c *Client) Policy() PolicyInterface { return c.policy }
|
||||
|
||||
// Close closes the underlying gRPC connection. Safe to call multiple times.
|
||||
func (c *Client) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.closeErr = c.conn.Close()
|
||||
})
|
||||
return c.closeErr
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewClient_EmptyAddress(t *testing.T) {
|
||||
_, err := NewClient(Config{Address: ""})
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "address")
|
||||
}
|
||||
|
||||
func TestNewClient_ValidConfig(t *testing.T) {
|
||||
client, err := NewClient(Config{
|
||||
Address: "localhost:50051",
|
||||
Auth: NoAuth(),
|
||||
TLS: &TLSConfig{Insecure: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
|
||||
assert.NotNil(t, client.Sandboxes())
|
||||
assert.NotNil(t, client.Providers())
|
||||
assert.NotNil(t, client.Exec())
|
||||
assert.NotNil(t, client.Files())
|
||||
assert.NotNil(t, client.Health())
|
||||
|
||||
err = client.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestClient_CloseIdempotent(t *testing.T) {
|
||||
client, err := NewClient(Config{
|
||||
Address: "localhost:50051",
|
||||
Auth: NoAuth(),
|
||||
TLS: &TLSConfig{Insecure: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = client.Close()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNewClient_DefaultAuth(t *testing.T) {
|
||||
client, err := NewClient(Config{
|
||||
Address: "localhost:50051",
|
||||
TLS: &TLSConfig{Insecure: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
_ = client.Close()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// SandboxConfig represents the full configuration state of a sandbox.
|
||||
type SandboxConfig = types.SandboxConfig
|
||||
|
||||
// GatewayConfig represents gateway-global settings.
|
||||
type GatewayConfig = types.GatewayConfig
|
||||
|
||||
// ConfigUpdate represents a configuration mutation request.
|
||||
type ConfigUpdate = types.ConfigUpdate
|
||||
|
||||
// ConfigUpdateResult holds the result of a configuration update operation.
|
||||
type ConfigUpdateResult = types.ConfigUpdateResult
|
||||
|
||||
// SettingValue is a typed setting value (string, bool, int64, or bytes).
|
||||
type SettingValue = types.SettingValue
|
||||
|
||||
// SettingValueType identifies which typed field of a SettingValue is active.
|
||||
type SettingValueType = types.SettingValueType
|
||||
|
||||
// EffectiveSetting is a setting value paired with its resolved scope.
|
||||
type EffectiveSetting = types.EffectiveSetting
|
||||
|
||||
// SettingScope indicates whether a setting is sandbox or global.
|
||||
type SettingScope = types.SettingScope
|
||||
|
||||
// PolicySource indicates the source of a policy payload.
|
||||
type PolicySource = types.PolicySource
|
||||
|
||||
// SettingValueType constants re-exported from types package.
|
||||
const (
|
||||
SettingValueString = types.SettingValueString
|
||||
SettingValueBool = types.SettingValueBool
|
||||
SettingValueInt = types.SettingValueInt
|
||||
SettingValueBytes = types.SettingValueBytes
|
||||
)
|
||||
|
||||
// SettingScope constants re-exported from types package.
|
||||
const (
|
||||
SettingScopeUnspecified = types.SettingScopeUnspecified
|
||||
SettingScopeSandbox = types.SettingScopeSandbox
|
||||
SettingScopeGlobal = types.SettingScopeGlobal
|
||||
)
|
||||
|
||||
// PolicySource constants re-exported from types package.
|
||||
const (
|
||||
PolicySourceUnspecified = types.PolicySourceUnspecified
|
||||
PolicySourceSandbox = types.PolicySourceSandbox
|
||||
PolicySourceGlobal = types.PolicySourceGlobal
|
||||
)
|
||||
|
||||
// ConfigInterface defines operations for reading and updating gateway and
|
||||
// sandbox configuration.
|
||||
type ConfigInterface interface {
|
||||
// GetSandbox retrieves the full configuration state for a sandbox,
|
||||
// including policy, effective settings, and revision metadata.
|
||||
// The sandbox is identified by name; the SDK resolves it to an ID internally.
|
||||
GetSandbox(ctx context.Context, workspace, sandboxName string) (*SandboxConfig, error)
|
||||
|
||||
// GetGateway retrieves gateway-global settings.
|
||||
GetGateway(ctx context.Context) (*GatewayConfig, error)
|
||||
|
||||
// Update applies a configuration mutation. For sandbox-scoped updates,
|
||||
// set ConfigUpdate.Name to the sandbox name. For global-scoped updates,
|
||||
// set ConfigUpdate.Global to true.
|
||||
Update(ctx context.Context, workspace string, update *ConfigUpdate) (*ConfigUpdateResult, error)
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package v1 provides a Go SDK for interacting with OpenShell servers.
|
||||
//
|
||||
// The SDK follows the Kubernetes client-go sub-client pattern: a single Client
|
||||
// provides typed accessors for each resource domain (Sandboxes, Providers, Exec,
|
||||
// Files, Health, Services, SSH, TCP, Config). All operations accept a context.Context and return idiomatic
|
||||
// Go types. Proto-generated types never appear in the public API.
|
||||
//
|
||||
// # Quick Start
|
||||
//
|
||||
// client, err := v1.NewClient(v1.Config{
|
||||
// Address: "gateway.example.com:443",
|
||||
// Auth: v1.StaticToken("my-token"),
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer client.Close()
|
||||
//
|
||||
// # Sandbox Lifecycle
|
||||
//
|
||||
// sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
|
||||
// Template: &v1.SandboxTemplate{Image: "python:3.12"},
|
||||
// Environment: map[string]string{"LANG": "en_US.UTF-8"},
|
||||
// }, nil)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// sandbox, err = client.Sandboxes().WaitReady(ctx, "default", sandbox.Name)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// # Command Execution (available in a future release)
|
||||
//
|
||||
// result, err := client.Exec().Run(ctx, "default", sandbox.Name, []string{"echo", "hello"}, v1.ExecOptions{})
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(string(result.Stdout)) // "hello\n"
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// _, err = client.Sandboxes().Get(ctx, "default", "missing")
|
||||
// if v1.IsNotFound(err) {
|
||||
// // handle not found
|
||||
// }
|
||||
//
|
||||
// # Watching
|
||||
//
|
||||
// watcher, err := client.Sandboxes().Watch(ctx, "default", sandbox.Name)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer watcher.Stop()
|
||||
// for event := range watcher.ResultChan() {
|
||||
// fmt.Printf("%s: %s\n", event.Type, event.Object.Name)
|
||||
// }
|
||||
//
|
||||
// # Watching with StopOnTerminal
|
||||
//
|
||||
// Use StopOnTerminal to auto-close the watcher when the sandbox reaches a
|
||||
// terminal phase (Ready or Error):
|
||||
//
|
||||
// watcher, err := client.Sandboxes().Watch(ctx, "default", sandbox.Name,
|
||||
// v1.WatchOptions{StopOnTerminal: true},
|
||||
// )
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// for event := range watcher.ResultChan() {
|
||||
// fmt.Printf("phase: %s\n", event.Object.Status.Phase)
|
||||
// }
|
||||
// // channel closes automatically after Ready or Error
|
||||
//
|
||||
// # Service Exposure (available in a future release)
|
||||
//
|
||||
// Expose an HTTP service running inside a sandbox and retrieve its public URL:
|
||||
//
|
||||
// endpoint, err := client.Services().Expose(ctx, "default", "my-sandbox", "api", 8080, true)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Service URL: %s\n", endpoint.URL)
|
||||
//
|
||||
// endpoints, err := client.Services().List(ctx, "default", "my-sandbox")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// for _, ep := range endpoints {
|
||||
// fmt.Printf(" %s → port %d (URL: %s)\n", ep.ServiceName, ep.TargetPort, ep.URL)
|
||||
// }
|
||||
//
|
||||
// # Provider Profiles (available in a future release)
|
||||
//
|
||||
// List available provider profiles and import new ones:
|
||||
//
|
||||
// profiles, err := client.Providers().Profiles().List(ctx, "default")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// for _, p := range profiles {
|
||||
// fmt.Printf("%s (%s): %s\n", p.DisplayName, p.Category, p.Description)
|
||||
// }
|
||||
//
|
||||
// result, err := client.Providers().Profiles().Import(ctx, "default", []v1.ProfileImportItem{
|
||||
// {Source: "openai-profile.yaml", Profile: v1.ProviderProfile{
|
||||
// DisplayName: "OpenAI",
|
||||
// Category: v1.ProfileCategoryInference,
|
||||
// }},
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// for _, d := range result.Diagnostics {
|
||||
// fmt.Printf("[%s] %s: %s\n", d.Severity, d.Field, d.Message)
|
||||
// }
|
||||
//
|
||||
// # Credential Refresh (available in a future release)
|
||||
//
|
||||
// Configure gateway-owned credential refresh for a provider:
|
||||
//
|
||||
// status, err := client.Providers().Refresh().Configure(ctx, "default", &v1.RefreshConfig{
|
||||
// Provider: "openai",
|
||||
// CredentialKey: "api-key",
|
||||
// Strategy: v1.RefreshStrategyOAuth2ClientCredentials,
|
||||
// Material: map[string]string{"client_id": "xxx", "client_secret": "yyy"},
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Refresh status: %s (next: %s)\n", status.Status, status.NextRefreshAt)
|
||||
//
|
||||
// # Token Refresh
|
||||
//
|
||||
// Use RefreshableToken for automatic OAuth2 token caching and refresh.
|
||||
// Concurrent callers share a single refresh call:
|
||||
//
|
||||
// tokenSource := oauth2Config.TokenSource(ctx, initialToken)
|
||||
// auth, err := v1.RefreshableToken(tokenSource,
|
||||
// v1.WithLeeway(30*time.Second),
|
||||
// )
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// client, err := v1.NewClient(v1.Config{
|
||||
// Address: "gateway.example.com:443",
|
||||
// Auth: auth,
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer client.Close()
|
||||
//
|
||||
// # Extra Headers
|
||||
//
|
||||
// Use WithExtraHeaders to attach additional per-RPC headers to any auth
|
||||
// provider. This is useful for edge proxies, API gateways, or any middleware
|
||||
// that requires custom headers alongside standard authentication:
|
||||
//
|
||||
// base := v1.StaticToken("my-token")
|
||||
// auth, err := v1.WithExtraHeaders(base, map[string]string{
|
||||
// "x-proxy-key": "proxy-secret",
|
||||
// "x-tenant-id": "acme-corp",
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// client, err := v1.NewClient(v1.Config{
|
||||
// Address: "gateway.example.com:443",
|
||||
// Auth: auth,
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer client.Close()
|
||||
//
|
||||
// Keys are normalized to lowercase (per HTTP/2 RFC 9113). On key collision,
|
||||
// extra headers take precedence over base auth headers. Empty-string values
|
||||
// are silently dropped. WithExtraHeaders composes with any AuthProvider,
|
||||
// including RefreshableToken:
|
||||
//
|
||||
// tokenSource := oauth2Config.TokenSource(ctx, initialToken)
|
||||
// refreshAuth, err := v1.RefreshableToken(tokenSource)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// auth, err := v1.WithExtraHeaders(refreshAuth, map[string]string{
|
||||
// "x-proxy-key": "proxy-secret",
|
||||
// })
|
||||
//
|
||||
// # SSH Session Management (available in a future release)
|
||||
//
|
||||
// Create an SSH session for a sandbox and use the returned connection details.
|
||||
// Note: CreateSession accepts a sandbox ID, not a name. For name-based access
|
||||
// with automatic session cleanup, prefer SSH().Tunnel() instead.
|
||||
//
|
||||
// session, err := client.SSH().CreateSession(ctx, "default", sandbox.ID)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("SSH to %s:%d (scheme: %s)\n",
|
||||
// session.GatewayHost, session.GatewayPort, session.GatewayScheme)
|
||||
// fmt.Printf("Host key: %s\n", session.HostKeyFingerprint)
|
||||
// // Use session.Token to authenticate the SSH connection.
|
||||
//
|
||||
// revoked, err := client.SSH().RevokeSession(ctx, "default", session.Token)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Session revoked: %v\n", revoked)
|
||||
//
|
||||
// # TCP Port Forwarding (available in a future release)
|
||||
//
|
||||
// Forward a local connection to a port inside a sandbox:
|
||||
//
|
||||
// conn, err := client.TCP().Forward(ctx, "default", "my-sandbox", 5432)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer conn.Close()
|
||||
//
|
||||
// // conn implements io.ReadWriteCloser, use it like a net.Conn.
|
||||
// _, err = conn.Write([]byte("PING\n"))
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// buf := make([]byte, 1024)
|
||||
// n, err := conn.Read(buf)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Response: %s\n", buf[:n])
|
||||
//
|
||||
// Use WithForwardServiceID to tag the forwarding session with a service
|
||||
// identifier for audit logging:
|
||||
//
|
||||
// conn, err := client.TCP().Forward(ctx, "default", "my-sandbox", 5432,
|
||||
// v1.WithForwardServiceID("billing-db"),
|
||||
// )
|
||||
//
|
||||
// # SSH Tunneling (available in a future release)
|
||||
//
|
||||
// Create an SSH tunnel to a sandbox port in a single call. Tunnel combines
|
||||
// session creation, TCP forwarding with an SSH relay target, and automatic
|
||||
// session cleanup into one operation:
|
||||
//
|
||||
// tunnel, err := client.SSH().Tunnel(ctx, "default", "my-sandbox", 22)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer tunnel.Close()
|
||||
//
|
||||
// // tunnel implements io.ReadWriteCloser. The underlying SSH session
|
||||
// // is automatically revoked when Close is called.
|
||||
// _, err = tunnel.Write([]byte("SSH-2.0-client\r\n"))
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// buf := make([]byte, 256)
|
||||
// n, err := tunnel.Read(buf)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Server banner: %s\n", buf[:n])
|
||||
//
|
||||
// Use WithTunnelServiceID to associate a service identifier with the tunnel:
|
||||
//
|
||||
// tunnel, err := client.SSH().Tunnel(ctx, "default", "my-sandbox", 22,
|
||||
// v1.WithTunnelServiceID("dev-ssh"),
|
||||
// )
|
||||
//
|
||||
// # Sandbox Policy
|
||||
//
|
||||
// Set an initial security policy when creating a sandbox:
|
||||
//
|
||||
// sandbox, err := client.Sandboxes().Create(ctx, "default", "secure-sandbox", &v1.SandboxSpec{
|
||||
// Template: &v1.SandboxTemplate{Image: "python:3.12"},
|
||||
// Policy: &v1.SandboxPolicy{
|
||||
// Version: 1,
|
||||
// Filesystem: &v1.FilesystemPolicy{
|
||||
// IncludeWorkdir: true,
|
||||
// ReadOnly: []string{"/usr", "/lib"},
|
||||
// },
|
||||
// Process: &v1.ProcessPolicy{
|
||||
// RunAsUser: "sandbox",
|
||||
// RunAsGroup: "sandbox",
|
||||
// },
|
||||
// NetworkPolicies: map[string]v1.NetworkPolicyRule{
|
||||
// "allow-api": {
|
||||
// Name: "allow-api",
|
||||
// Endpoints: []v1.PolicyNetworkEndpoint{
|
||||
// {Host: "api.example.com", Port: 443, Protocol: "tcp"},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// }, nil)
|
||||
//
|
||||
// Replace the full policy at runtime via configuration update (available in a future release):
|
||||
//
|
||||
// result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
|
||||
// Name: "secure-sandbox",
|
||||
// Policy: &v1.SandboxPolicy{
|
||||
// Version: 2,
|
||||
// NetworkPolicies: map[string]v1.NetworkPolicyRule{
|
||||
// "allow-all": {Name: "allow-all"},
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
//
|
||||
// Read a policy back from revision history (available in a future release):
|
||||
//
|
||||
// revisions, err := client.Policy().List(ctx, "default")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// for _, rev := range revisions {
|
||||
// if rev.Policy != nil {
|
||||
// fmt.Printf("v%d: %d network rules\n", rev.Version, len(rev.Policy.NetworkPolicies))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Configuration Management (available in a future release)
|
||||
//
|
||||
// Read sandbox and gateway configuration, and update settings:
|
||||
//
|
||||
// sbCfg, err := client.Config().GetSandbox(ctx, "default", "my-sandbox")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Config revision: %d\n", sbCfg.ConfigRevision)
|
||||
// for name, setting := range sbCfg.Settings {
|
||||
// fmt.Printf(" %s = %v (scope: %s)\n", name, setting.Value, setting.Scope)
|
||||
// }
|
||||
//
|
||||
// gwCfg, err := client.Config().GetGateway(ctx)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("Gateway settings revision: %d\n", gwCfg.SettingsRevision)
|
||||
//
|
||||
// result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
|
||||
// Name: "my-sandbox",
|
||||
// SettingKey: "max_tokens",
|
||||
// SettingValue: &v1.SettingValue{
|
||||
// Type: v1.SettingValueInt,
|
||||
// IntVal: 8192,
|
||||
// },
|
||||
// })
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("New settings revision: %d\n", result.SettingsRevision)
|
||||
package v1
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// ErrorCode classifies SDK errors by their gRPC origin.
|
||||
type ErrorCode = types.ErrorCode
|
||||
|
||||
// ErrorCode values for classifying gRPC errors.
|
||||
const (
|
||||
ErrorNotFound = types.ErrorNotFound
|
||||
ErrorAlreadyExists = types.ErrorAlreadyExists
|
||||
ErrorUnavailable = types.ErrorUnavailable
|
||||
ErrorPermissionDenied = types.ErrorPermissionDenied
|
||||
ErrorInvalidArgument = types.ErrorInvalidArgument
|
||||
ErrorDeadlineExceeded = types.ErrorDeadlineExceeded
|
||||
ErrorCancelled = types.ErrorCancelled
|
||||
ErrorInternal = types.ErrorInternal
|
||||
ErrorUnimplemented = types.ErrorUnimplemented
|
||||
ErrorConflict = types.ErrorConflict
|
||||
ErrorUnauthenticated = types.ErrorUnauthenticated
|
||||
)
|
||||
|
||||
// StatusError is the typed error returned by all SDK operations.
|
||||
type StatusError = types.StatusError
|
||||
|
||||
// IsNotFound returns true if the error indicates a resource was not found.
|
||||
func IsNotFound(err error) bool { return types.IsNotFound(err) }
|
||||
|
||||
// IsAlreadyExists returns true if the error indicates a resource already exists.
|
||||
func IsAlreadyExists(err error) bool { return types.IsAlreadyExists(err) }
|
||||
|
||||
// IsUnavailable returns true if the error indicates the service is unavailable.
|
||||
func IsUnavailable(err error) bool { return types.IsUnavailable(err) }
|
||||
|
||||
// IsPermissionDenied returns true if the error indicates insufficient permissions.
|
||||
func IsPermissionDenied(err error) bool { return types.IsPermissionDenied(err) }
|
||||
|
||||
// IsInvalidArgument returns true if the error indicates an invalid argument.
|
||||
func IsInvalidArgument(err error) bool { return types.IsInvalidArgument(err) }
|
||||
|
||||
// IsDeadlineExceeded returns true if the error indicates a deadline was exceeded.
|
||||
func IsDeadlineExceeded(err error) bool { return types.IsDeadlineExceeded(err) }
|
||||
|
||||
// IsCancelled returns true if the error indicates the operation was cancelled.
|
||||
func IsCancelled(err error) bool { return types.IsCancelled(err) }
|
||||
|
||||
// IsUnimplemented returns true if the error indicates the operation is not implemented.
|
||||
func IsUnimplemented(err error) bool { return types.IsUnimplemented(err) }
|
||||
|
||||
// IsConflict returns true if the error indicates a conflict, such as
|
||||
// optimistic concurrency or an invalid state transition.
|
||||
func IsConflict(err error) bool { return types.IsConflict(err) }
|
||||
|
||||
// IsUnauthenticated returns true if the error indicates missing or invalid credentials.
|
||||
func IsUnauthenticated(err error) bool { return types.IsUnauthenticated(err) }
|
||||
@@ -0,0 +1,127 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStatusError_Error(t *testing.T) {
|
||||
err := &StatusError{
|
||||
Code: ErrorNotFound,
|
||||
Message: "sandbox not found",
|
||||
}
|
||||
s := err.Error()
|
||||
assert.Contains(t, s, "NotFound")
|
||||
assert.Contains(t, s, "sandbox not found")
|
||||
}
|
||||
|
||||
func TestStatusError_ErrorWithCause(t *testing.T) {
|
||||
cause := fmt.Errorf("underlying issue")
|
||||
err := &StatusError{
|
||||
Code: ErrorInvalidArgument,
|
||||
Message: "bad name",
|
||||
Cause: cause,
|
||||
}
|
||||
s := err.Error()
|
||||
assert.Contains(t, s, "InvalidArgument")
|
||||
assert.Contains(t, s, "bad name")
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestIsNotFound(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorNotFound, Message: "not found"}
|
||||
assert.True(t, IsNotFound(err))
|
||||
assert.False(t, IsAlreadyExists(err))
|
||||
}
|
||||
|
||||
func TestIsAlreadyExists(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorAlreadyExists, Message: "exists"}
|
||||
assert.True(t, IsAlreadyExists(err))
|
||||
assert.False(t, IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestIsUnavailable(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorUnavailable, Message: "down"}
|
||||
assert.True(t, IsUnavailable(err))
|
||||
}
|
||||
|
||||
func TestIsPermissionDenied(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorPermissionDenied, Message: "denied"}
|
||||
assert.True(t, IsPermissionDenied(err))
|
||||
}
|
||||
|
||||
func TestIsInvalidArgument(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorInvalidArgument, Message: "invalid"}
|
||||
assert.True(t, IsInvalidArgument(err))
|
||||
}
|
||||
|
||||
func TestIsDeadlineExceeded(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorDeadlineExceeded, Message: "timeout"}
|
||||
assert.True(t, IsDeadlineExceeded(err))
|
||||
}
|
||||
|
||||
func TestIsCancelled(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorCancelled, Message: "cancelled"}
|
||||
assert.True(t, IsCancelled(err))
|
||||
}
|
||||
|
||||
func TestIsConflict(t *testing.T) {
|
||||
err := &StatusError{Code: ErrorConflict, Message: "version conflict"}
|
||||
assert.True(t, IsConflict(err))
|
||||
assert.False(t, IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestIsHelpers_NonStatusError(t *testing.T) {
|
||||
err := errors.New("plain error")
|
||||
assert.False(t, IsNotFound(err))
|
||||
assert.False(t, IsAlreadyExists(err))
|
||||
assert.False(t, IsUnavailable(err))
|
||||
assert.False(t, IsPermissionDenied(err))
|
||||
assert.False(t, IsInvalidArgument(err))
|
||||
assert.False(t, IsDeadlineExceeded(err))
|
||||
assert.False(t, IsCancelled(err))
|
||||
assert.False(t, IsConflict(err))
|
||||
}
|
||||
|
||||
func TestIsHelpers_NilError(t *testing.T) {
|
||||
assert.False(t, IsNotFound(nil))
|
||||
assert.False(t, IsConflict(nil))
|
||||
}
|
||||
|
||||
func TestStatusError_WrappedError(t *testing.T) {
|
||||
inner := &StatusError{Code: ErrorNotFound, Message: "not found"}
|
||||
wrapped := fmt.Errorf("operation failed: %w", inner)
|
||||
assert.True(t, IsNotFound(wrapped))
|
||||
|
||||
var se *StatusError
|
||||
require.True(t, errors.As(wrapped, &se))
|
||||
assert.Equal(t, ErrorNotFound, se.Code)
|
||||
}
|
||||
|
||||
func TestErrorCode_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
code ErrorCode
|
||||
want string
|
||||
}{
|
||||
{ErrorNotFound, "NotFound"},
|
||||
{ErrorAlreadyExists, "AlreadyExists"},
|
||||
{ErrorUnavailable, "Unavailable"},
|
||||
{ErrorPermissionDenied, "PermissionDenied"},
|
||||
{ErrorInvalidArgument, "InvalidArgument"},
|
||||
{ErrorDeadlineExceeded, "DeadlineExceeded"},
|
||||
{ErrorCancelled, "Cancelled"},
|
||||
{ErrorInternal, "Internal"},
|
||||
{ErrorUnimplemented, "Unimplemented"},
|
||||
{ErrorConflict, "Conflict"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.want, tt.code.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// ExecResult holds the collected output of a completed command execution.
|
||||
type ExecResult = types.ExecResult
|
||||
|
||||
// ExecChunk represents a single chunk of output from a streaming command execution.
|
||||
type ExecChunk = types.ExecChunk
|
||||
|
||||
// ExecStream provides an iterator interface over streaming command output.
|
||||
type ExecStream interface {
|
||||
Next() (*ExecChunk, error)
|
||||
ExitCode() (int, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// InteractiveSession provides bidirectional I/O for interactive command execution.
|
||||
type InteractiveSession interface {
|
||||
Read(p []byte) (int, error)
|
||||
Write(p []byte) (int, error)
|
||||
Resize(cols, rows uint32) error
|
||||
ExitCode() (int, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ExecInterface defines command execution operations on sandboxes.
|
||||
// Methods accept a sandbox name and resolve it to an ID internally.
|
||||
type ExecInterface interface {
|
||||
Run(ctx context.Context, workspace, sandboxName string, command []string, opts ...ExecOptions) (*ExecResult, error)
|
||||
Stream(ctx context.Context, workspace, sandboxName string, command []string, opts ...ExecOptions) (ExecStream, error)
|
||||
Interactive(ctx context.Context, workspace, sandboxName string, command []string, cols, rows uint32, opts ...ExecOptions) (InteractiveSession, error)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import "context"
|
||||
|
||||
// FileInterface defines file transfer operations on sandboxes.
|
||||
// Methods accept a sandbox name and resolve it to an ID internally.
|
||||
type FileInterface interface {
|
||||
Upload(ctx context.Context, workspace, sandboxName string, localPath string, remotePath string) error
|
||||
Download(ctx context.Context, workspace, sandboxName string, remotePath string, localPath string) error
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package v1 provides the OpenShell SDK client.
|
||||
// gRPC error conversion is handled by the internal/converter package.
|
||||
package v1
|
||||
|
||||
import "context"
|
||||
|
||||
func contextError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
switch err {
|
||||
case context.DeadlineExceeded:
|
||||
return &StatusError{Code: ErrorDeadlineExceeded, Message: err.Error(), Cause: err}
|
||||
case context.Canceled:
|
||||
return &StatusError{Code: ErrorCancelled, Message: err.Error(), Cause: err}
|
||||
default:
|
||||
return &StatusError{Code: ErrorInternal, Message: err.Error(), Cause: err}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// HealthResult holds the result of a health check.
|
||||
type HealthResult = types.HealthResult
|
||||
|
||||
// HealthInterface defines health check operations.
|
||||
type HealthInterface interface {
|
||||
Check(ctx context.Context) (*HealthResult, error)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build integration
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func gatewayAddress(t *testing.T) string {
|
||||
t.Helper()
|
||||
addr := os.Getenv("OPENSHELL_GATEWAY_ADDRESS")
|
||||
if addr == "" {
|
||||
t.Skip("OPENSHELL_GATEWAY_ADDRESS not set")
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
func TestIntegration_HealthCheck(t *testing.T) {
|
||||
addr := gatewayAddress(t)
|
||||
|
||||
client, err := NewClient(Config{Address: addr})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
t.Skip("TODO: Health().Check() is a stub until PR B lands")
|
||||
|
||||
_, err = client.Health().Check(context.Background())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIntegration_ProviderLifecycle(t *testing.T) {
|
||||
addr := gatewayAddress(t)
|
||||
|
||||
client, err := NewClient(Config{Address: addr})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
t.Skip("TODO: implement provider create/get/list/delete integration test")
|
||||
}
|
||||
|
||||
func TestIntegration_SandboxLifecycle(t *testing.T) {
|
||||
addr := gatewayAddress(t)
|
||||
|
||||
client, err := NewClient(Config{Address: addr})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
t.Skip("TODO: implement sandbox create/wait-ready/delete integration test")
|
||||
}
|
||||
|
||||
func TestIntegration_ExecRun(t *testing.T) {
|
||||
addr := gatewayAddress(t)
|
||||
|
||||
client, err := NewClient(Config{Address: addr})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
t.Skip("TODO: implement exec run integration test")
|
||||
}
|
||||
|
||||
func TestIntegration_FileTransfer(t *testing.T) {
|
||||
addr := gatewayAddress(t)
|
||||
|
||||
client, err := NewClient(Config{Address: addr})
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
t.Skip("TODO: implement file upload/download integration test")
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import "google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
// CopyStringMap returns a shallow copy of a string-to-string map.
|
||||
// Returns nil for nil input.
|
||||
func CopyStringMap(m map[string]string) map[string]string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
c := make(map[string]string, len(m))
|
||||
for k, v := range m {
|
||||
c[k] = v
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// CopyBoolPtr returns a copy of a *bool pointer.
|
||||
// Returns nil for nil input.
|
||||
func CopyBoolPtr(p *bool) *bool {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
v := *p
|
||||
return &v
|
||||
}
|
||||
|
||||
// CopyStringSlice returns a copy of a string slice.
|
||||
// Returns nil for nil input.
|
||||
func CopyStringSlice(s []string) []string {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]string, len(s))
|
||||
copy(c, s)
|
||||
return c
|
||||
}
|
||||
|
||||
// CopyByteSlice returns a copy of a byte slice.
|
||||
// Returns nil for nil input.
|
||||
func CopyByteSlice(b []byte) []byte {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := make([]byte, len(b))
|
||||
copy(c, b)
|
||||
return c
|
||||
}
|
||||
|
||||
func structToMap(s *structpb.Struct) map[string]any {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.AsMap()
|
||||
}
|
||||
|
||||
func mapToStruct(m map[string]any) (*structpb.Struct, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return structpb.NewStruct(m)
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
sandboxpb "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// These tests use protobuf reflection to detect proto fields that the
|
||||
// converter layer does not handle. When buf generates new fields from an
|
||||
// updated .proto, the field name appears in the proto descriptor but not in
|
||||
// the "handled" set below.
|
||||
//
|
||||
// Unhandled fields FAIL the test so that proto drift is caught immediately.
|
||||
// If a field is intentionally deferred, add it to the "skipped" set with a
|
||||
// justification comment.
|
||||
|
||||
func TestConverterCoversAllProtoFields_SandboxSpec(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"log_level": true,
|
||||
"environment": true,
|
||||
"template": true,
|
||||
"policy": true,
|
||||
"providers": true,
|
||||
"resource_requirements": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&pb.SandboxSpec{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_SandboxTemplate(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"image": true,
|
||||
"runtime_class_name": true,
|
||||
"agent_socket": true,
|
||||
"labels": true,
|
||||
"annotations": true,
|
||||
"environment": true,
|
||||
"resources": true,
|
||||
"user_namespaces": true,
|
||||
"driver_config": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&pb.SandboxTemplate{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_SandboxStatus(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"sandbox_name": true,
|
||||
"agent_pod": true,
|
||||
"agent_fd": true,
|
||||
"sandbox_fd": true,
|
||||
"phase": true,
|
||||
"conditions": true,
|
||||
"current_policy_version": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&pb.SandboxStatus{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_SandboxCondition(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"type": true,
|
||||
"status": true,
|
||||
"reason": true,
|
||||
"message": true,
|
||||
"last_transition_time": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&pb.SandboxCondition{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_SandboxPolicy(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"version": true,
|
||||
"filesystem": true,
|
||||
"network_policies": true,
|
||||
"process": true,
|
||||
"landlock": true,
|
||||
}
|
||||
|
||||
skipped := fieldSet{
|
||||
// Middleware support is not yet exposed in the SDK domain model.
|
||||
// Tracked in GitHub issue #36 for Drop D.
|
||||
"network_middlewares": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&sandboxpb.SandboxPolicy{}).ProtoReflect().Descriptor(), handled, skipped)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_NetworkEndpoint(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"host": true,
|
||||
"port": true,
|
||||
"ports": true,
|
||||
"protocol": true,
|
||||
"tls": true,
|
||||
"enforcement": true,
|
||||
"access": true,
|
||||
"rules": true,
|
||||
"allowed_ips": true,
|
||||
"deny_rules": true,
|
||||
"allow_encoded_slash": true,
|
||||
"persisted_queries": true,
|
||||
"graphql_persisted_queries": true,
|
||||
"graphql_max_body_bytes": true,
|
||||
"path": true,
|
||||
"websocket_credential_rewrite": true,
|
||||
"request_body_credential_rewrite": true,
|
||||
"advisor_proposed": true,
|
||||
"credential_signing": true,
|
||||
"signing_service": true,
|
||||
"signing_region": true,
|
||||
"json_rpc_max_body_bytes": true,
|
||||
"mcp": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&sandboxpb.NetworkEndpoint{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_L7Allow(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"method": true,
|
||||
"path": true,
|
||||
"command": true,
|
||||
"query": true,
|
||||
"operation_type": true,
|
||||
"operation_name": true,
|
||||
"fields": true,
|
||||
"params": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&sandboxpb.L7Allow{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_L7DenyRule(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"method": true,
|
||||
"path": true,
|
||||
"command": true,
|
||||
"query": true,
|
||||
"operation_type": true,
|
||||
"operation_name": true,
|
||||
"fields": true,
|
||||
"params": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&sandboxpb.L7DenyRule{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_Provider(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"metadata": true,
|
||||
"type": true,
|
||||
"credentials": true,
|
||||
"config": true,
|
||||
"credential_expires_at_ms": true,
|
||||
"profile_workspace": true,
|
||||
"credential_handles": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&dm.Provider{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_CredentialHandle(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"driver": true,
|
||||
"handle": true,
|
||||
"metadata": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&dm.CredentialHandle{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
func TestConverterCoversAllProtoFields_McpOptions(t *testing.T) {
|
||||
handled := fieldSet{
|
||||
"strict_tool_names": true,
|
||||
"allow_all_known_mcp_methods": true,
|
||||
}
|
||||
|
||||
assertAllFieldsCovered(t, (&sandboxpb.McpOptions{}).ProtoReflect().Descriptor(), handled, nil)
|
||||
}
|
||||
|
||||
// fieldSet tracks proto field names that the converter handles.
|
||||
type fieldSet map[string]bool
|
||||
|
||||
// assertAllFieldsCovered fails the test for proto fields not present in
|
||||
// either handled or skipped. Stale entries in the handled set (fields
|
||||
// removed from the proto) also fail.
|
||||
func assertAllFieldsCovered(
|
||||
t *testing.T,
|
||||
desc protoreflect.MessageDescriptor,
|
||||
handled fieldSet,
|
||||
skipped fieldSet,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
fields := desc.Fields()
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
name := string(fields.Get(i).Name())
|
||||
if handled[name] || skipped[name] {
|
||||
continue
|
||||
}
|
||||
t.Errorf(
|
||||
"proto %s field %q is not handled by the converter and not explicitly skipped. "+
|
||||
"Add converter support in the appropriate FromProto/ToProto function, "+
|
||||
"or add it to the skipped set with a justification.",
|
||||
desc.FullName(), name,
|
||||
)
|
||||
}
|
||||
|
||||
for name := range handled {
|
||||
found := false
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
if string(fields.Get(i).Name()) == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf(
|
||||
"handled field %q is listed for proto %s but does not exist in the descriptor. "+
|
||||
"The proto field may have been removed or renamed.",
|
||||
name, desc.FullName(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package converter maps between gRPC/proto types and SDK domain types.
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
var grpcToSDK = map[codes.Code]types.ErrorCode{
|
||||
codes.NotFound: types.ErrorNotFound,
|
||||
codes.AlreadyExists: types.ErrorAlreadyExists,
|
||||
codes.Unavailable: types.ErrorUnavailable,
|
||||
codes.PermissionDenied: types.ErrorPermissionDenied,
|
||||
codes.Unauthenticated: types.ErrorUnauthenticated,
|
||||
codes.InvalidArgument: types.ErrorInvalidArgument,
|
||||
codes.DeadlineExceeded: types.ErrorDeadlineExceeded,
|
||||
codes.Canceled: types.ErrorCancelled,
|
||||
codes.Internal: types.ErrorInternal,
|
||||
codes.Unimplemented: types.ErrorUnimplemented,
|
||||
codes.Aborted: types.ErrorConflict,
|
||||
codes.FailedPrecondition: types.ErrorConflict,
|
||||
}
|
||||
|
||||
// FromGRPCError converts a gRPC error to a typed StatusError.
|
||||
// Returns nil for nil errors and OK status. Non-gRPC errors pass through unchanged.
|
||||
func FromGRPCError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if st.Code() == codes.OK {
|
||||
return nil
|
||||
}
|
||||
|
||||
code, mapped := grpcToSDK[st.Code()]
|
||||
if !mapped {
|
||||
code = types.ErrorInternal
|
||||
}
|
||||
|
||||
return &types.StatusError{
|
||||
Code: code,
|
||||
Message: st.Message(),
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestFromGRPCError_NotFound(t *testing.T) {
|
||||
grpcErr := status.Error(codes.NotFound, "sandbox not found")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsNotFound(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_AlreadyExists(t *testing.T) {
|
||||
grpcErr := status.Error(codes.AlreadyExists, "already exists")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsAlreadyExists(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_Unavailable(t *testing.T) {
|
||||
grpcErr := status.Error(codes.Unavailable, "service down")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsUnavailable(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_PermissionDenied(t *testing.T) {
|
||||
grpcErr := status.Error(codes.PermissionDenied, "denied")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsPermissionDenied(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_InvalidArgument(t *testing.T) {
|
||||
grpcErr := status.Error(codes.InvalidArgument, "bad arg")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsInvalidArgument(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_DeadlineExceeded(t *testing.T) {
|
||||
grpcErr := status.Error(codes.DeadlineExceeded, "timeout")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsDeadlineExceeded(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_Cancelled(t *testing.T) {
|
||||
grpcErr := status.Error(codes.Canceled, "cancelled")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsCancelled(err))
|
||||
}
|
||||
|
||||
func TestFromGRPCError_Internal(t *testing.T) {
|
||||
grpcErr := status.Error(codes.Internal, "internal error")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
|
||||
var se *v1.StatusError
|
||||
require.ErrorAs(t, err, &se)
|
||||
assert.Equal(t, v1.ErrorInternal, se.Code)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_Unimplemented(t *testing.T) {
|
||||
grpcErr := status.Error(codes.Unimplemented, "not implemented")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
|
||||
var se *v1.StatusError
|
||||
require.ErrorAs(t, err, &se)
|
||||
assert.Equal(t, v1.ErrorUnimplemented, se.Code)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_Aborted(t *testing.T) {
|
||||
grpcErr := status.Error(codes.Aborted, "version conflict")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
assert.True(t, v1.IsConflict(err))
|
||||
|
||||
var se *v1.StatusError
|
||||
require.ErrorAs(t, err, &se)
|
||||
assert.Equal(t, v1.ErrorConflict, se.Code)
|
||||
assert.Equal(t, "version conflict", se.Message)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_UnmappedCode(t *testing.T) {
|
||||
grpcErr := status.Error(codes.DataLoss, "data loss")
|
||||
err := FromGRPCError(grpcErr)
|
||||
require.Error(t, err)
|
||||
|
||||
var se *v1.StatusError
|
||||
require.ErrorAs(t, err, &se)
|
||||
assert.Equal(t, v1.ErrorInternal, se.Code)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_NilError(t *testing.T) {
|
||||
err := FromGRPCError(nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_NonGRPCError(t *testing.T) {
|
||||
err := FromGRPCError(assert.AnError)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
}
|
||||
|
||||
func TestFromGRPCError_OKStatus(t *testing.T) {
|
||||
grpcErr := status.Error(codes.OK, "")
|
||||
err := FromGRPCError(grpcErr)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
)
|
||||
|
||||
// --- LogLine ---
|
||||
|
||||
// LogLineFromProto converts a proto SandboxLogLine to an SDK LogLine.
|
||||
func LogLineFromProto(l *pb.SandboxLogLine) *types.LogLine {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.LogLine{
|
||||
Timestamp: TimeFromMillis(l.GetTimestampMs()),
|
||||
Level: l.GetLevel(),
|
||||
Target: l.GetTarget(),
|
||||
Message: l.GetMessage(),
|
||||
Source: l.GetSource(),
|
||||
Fields: CopyStringMap(l.GetFields()),
|
||||
}
|
||||
}
|
||||
|
||||
// --- LogResult ---
|
||||
|
||||
// LogResultFromProto converts a proto GetSandboxLogsResponse to an SDK LogResult.
|
||||
func LogResultFromProto(r *pb.GetSandboxLogsResponse) *types.LogResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
result := &types.LogResult{
|
||||
BufferTotal: r.GetBufferTotal(),
|
||||
}
|
||||
if logs := r.GetLogs(); len(logs) > 0 {
|
||||
result.Lines = make([]types.LogLine, 0, len(logs))
|
||||
for _, l := range logs {
|
||||
if converted := LogLineFromProto(l); converted != nil {
|
||||
result.Lines = append(result.Lines, *converted)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// --- LogLine ---
|
||||
|
||||
func TestLogLineFromProto(t *testing.T) {
|
||||
proto := &pb.SandboxLogLine{
|
||||
SandboxId: "sbx-1",
|
||||
TimestampMs: 1700000000000,
|
||||
Level: "INFO",
|
||||
Target: "network",
|
||||
Message: "Connection established",
|
||||
Source: "sandbox-agent",
|
||||
Fields: map[string]string{
|
||||
"host": "api.example.com",
|
||||
"port": "443",
|
||||
},
|
||||
}
|
||||
|
||||
line := LogLineFromProto(proto)
|
||||
|
||||
require.NotNil(t, line)
|
||||
assert.False(t, line.Timestamp.IsZero())
|
||||
assert.Equal(t, "INFO", line.Level)
|
||||
assert.Equal(t, "network", line.Target)
|
||||
assert.Equal(t, "Connection established", line.Message)
|
||||
assert.Equal(t, "sandbox-agent", line.Source)
|
||||
assert.Equal(t, "api.example.com", line.Fields["host"])
|
||||
assert.Equal(t, "443", line.Fields["port"])
|
||||
}
|
||||
|
||||
func TestLogLineFromProto_Nil(t *testing.T) {
|
||||
assert.Nil(t, LogLineFromProto(nil))
|
||||
}
|
||||
|
||||
func TestLogLineDeepCopy(t *testing.T) {
|
||||
proto := &pb.SandboxLogLine{
|
||||
TimestampMs: 1700000000000,
|
||||
Level: "WARN",
|
||||
Message: "test",
|
||||
Fields: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
line := LogLineFromProto(proto)
|
||||
proto.Fields["key"] = "changed"
|
||||
|
||||
assert.Equal(t, "value", line.Fields["key"])
|
||||
}
|
||||
|
||||
// --- LogResult ---
|
||||
|
||||
func TestLogResultFromProto(t *testing.T) {
|
||||
proto := &pb.GetSandboxLogsResponse{
|
||||
Logs: []*pb.SandboxLogLine{
|
||||
{TimestampMs: 1700000000000, Level: "INFO", Message: "first"},
|
||||
{TimestampMs: 1700000001000, Level: "DEBUG", Message: "second"},
|
||||
},
|
||||
BufferTotal: 100,
|
||||
}
|
||||
|
||||
result := LogResultFromProto(proto)
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Len(t, result.Lines, 2)
|
||||
assert.Equal(t, "INFO", result.Lines[0].Level)
|
||||
assert.Equal(t, "first", result.Lines[0].Message)
|
||||
assert.Equal(t, "DEBUG", result.Lines[1].Level)
|
||||
assert.Equal(t, "second", result.Lines[1].Message)
|
||||
assert.Equal(t, uint32(100), result.BufferTotal)
|
||||
}
|
||||
|
||||
func TestLogResultFromProto_Nil(t *testing.T) {
|
||||
assert.Nil(t, LogResultFromProto(nil))
|
||||
}
|
||||
|
||||
func TestLogResultFromProto_EmptyLogs(t *testing.T) {
|
||||
proto := &pb.GetSandboxLogsResponse{
|
||||
BufferTotal: 0,
|
||||
}
|
||||
|
||||
result := LogResultFromProto(proto)
|
||||
require.NotNil(t, result)
|
||||
assert.Empty(t, result.Lines)
|
||||
assert.Equal(t, uint32(0), result.BufferTotal)
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
sbv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1"
|
||||
)
|
||||
|
||||
// --- NetworkPolicyRule ---
|
||||
|
||||
// NetworkPolicyRuleFromProto converts a proto NetworkPolicyRule to an SDK NetworkPolicyRule.
|
||||
func NetworkPolicyRuleFromProto(r *sbv1.NetworkPolicyRule) *types.NetworkPolicyRule {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
result := &types.NetworkPolicyRule{
|
||||
Name: r.GetName(),
|
||||
}
|
||||
if eps := r.GetEndpoints(); len(eps) > 0 {
|
||||
result.Endpoints = make([]types.PolicyNetworkEndpoint, len(eps))
|
||||
for i, ep := range eps {
|
||||
if ep != nil {
|
||||
result.Endpoints[i] = policyNetworkEndpointFromProto(ep)
|
||||
}
|
||||
}
|
||||
}
|
||||
if bins := r.GetBinaries(); len(bins) > 0 {
|
||||
result.Binaries = make([]types.PolicyNetworkBinary, len(bins))
|
||||
for i, b := range bins {
|
||||
if b != nil {
|
||||
result.Binaries[i] = types.PolicyNetworkBinary{Path: b.GetPath()}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NetworkPolicyRuleToProto converts an SDK NetworkPolicyRule to a proto NetworkPolicyRule.
|
||||
func NetworkPolicyRuleToProto(r *types.NetworkPolicyRule) *sbv1.NetworkPolicyRule {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
result := &sbv1.NetworkPolicyRule{
|
||||
Name: r.Name,
|
||||
}
|
||||
if len(r.Endpoints) > 0 {
|
||||
result.Endpoints = make([]*sbv1.NetworkEndpoint, len(r.Endpoints))
|
||||
for i := range r.Endpoints {
|
||||
result.Endpoints[i] = policyNetworkEndpointToProto(&r.Endpoints[i])
|
||||
}
|
||||
}
|
||||
if len(r.Binaries) > 0 {
|
||||
result.Binaries = make([]*sbv1.NetworkBinary, len(r.Binaries))
|
||||
for i := range r.Binaries {
|
||||
result.Binaries[i] = &sbv1.NetworkBinary{Path: r.Binaries[i].Path}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- PolicyNetworkEndpoint ---
|
||||
|
||||
func policyNetworkEndpointFromProto(ep *sbv1.NetworkEndpoint) types.PolicyNetworkEndpoint {
|
||||
result := types.PolicyNetworkEndpoint{
|
||||
Host: ep.GetHost(),
|
||||
Port: ep.GetPort(),
|
||||
Protocol: ep.GetProtocol(),
|
||||
TLS: ep.GetTls(),
|
||||
Enforcement: ep.GetEnforcement(),
|
||||
Access: ep.GetAccess(),
|
||||
AllowEncodedSlash: ep.GetAllowEncodedSlash(),
|
||||
PersistedQueries: ep.GetPersistedQueries(),
|
||||
GraphqlMaxBodyBytes: ep.GetGraphqlMaxBodyBytes(),
|
||||
Path: ep.GetPath(),
|
||||
WebsocketCredentialRewrite: ep.GetWebsocketCredentialRewrite(),
|
||||
RequestBodyCredentialRewrite: ep.GetRequestBodyCredentialRewrite(),
|
||||
AdvisorProposed: ep.GetAdvisorProposed(),
|
||||
CredentialSigning: ep.GetCredentialSigning(),
|
||||
SigningService: ep.GetSigningService(),
|
||||
SigningRegion: ep.GetSigningRegion(),
|
||||
JsonRpcMaxBodyBytes: ep.GetJsonRpcMaxBodyBytes(),
|
||||
}
|
||||
if mcp := ep.GetMcp(); mcp != nil {
|
||||
result.Mcp = mcpOptionsFromProto(mcp)
|
||||
}
|
||||
if ports := ep.GetPorts(); len(ports) > 0 {
|
||||
result.Ports = make([]uint32, len(ports))
|
||||
copy(result.Ports, ports)
|
||||
}
|
||||
if ips := ep.GetAllowedIps(); len(ips) > 0 {
|
||||
result.AllowedIPs = CopyStringSlice(ips)
|
||||
}
|
||||
if rules := ep.GetRules(); len(rules) > 0 {
|
||||
result.Rules = make([]types.L7Rule, len(rules))
|
||||
for i, r := range rules {
|
||||
if r != nil {
|
||||
result.Rules[i] = l7RuleFromProto(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if deny := ep.GetDenyRules(); len(deny) > 0 {
|
||||
result.DenyRules = make([]types.L7DenyRule, len(deny))
|
||||
for i, r := range deny {
|
||||
if r != nil {
|
||||
result.DenyRules[i] = l7DenyRuleFromProto(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if gql := ep.GetGraphqlPersistedQueries(); len(gql) > 0 {
|
||||
result.GraphqlPersistedQueries = make(map[string]types.GraphqlOperation, len(gql))
|
||||
for k, v := range gql {
|
||||
if v != nil {
|
||||
result.GraphqlPersistedQueries[k] = graphqlOperationFromProto(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func policyNetworkEndpointToProto(ep *types.PolicyNetworkEndpoint) *sbv1.NetworkEndpoint {
|
||||
result := &sbv1.NetworkEndpoint{
|
||||
Host: ep.Host,
|
||||
Port: ep.Port,
|
||||
Protocol: ep.Protocol,
|
||||
Tls: ep.TLS,
|
||||
Enforcement: ep.Enforcement,
|
||||
Access: ep.Access,
|
||||
AllowEncodedSlash: ep.AllowEncodedSlash,
|
||||
PersistedQueries: ep.PersistedQueries,
|
||||
GraphqlMaxBodyBytes: ep.GraphqlMaxBodyBytes,
|
||||
Path: ep.Path,
|
||||
WebsocketCredentialRewrite: ep.WebsocketCredentialRewrite,
|
||||
RequestBodyCredentialRewrite: ep.RequestBodyCredentialRewrite,
|
||||
AdvisorProposed: ep.AdvisorProposed,
|
||||
CredentialSigning: ep.CredentialSigning,
|
||||
SigningService: ep.SigningService,
|
||||
SigningRegion: ep.SigningRegion,
|
||||
JsonRpcMaxBodyBytes: ep.JsonRpcMaxBodyBytes,
|
||||
}
|
||||
if ep.Mcp != nil {
|
||||
result.Mcp = mcpOptionsToProto(ep.Mcp)
|
||||
}
|
||||
if len(ep.Ports) > 0 {
|
||||
result.Ports = make([]uint32, len(ep.Ports))
|
||||
copy(result.Ports, ep.Ports)
|
||||
}
|
||||
if len(ep.AllowedIPs) > 0 {
|
||||
result.AllowedIps = CopyStringSlice(ep.AllowedIPs)
|
||||
}
|
||||
if len(ep.Rules) > 0 {
|
||||
result.Rules = make([]*sbv1.L7Rule, len(ep.Rules))
|
||||
for i := range ep.Rules {
|
||||
result.Rules[i] = l7RuleToProto(&ep.Rules[i])
|
||||
}
|
||||
}
|
||||
if len(ep.DenyRules) > 0 {
|
||||
result.DenyRules = make([]*sbv1.L7DenyRule, len(ep.DenyRules))
|
||||
for i := range ep.DenyRules {
|
||||
result.DenyRules[i] = l7DenyRuleToProto(&ep.DenyRules[i])
|
||||
}
|
||||
}
|
||||
if len(ep.GraphqlPersistedQueries) > 0 {
|
||||
result.GraphqlPersistedQueries = make(map[string]*sbv1.GraphqlOperation, len(ep.GraphqlPersistedQueries))
|
||||
for k, v := range ep.GraphqlPersistedQueries {
|
||||
result.GraphqlPersistedQueries[k] = graphqlOperationToProto(&v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- L7Rule ---
|
||||
|
||||
func l7RuleFromProto(r *sbv1.L7Rule) types.L7Rule {
|
||||
result := types.L7Rule{}
|
||||
if a := r.GetAllow(); a != nil {
|
||||
result.Allow = &types.L7Allow{
|
||||
Method: a.GetMethod(),
|
||||
Path: a.GetPath(),
|
||||
Command: a.GetCommand(),
|
||||
OperationType: a.GetOperationType(),
|
||||
OperationName: a.GetOperationName(),
|
||||
Fields: CopyStringSlice(a.GetFields()),
|
||||
}
|
||||
if q := a.GetQuery(); len(q) > 0 {
|
||||
result.Allow.Query = l7QueryMapFromProto(q)
|
||||
}
|
||||
if p := a.GetParams(); len(p) > 0 {
|
||||
result.Allow.Params = l7QueryMapFromProto(p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func l7RuleToProto(r *types.L7Rule) *sbv1.L7Rule {
|
||||
result := &sbv1.L7Rule{}
|
||||
if r.Allow != nil {
|
||||
result.Allow = &sbv1.L7Allow{
|
||||
Method: r.Allow.Method,
|
||||
Path: r.Allow.Path,
|
||||
Command: r.Allow.Command,
|
||||
OperationType: r.Allow.OperationType,
|
||||
OperationName: r.Allow.OperationName,
|
||||
Fields: CopyStringSlice(r.Allow.Fields),
|
||||
}
|
||||
if len(r.Allow.Query) > 0 {
|
||||
result.Allow.Query = l7QueryMapToProto(r.Allow.Query)
|
||||
}
|
||||
if len(r.Allow.Params) > 0 {
|
||||
result.Allow.Params = l7QueryMapToProto(r.Allow.Params)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- L7DenyRule ---
|
||||
|
||||
func l7DenyRuleFromProto(r *sbv1.L7DenyRule) types.L7DenyRule {
|
||||
result := types.L7DenyRule{
|
||||
Method: r.GetMethod(),
|
||||
Path: r.GetPath(),
|
||||
Command: r.GetCommand(),
|
||||
OperationType: r.GetOperationType(),
|
||||
OperationName: r.GetOperationName(),
|
||||
Fields: CopyStringSlice(r.GetFields()),
|
||||
Query: l7QueryMapFromProtoDeny(r.GetQuery()),
|
||||
}
|
||||
if p := r.GetParams(); len(p) > 0 {
|
||||
result.Params = l7QueryMapFromProto(p)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func l7DenyRuleToProto(r *types.L7DenyRule) *sbv1.L7DenyRule {
|
||||
result := &sbv1.L7DenyRule{
|
||||
Method: r.Method,
|
||||
Path: r.Path,
|
||||
Command: r.Command,
|
||||
OperationType: r.OperationType,
|
||||
OperationName: r.OperationName,
|
||||
Fields: CopyStringSlice(r.Fields),
|
||||
}
|
||||
if len(r.Query) > 0 {
|
||||
result.Query = l7QueryMapToProtoDeny(r.Query)
|
||||
}
|
||||
if len(r.Params) > 0 {
|
||||
result.Params = l7QueryMapToProto(r.Params)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- L7QueryMatcher helpers ---
|
||||
|
||||
func l7QueryMapFromProto(m map[string]*sbv1.L7QueryMatcher) map[string]types.L7QueryMatcher {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]types.L7QueryMatcher, len(m))
|
||||
for k, v := range m {
|
||||
if v != nil {
|
||||
result[k] = types.L7QueryMatcher{
|
||||
Glob: v.GetGlob(),
|
||||
Any: CopyStringSlice(v.GetAny()),
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func l7QueryMapToProto(m map[string]types.L7QueryMatcher) map[string]*sbv1.L7QueryMatcher {
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]*sbv1.L7QueryMatcher, len(m))
|
||||
for k, v := range m {
|
||||
result[k] = &sbv1.L7QueryMatcher{
|
||||
Glob: v.Glob,
|
||||
Any: CopyStringSlice(v.Any),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// L7DenyRule uses the same L7QueryMatcher proto type but on a different message.
|
||||
func l7QueryMapFromProtoDeny(m map[string]*sbv1.L7QueryMatcher) map[string]types.L7QueryMatcher {
|
||||
return l7QueryMapFromProto(m)
|
||||
}
|
||||
|
||||
func l7QueryMapToProtoDeny(m map[string]types.L7QueryMatcher) map[string]*sbv1.L7QueryMatcher {
|
||||
return l7QueryMapToProto(m)
|
||||
}
|
||||
|
||||
// --- GraphqlOperation ---
|
||||
|
||||
func graphqlOperationFromProto(op *sbv1.GraphqlOperation) types.GraphqlOperation {
|
||||
return types.GraphqlOperation{
|
||||
OperationType: op.GetOperationType(),
|
||||
OperationName: op.GetOperationName(),
|
||||
Fields: CopyStringSlice(op.GetFields()),
|
||||
}
|
||||
}
|
||||
|
||||
func graphqlOperationToProto(op *types.GraphqlOperation) *sbv1.GraphqlOperation {
|
||||
return &sbv1.GraphqlOperation{
|
||||
OperationType: op.OperationType,
|
||||
OperationName: op.OperationName,
|
||||
Fields: CopyStringSlice(op.Fields),
|
||||
}
|
||||
}
|
||||
|
||||
// --- McpOptions ---
|
||||
|
||||
func mcpOptionsFromProto(m *sbv1.McpOptions) *types.McpOptions {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.McpOptions{
|
||||
StrictToolNames: m.StrictToolNames,
|
||||
AllowAllKnownMcpMethods: m.AllowAllKnownMcpMethods,
|
||||
}
|
||||
}
|
||||
|
||||
func mcpOptionsToProto(m *types.McpOptions) *sbv1.McpOptions {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return &sbv1.McpOptions{
|
||||
StrictToolNames: m.StrictToolNames,
|
||||
AllowAllKnownMcpMethods: m.AllowAllKnownMcpMethods,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
sbv1 "github.com/NVIDIA/OpenShell/sdk/go/proto/sandboxv1"
|
||||
)
|
||||
|
||||
// --- PolicyLoadStatus enum mapping ---
|
||||
|
||||
// PolicyLoadStatusFromProto converts a proto PolicyStatus to an SDK PolicyLoadStatus.
|
||||
func PolicyLoadStatusFromProto(s pb.PolicyStatus) types.PolicyLoadStatus {
|
||||
switch s {
|
||||
case pb.PolicyStatus_POLICY_STATUS_PENDING:
|
||||
return types.PolicyLoadStatusPending
|
||||
case pb.PolicyStatus_POLICY_STATUS_LOADED:
|
||||
return types.PolicyLoadStatusLoaded
|
||||
case pb.PolicyStatus_POLICY_STATUS_FAILED:
|
||||
return types.PolicyLoadStatusFailed
|
||||
case pb.PolicyStatus_POLICY_STATUS_SUPERSEDED:
|
||||
return types.PolicyLoadStatusSuperseded
|
||||
default:
|
||||
return types.PolicyLoadStatusUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
// PolicyLoadStatusToProto converts an SDK PolicyLoadStatus to a proto PolicyStatus.
|
||||
func PolicyLoadStatusToProto(s types.PolicyLoadStatus) pb.PolicyStatus {
|
||||
switch s {
|
||||
case types.PolicyLoadStatusPending:
|
||||
return pb.PolicyStatus_POLICY_STATUS_PENDING
|
||||
case types.PolicyLoadStatusLoaded:
|
||||
return pb.PolicyStatus_POLICY_STATUS_LOADED
|
||||
case types.PolicyLoadStatusFailed:
|
||||
return pb.PolicyStatus_POLICY_STATUS_FAILED
|
||||
case types.PolicyLoadStatusSuperseded:
|
||||
return pb.PolicyStatus_POLICY_STATUS_SUPERSEDED
|
||||
default:
|
||||
return pb.PolicyStatus_POLICY_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
// --- PolicyChunk ---
|
||||
|
||||
// PolicyChunkFromProto converts a proto PolicyChunk to an SDK PolicyChunk.
|
||||
func PolicyChunkFromProto(c *pb.PolicyChunk) *types.PolicyChunk {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.PolicyChunk{
|
||||
ID: c.GetId(),
|
||||
Status: c.GetStatus(),
|
||||
RuleName: c.GetRuleName(),
|
||||
ProposedRule: NetworkPolicyRuleFromProto(c.GetProposedRule()),
|
||||
Rationale: c.GetRationale(),
|
||||
SecurityNotes: c.GetSecurityNotes(),
|
||||
Confidence: c.GetConfidence(),
|
||||
DenialSummaryIDs: CopyStringSlice(c.GetDenialSummaryIds()),
|
||||
CreatedAt: TimeFromMillis(c.GetCreatedAtMs()),
|
||||
DecidedAt: TimeFromMillis(c.GetDecidedAtMs()),
|
||||
Stage: c.GetStage(),
|
||||
SupersedesChunkID: c.GetSupersedesChunkId(),
|
||||
HitCount: c.GetHitCount(),
|
||||
FirstSeen: TimeFromMillis(c.GetFirstSeenMs()),
|
||||
LastSeen: TimeFromMillis(c.GetLastSeenMs()),
|
||||
Binary: c.GetBinary(),
|
||||
ValidationResult: c.GetValidationResult(),
|
||||
RejectionReason: c.GetRejectionReason(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- DraftPolicy ---
|
||||
|
||||
// DraftPolicyFromProto converts a proto GetDraftPolicyResponse to an SDK DraftPolicy.
|
||||
func DraftPolicyFromProto(r *pb.GetDraftPolicyResponse) *types.DraftPolicy {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
result := &types.DraftPolicy{
|
||||
RollingSummary: r.GetRollingSummary(),
|
||||
DraftVersion: r.GetDraftVersion(),
|
||||
LastAnalyzedAt: TimeFromMillis(r.GetLastAnalyzedAtMs()),
|
||||
}
|
||||
if chunks := r.GetChunks(); len(chunks) > 0 {
|
||||
result.Chunks = make([]types.PolicyChunk, 0, len(chunks))
|
||||
for _, c := range chunks {
|
||||
if converted := PolicyChunkFromProto(c); converted != nil {
|
||||
result.Chunks = append(result.Chunks, *converted)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- SandboxPolicy ---
|
||||
|
||||
// SandboxPolicyFromProto converts a proto SandboxPolicy to an SDK SandboxPolicy.
|
||||
// Returns nil for nil input. All slice and map fields are deep-copied.
|
||||
func SandboxPolicyFromProto(p *sbv1.SandboxPolicy) *types.SandboxPolicy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
result := &types.SandboxPolicy{
|
||||
Version: p.GetVersion(),
|
||||
Filesystem: filesystemPolicyFromProto(p.GetFilesystem()),
|
||||
Landlock: landlockPolicyFromProto(p.GetLandlock()),
|
||||
Process: processPolicyFromProto(p.GetProcess()),
|
||||
}
|
||||
if np := p.GetNetworkPolicies(); np != nil {
|
||||
result.NetworkPolicies = make(map[string]types.NetworkPolicyRule, len(np))
|
||||
for k, v := range np {
|
||||
if converted := NetworkPolicyRuleFromProto(v); converted != nil {
|
||||
result.NetworkPolicies[k] = *converted
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SandboxPolicyToProto converts an SDK SandboxPolicy to a proto SandboxPolicy.
|
||||
// Returns nil for nil input. All slice and map fields are deep-copied.
|
||||
func SandboxPolicyToProto(p *types.SandboxPolicy) *sbv1.SandboxPolicy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
result := &sbv1.SandboxPolicy{
|
||||
Version: p.Version,
|
||||
Filesystem: filesystemPolicyToProto(p.Filesystem),
|
||||
Landlock: landlockPolicyToProto(p.Landlock),
|
||||
Process: processPolicyToProto(p.Process),
|
||||
}
|
||||
if p.NetworkPolicies != nil {
|
||||
result.NetworkPolicies = make(map[string]*sbv1.NetworkPolicyRule, len(p.NetworkPolicies))
|
||||
for k, v := range p.NetworkPolicies {
|
||||
result.NetworkPolicies[k] = NetworkPolicyRuleToProto(&v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func filesystemPolicyFromProto(f *sbv1.FilesystemPolicy) *types.FilesystemPolicy {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.FilesystemPolicy{
|
||||
IncludeWorkdir: f.GetIncludeWorkdir(),
|
||||
ReadOnly: CopyStringSlice(f.GetReadOnly()),
|
||||
ReadWrite: CopyStringSlice(f.GetReadWrite()),
|
||||
}
|
||||
}
|
||||
|
||||
func filesystemPolicyToProto(f *types.FilesystemPolicy) *sbv1.FilesystemPolicy {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
return &sbv1.FilesystemPolicy{
|
||||
IncludeWorkdir: f.IncludeWorkdir,
|
||||
ReadOnly: CopyStringSlice(f.ReadOnly),
|
||||
ReadWrite: CopyStringSlice(f.ReadWrite),
|
||||
}
|
||||
}
|
||||
|
||||
func landlockPolicyFromProto(l *sbv1.LandlockPolicy) *types.LandlockPolicy {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.LandlockPolicy{
|
||||
Compatibility: l.GetCompatibility(),
|
||||
}
|
||||
}
|
||||
|
||||
func landlockPolicyToProto(l *types.LandlockPolicy) *sbv1.LandlockPolicy {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return &sbv1.LandlockPolicy{
|
||||
Compatibility: l.Compatibility,
|
||||
}
|
||||
}
|
||||
|
||||
func processPolicyFromProto(p *sbv1.ProcessPolicy) *types.ProcessPolicy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.ProcessPolicy{
|
||||
RunAsUser: p.GetRunAsUser(),
|
||||
RunAsGroup: p.GetRunAsGroup(),
|
||||
}
|
||||
}
|
||||
|
||||
func processPolicyToProto(p *types.ProcessPolicy) *sbv1.ProcessPolicy {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &sbv1.ProcessPolicy{
|
||||
RunAsUser: p.RunAsUser,
|
||||
RunAsGroup: p.RunAsGroup,
|
||||
}
|
||||
}
|
||||
|
||||
// --- SandboxPolicyRevision ---
|
||||
|
||||
// SandboxPolicyRevisionFromProto converts a proto SandboxPolicyRevision to an SDK SandboxPolicyRevision.
|
||||
func SandboxPolicyRevisionFromProto(r *pb.SandboxPolicyRevision) *types.SandboxPolicyRevision {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.SandboxPolicyRevision{
|
||||
Version: r.GetVersion(),
|
||||
PolicyHash: r.GetPolicyHash(),
|
||||
Status: PolicyLoadStatusFromProto(r.GetStatus()),
|
||||
LoadError: r.GetLoadError(),
|
||||
CreatedAt: TimeFromMillis(r.GetCreatedAtMs()),
|
||||
LoadedAt: TimeFromMillis(r.GetLoadedAtMs()),
|
||||
Policy: SandboxPolicyFromProto(r.GetPolicy()),
|
||||
}
|
||||
}
|
||||
|
||||
// --- PolicyStatusResult ---
|
||||
|
||||
// PolicyStatusResultFromProto converts a proto GetSandboxPolicyStatusResponse to an SDK PolicyStatusResult.
|
||||
func PolicyStatusResultFromProto(r *pb.GetSandboxPolicyStatusResponse) *types.PolicyStatusResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
result := &types.PolicyStatusResult{
|
||||
ActiveVersion: r.GetActiveVersion(),
|
||||
}
|
||||
if rev := SandboxPolicyRevisionFromProto(r.GetRevision()); rev != nil {
|
||||
result.Revision = *rev
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- ApproveResult ---
|
||||
|
||||
// ApproveResultFromProto converts a proto ApproveDraftChunkResponse to an SDK ApproveResult.
|
||||
func ApproveResultFromProto(r *pb.ApproveDraftChunkResponse) *types.ApproveResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.ApproveResult{
|
||||
PolicyVersion: r.GetPolicyVersion(),
|
||||
PolicyHash: r.GetPolicyHash(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- ApproveAllResult ---
|
||||
|
||||
// ApproveAllResultFromProto converts a proto ApproveAllDraftChunksResponse to an SDK ApproveAllResult.
|
||||
func ApproveAllResultFromProto(r *pb.ApproveAllDraftChunksResponse) *types.ApproveAllResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.ApproveAllResult{
|
||||
PolicyVersion: r.GetPolicyVersion(),
|
||||
PolicyHash: r.GetPolicyHash(),
|
||||
ChunksApproved: r.GetChunksApproved(),
|
||||
ChunksSkipped: r.GetChunksSkipped(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- UndoResult ---
|
||||
|
||||
// UndoResultFromProto converts a proto UndoDraftChunkResponse to an SDK UndoResult.
|
||||
func UndoResultFromProto(r *pb.UndoDraftChunkResponse) *types.UndoResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.UndoResult{
|
||||
PolicyVersion: r.GetPolicyVersion(),
|
||||
PolicyHash: r.GetPolicyHash(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- ClearResult ---
|
||||
|
||||
// ClearResultFromProto converts a proto ClearDraftChunksResponse to an SDK ClearResult.
|
||||
func ClearResultFromProto(r *pb.ClearDraftChunksResponse) *types.ClearResult {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.ClearResult{
|
||||
ChunksCleared: r.GetChunksCleared(),
|
||||
}
|
||||
}
|
||||
|
||||
// --- DraftHistoryEntry ---
|
||||
|
||||
// DraftHistoryEntryFromProto converts a proto DraftHistoryEntry to an SDK DraftHistoryEntry.
|
||||
func DraftHistoryEntryFromProto(e *pb.DraftHistoryEntry) *types.DraftHistoryEntry {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return &types.DraftHistoryEntry{
|
||||
Timestamp: TimeFromMillis(e.GetTimestampMs()),
|
||||
EventType: e.GetEventType(),
|
||||
Description: e.GetDescription(),
|
||||
ChunkID: e.GetChunkId(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
|
||||
)
|
||||
|
||||
// ProviderFromProto converts a proto Provider to an SDK Provider.
|
||||
func ProviderFromProto(p *dm.Provider) *types.Provider {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &types.Provider{
|
||||
Type: p.GetType(),
|
||||
Spec: types.ProviderSpec{
|
||||
Config: CopyStringMap(p.GetConfig()),
|
||||
ProfileWorkspace: p.GetProfileWorkspace(),
|
||||
},
|
||||
}
|
||||
|
||||
if m := p.GetMetadata(); m != nil {
|
||||
result.ID = m.GetId()
|
||||
result.Name = m.GetName()
|
||||
result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs())
|
||||
result.Labels = CopyStringMap(m.GetLabels())
|
||||
result.Annotations = CopyStringMap(m.GetAnnotations())
|
||||
result.ResourceVersion = m.GetResourceVersion()
|
||||
result.Workspace = m.GetWorkspace()
|
||||
result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs())
|
||||
}
|
||||
|
||||
if expires := p.GetCredentialExpiresAtMs(); len(expires) > 0 {
|
||||
result.Spec.CredentialExpiresAt = make(map[string]time.Time, len(expires))
|
||||
for k, ms := range expires {
|
||||
result.Spec.CredentialExpiresAt[k] = TimeFromMillis(ms)
|
||||
}
|
||||
}
|
||||
|
||||
if handles := p.GetCredentialHandles(); len(handles) > 0 {
|
||||
result.Spec.CredentialHandles = make(map[string]types.CredentialHandle, len(handles))
|
||||
for k, h := range handles {
|
||||
result.Spec.CredentialHandles[k] = types.CredentialHandle{
|
||||
Driver: h.GetDriver(),
|
||||
Handle: h.GetHandle(),
|
||||
Metadata: CopyStringMap(h.GetMetadata()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ProviderToProto converts an SDK Provider to a proto Provider.
|
||||
func ProviderToProto(p *types.Provider) *dm.Provider {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &dm.Provider{
|
||||
Metadata: &dm.ObjectMeta{
|
||||
Id: p.ID,
|
||||
Name: p.Name,
|
||||
CreatedAtMs: MillisFromTime(p.CreatedAt),
|
||||
Labels: CopyStringMap(p.Labels),
|
||||
Annotations: CopyStringMap(p.Annotations),
|
||||
ResourceVersion: p.ResourceVersion,
|
||||
Workspace: p.Workspace,
|
||||
DeletionTimestampMs: MillisFromTimePtr(p.DeletionTimestamp),
|
||||
},
|
||||
Type: p.Type,
|
||||
Credentials: CopyStringMap(p.Spec.Credentials),
|
||||
Config: CopyStringMap(p.Spec.Config),
|
||||
ProfileWorkspace: p.Spec.ProfileWorkspace,
|
||||
}
|
||||
|
||||
if len(p.Spec.CredentialExpiresAt) > 0 {
|
||||
result.CredentialExpiresAtMs = make(map[string]int64, len(p.Spec.CredentialExpiresAt))
|
||||
for k, t := range p.Spec.CredentialExpiresAt {
|
||||
result.CredentialExpiresAtMs[k] = MillisFromTime(t)
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.Spec.CredentialHandles) > 0 {
|
||||
result.CredentialHandles = make(map[string]*dm.CredentialHandle, len(p.Spec.CredentialHandles))
|
||||
for k, h := range p.Spec.CredentialHandles {
|
||||
result.CredentialHandles[k] = &dm.CredentialHandle{
|
||||
Driver: h.Driver,
|
||||
Handle: h.Handle,
|
||||
Metadata: CopyStringMap(h.Metadata),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProviderFromProto_Nil(t *testing.T) {
|
||||
assert.Nil(t, ProviderFromProto(nil))
|
||||
}
|
||||
|
||||
func TestProviderFromProto_Full(t *testing.T) {
|
||||
proto := &dm.Provider{
|
||||
Metadata: &dm.ObjectMeta{
|
||||
Id: "prov-1",
|
||||
Name: "claude-provider",
|
||||
CreatedAtMs: 1700000000000,
|
||||
Labels: map[string]string{"env": "prod"},
|
||||
Annotations: map[string]string{"note": "test"},
|
||||
ResourceVersion: 42,
|
||||
Workspace: "default",
|
||||
},
|
||||
Type: "claude",
|
||||
Credentials: map[string]string{"api_key": "secret"},
|
||||
Config: map[string]string{"base_url": "https://api.example.com"},
|
||||
ProfileWorkspace: "shared",
|
||||
CredentialExpiresAtMs: map[string]int64{
|
||||
"api_key": 1700003600000,
|
||||
},
|
||||
CredentialHandles: map[string]*dm.CredentialHandle{
|
||||
"api_key": {
|
||||
Driver: "vault",
|
||||
Handle: "secret/data/claude",
|
||||
Metadata: map[string]string{"version": "3"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := ProviderFromProto(proto)
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "prov-1", result.ID)
|
||||
assert.Equal(t, "claude-provider", result.Name)
|
||||
assert.Equal(t, "claude", result.Type)
|
||||
assert.Equal(t, uint64(42), result.ResourceVersion)
|
||||
assert.Equal(t, "default", result.Workspace)
|
||||
assert.Equal(t, map[string]string{"env": "prod"}, result.Labels)
|
||||
assert.Equal(t, map[string]string{"note": "test"}, result.Annotations)
|
||||
assert.Equal(t, map[string]string{"base_url": "https://api.example.com"}, result.Spec.Config)
|
||||
assert.Equal(t, "shared", result.Spec.ProfileWorkspace)
|
||||
|
||||
require.Len(t, result.Spec.CredentialExpiresAt, 1)
|
||||
assert.False(t, result.Spec.CredentialExpiresAt["api_key"].IsZero())
|
||||
|
||||
require.Len(t, result.Spec.CredentialHandles, 1)
|
||||
h := result.Spec.CredentialHandles["api_key"]
|
||||
assert.Equal(t, "vault", h.Driver)
|
||||
assert.Equal(t, "secret/data/claude", h.Handle)
|
||||
assert.Equal(t, map[string]string{"version": "3"}, h.Metadata)
|
||||
}
|
||||
|
||||
func TestProviderFromProto_NilMetadata(t *testing.T) {
|
||||
proto := &dm.Provider{
|
||||
Type: "openai",
|
||||
Config: map[string]string{"key": "val"},
|
||||
}
|
||||
|
||||
result := ProviderFromProto(proto)
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "", result.ID)
|
||||
assert.Equal(t, "", result.Name)
|
||||
assert.Equal(t, "openai", result.Type)
|
||||
assert.Equal(t, map[string]string{"key": "val"}, result.Spec.Config)
|
||||
}
|
||||
|
||||
func TestProviderFromProto_EmptyHandles(t *testing.T) {
|
||||
proto := &dm.Provider{
|
||||
Type: "test",
|
||||
CredentialHandles: map[string]*dm.CredentialHandle{},
|
||||
}
|
||||
|
||||
result := ProviderFromProto(proto)
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Nil(t, result.Spec.CredentialHandles)
|
||||
}
|
||||
|
||||
func TestProviderToProto_Nil(t *testing.T) {
|
||||
assert.Nil(t, ProviderToProto(nil))
|
||||
}
|
||||
|
||||
func TestProviderToProto_Full(t *testing.T) {
|
||||
expires := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
provider := &types.Provider{
|
||||
ID: "prov-1",
|
||||
Name: "test-provider",
|
||||
Type: "claude",
|
||||
Labels: map[string]string{"env": "dev"},
|
||||
Annotations: map[string]string{"note": "x"},
|
||||
ResourceVersion: 7,
|
||||
Workspace: "ws-1",
|
||||
Spec: types.ProviderSpec{
|
||||
Credentials: map[string]string{"token": "abc"},
|
||||
Config: map[string]string{"url": "https://example.com"},
|
||||
ProfileWorkspace: "global",
|
||||
CredentialExpiresAt: map[string]time.Time{"token": expires},
|
||||
CredentialHandles: map[string]types.CredentialHandle{
|
||||
"token": {
|
||||
Driver: "k8s-secrets",
|
||||
Handle: "ns/secret-name",
|
||||
Metadata: map[string]string{"k": "v"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := ProviderToProto(provider)
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, "prov-1", result.Metadata.Id)
|
||||
assert.Equal(t, "test-provider", result.Metadata.Name)
|
||||
assert.Equal(t, "claude", result.Type)
|
||||
assert.Equal(t, "global", result.ProfileWorkspace)
|
||||
assert.Equal(t, map[string]string{"token": "abc"}, result.Credentials)
|
||||
assert.Equal(t, map[string]string{"url": "https://example.com"}, result.Config)
|
||||
|
||||
require.Len(t, result.CredentialExpiresAtMs, 1)
|
||||
assert.Greater(t, result.CredentialExpiresAtMs["token"], int64(0))
|
||||
|
||||
require.Len(t, result.CredentialHandles, 1)
|
||||
h := result.CredentialHandles["token"]
|
||||
assert.Equal(t, "k8s-secrets", h.Driver)
|
||||
assert.Equal(t, "ns/secret-name", h.Handle)
|
||||
assert.Equal(t, map[string]string{"k": "v"}, h.Metadata)
|
||||
}
|
||||
|
||||
func TestProviderRoundTrip(t *testing.T) {
|
||||
original := &types.Provider{
|
||||
ID: "rt-1",
|
||||
Name: "roundtrip",
|
||||
Type: "gitlab",
|
||||
ResourceVersion: 3,
|
||||
Workspace: "default",
|
||||
Labels: map[string]string{"team": "infra"},
|
||||
Spec: types.ProviderSpec{
|
||||
Config: map[string]string{"url": "https://gitlab.com"},
|
||||
ProfileWorkspace: "shared",
|
||||
CredentialHandles: map[string]types.CredentialHandle{
|
||||
"pat": {Driver: "vault", Handle: "secret/gitlab", Metadata: map[string]string{"ver": "1"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
proto := ProviderToProto(original)
|
||||
back := ProviderFromProto(proto)
|
||||
|
||||
assert.Equal(t, original.ID, back.ID)
|
||||
assert.Equal(t, original.Name, back.Name)
|
||||
assert.Equal(t, original.Type, back.Type)
|
||||
assert.Equal(t, original.Workspace, back.Workspace)
|
||||
assert.Equal(t, original.Labels, back.Labels)
|
||||
assert.Equal(t, original.Spec.Config, back.Spec.Config)
|
||||
assert.Equal(t, original.Spec.ProfileWorkspace, back.Spec.ProfileWorkspace)
|
||||
assert.Equal(t, original.Spec.CredentialHandles, back.Spec.CredentialHandles)
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
)
|
||||
|
||||
// SandboxFromProto converts a proto Sandbox to an SDK Sandbox.
|
||||
func SandboxFromProto(s *pb.Sandbox) *types.Sandbox {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &types.Sandbox{}
|
||||
|
||||
if m := s.GetMetadata(); m != nil {
|
||||
result.ID = m.GetId()
|
||||
result.Name = m.GetName()
|
||||
result.CreatedAt = TimeFromMillis(m.GetCreatedAtMs())
|
||||
result.Labels = CopyStringMap(m.GetLabels())
|
||||
result.Annotations = CopyStringMap(m.GetAnnotations())
|
||||
result.ResourceVersion = m.GetResourceVersion()
|
||||
result.Workspace = m.GetWorkspace()
|
||||
result.DeletionTimestamp = TimeFromMillisPtr(m.GetDeletionTimestampMs())
|
||||
}
|
||||
|
||||
if spec := s.GetSpec(); spec != nil {
|
||||
result.Spec = sandboxSpecFromProto(spec)
|
||||
}
|
||||
|
||||
if status := s.GetStatus(); status != nil {
|
||||
result.Status = sandboxStatusFromProto(status)
|
||||
} else {
|
||||
result.Status.Phase = types.SandboxUnknown
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func sandboxSpecFromProto(spec *pb.SandboxSpec) types.SandboxSpec {
|
||||
result := types.SandboxSpec{
|
||||
LogLevel: spec.GetLogLevel(),
|
||||
Environment: CopyStringMap(spec.GetEnvironment()),
|
||||
Providers: CopyStringSlice(spec.GetProviders()),
|
||||
Policy: SandboxPolicyFromProto(spec.GetPolicy()),
|
||||
}
|
||||
|
||||
if tmpl := spec.GetTemplate(); tmpl != nil {
|
||||
result.Template = &types.SandboxTemplate{
|
||||
Image: tmpl.GetImage(),
|
||||
RuntimeClassName: tmpl.GetRuntimeClassName(),
|
||||
AgentSocket: tmpl.GetAgentSocket(),
|
||||
Labels: CopyStringMap(tmpl.GetLabels()),
|
||||
Annotations: CopyStringMap(tmpl.GetAnnotations()),
|
||||
Environment: CopyStringMap(tmpl.GetEnvironment()),
|
||||
Resources: structToMap(tmpl.GetResources()),
|
||||
UserNamespaces: CopyBoolPtr(tmpl.UserNamespaces),
|
||||
DriverConfig: structToMap(tmpl.GetDriverConfig()),
|
||||
}
|
||||
}
|
||||
|
||||
if rr := spec.GetResourceRequirements(); rr != nil {
|
||||
if gpu := rr.GetGpu(); gpu != nil && gpu.Count != nil {
|
||||
result.GPUCount = gpu.Count
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func sandboxStatusFromProto(status *pb.SandboxStatus) types.SandboxStatus {
|
||||
result := types.SandboxStatus{
|
||||
SandboxName: status.GetSandboxName(),
|
||||
AgentPod: status.GetAgentPod(),
|
||||
AgentFd: status.GetAgentFd(),
|
||||
SandboxFd: status.GetSandboxFd(),
|
||||
Phase: SandboxPhaseFromProto(status.GetPhase()),
|
||||
CurrentPolicyVersion: status.GetCurrentPolicyVersion(),
|
||||
}
|
||||
|
||||
for _, c := range status.GetConditions() {
|
||||
result.Conditions = append(result.Conditions, types.SandboxCondition{
|
||||
Type: c.GetType(),
|
||||
Status: c.GetStatus(),
|
||||
Reason: c.GetReason(),
|
||||
Message: c.GetMessage(),
|
||||
LastTransitionTime: c.GetLastTransitionTime(),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SandboxPhaseFromProto converts a proto SandboxPhase to an SDK SandboxPhase.
|
||||
func SandboxPhaseFromProto(phase pb.SandboxPhase) types.SandboxPhase {
|
||||
switch phase {
|
||||
case pb.SandboxPhase_SANDBOX_PHASE_PROVISIONING:
|
||||
return types.SandboxProvisioning
|
||||
case pb.SandboxPhase_SANDBOX_PHASE_READY:
|
||||
return types.SandboxReady
|
||||
case pb.SandboxPhase_SANDBOX_PHASE_ERROR:
|
||||
return types.SandboxError
|
||||
case pb.SandboxPhase_SANDBOX_PHASE_DELETING:
|
||||
return types.SandboxDeleting
|
||||
case pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN:
|
||||
return types.SandboxUnknown
|
||||
default:
|
||||
return types.SandboxUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// SandboxPhaseToProto converts an SDK SandboxPhase to a proto SandboxPhase.
|
||||
func SandboxPhaseToProto(phase types.SandboxPhase) pb.SandboxPhase {
|
||||
switch phase {
|
||||
case types.SandboxProvisioning:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_PROVISIONING
|
||||
case types.SandboxReady:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_READY
|
||||
case types.SandboxError:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_ERROR
|
||||
case types.SandboxDeleting:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_DELETING
|
||||
case types.SandboxUnknown:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN
|
||||
default:
|
||||
return pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// SandboxToProto converts an SDK Sandbox to a proto Sandbox.
|
||||
func SandboxToProto(s *types.Sandbox) (*pb.Sandbox, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
spec, err := SandboxSpecToProto(&s.Spec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert sandbox spec: %w", err)
|
||||
}
|
||||
|
||||
return &pb.Sandbox{
|
||||
Metadata: &dm.ObjectMeta{
|
||||
Id: s.ID,
|
||||
Name: s.Name,
|
||||
CreatedAtMs: MillisFromTime(s.CreatedAt),
|
||||
Labels: CopyStringMap(s.Labels),
|
||||
Annotations: CopyStringMap(s.Annotations),
|
||||
ResourceVersion: s.ResourceVersion,
|
||||
Workspace: s.Workspace,
|
||||
DeletionTimestampMs: MillisFromTimePtr(s.DeletionTimestamp),
|
||||
},
|
||||
Spec: spec,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SandboxSpecToProto converts an SDK SandboxSpec to a proto SandboxSpec.
|
||||
func SandboxSpecToProto(spec *types.SandboxSpec) (*pb.SandboxSpec, error) {
|
||||
if spec == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := &pb.SandboxSpec{
|
||||
LogLevel: spec.LogLevel,
|
||||
Environment: CopyStringMap(spec.Environment),
|
||||
Providers: CopyStringSlice(spec.Providers),
|
||||
Policy: SandboxPolicyToProto(spec.Policy),
|
||||
}
|
||||
|
||||
if spec.Template != nil {
|
||||
resources, err := mapToStruct(spec.Template.Resources)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert template resources: %w", err)
|
||||
}
|
||||
driverConfig, err := mapToStruct(spec.Template.DriverConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert template driver config: %w", err)
|
||||
}
|
||||
result.Template = &pb.SandboxTemplate{
|
||||
Image: spec.Template.Image,
|
||||
RuntimeClassName: spec.Template.RuntimeClassName,
|
||||
AgentSocket: spec.Template.AgentSocket,
|
||||
Labels: CopyStringMap(spec.Template.Labels),
|
||||
Annotations: CopyStringMap(spec.Template.Annotations),
|
||||
Environment: CopyStringMap(spec.Template.Environment),
|
||||
Resources: resources,
|
||||
UserNamespaces: CopyBoolPtr(spec.Template.UserNamespaces),
|
||||
DriverConfig: driverConfig,
|
||||
}
|
||||
}
|
||||
|
||||
if spec.GPUCount != nil {
|
||||
result.ResourceRequirements = &pb.ResourceRequirements{
|
||||
Gpu: &pb.GpuResourceRequirements{
|
||||
Count: spec.GPUCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
dm "github.com/NVIDIA/OpenShell/sdk/go/proto/datamodelv1"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestSandboxFromProto(t *testing.T) {
|
||||
userNS := true
|
||||
gpuCount := uint32(2)
|
||||
proto := &pb.Sandbox{
|
||||
Metadata: &dm.ObjectMeta{
|
||||
Id: "sb-1",
|
||||
Name: "my-sandbox",
|
||||
CreatedAtMs: 1700000000000,
|
||||
Labels: map[string]string{"env": "dev"},
|
||||
Annotations: map[string]string{"owner": "team-a"},
|
||||
ResourceVersion: 3,
|
||||
Workspace: "prod",
|
||||
DeletionTimestampMs: 1700000060000,
|
||||
},
|
||||
Spec: &pb.SandboxSpec{
|
||||
LogLevel: "debug",
|
||||
Environment: map[string]string{"FOO": "bar"},
|
||||
Template: &pb.SandboxTemplate{
|
||||
Image: "nvidia/sandbox:latest",
|
||||
RuntimeClassName: "kata",
|
||||
AgentSocket: "/var/run/agent.sock",
|
||||
Labels: map[string]string{"app": "test"},
|
||||
Annotations: map[string]string{"note": "hello"},
|
||||
Environment: map[string]string{"TMPL_VAR": "val"},
|
||||
UserNamespaces: &userNS,
|
||||
},
|
||||
Providers: []string{"claude", "github"},
|
||||
ResourceRequirements: &pb.ResourceRequirements{
|
||||
Gpu: &pb.GpuResourceRequirements{
|
||||
Count: &gpuCount,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: &pb.SandboxStatus{
|
||||
SandboxName: "sb-compute-1",
|
||||
AgentPod: "agent-pod-xyz",
|
||||
AgentFd: "fd-agent",
|
||||
SandboxFd: "fd-sandbox",
|
||||
Phase: pb.SandboxPhase_SANDBOX_PHASE_READY,
|
||||
CurrentPolicyVersion: 7,
|
||||
Conditions: []*pb.SandboxCondition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "AllGood",
|
||||
Message: "Sandbox is ready",
|
||||
LastTransitionTime: "2024-01-01T00:00:00Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
s := SandboxFromProto(proto)
|
||||
|
||||
require.NotNil(t, s)
|
||||
assert.Equal(t, "sb-1", s.ID)
|
||||
assert.Equal(t, "my-sandbox", s.Name)
|
||||
assert.Equal(t, time.UnixMilli(1700000000000).UTC(), s.CreatedAt)
|
||||
assert.Equal(t, map[string]string{"env": "dev"}, s.Labels)
|
||||
assert.Equal(t, map[string]string{"owner": "team-a"}, s.Annotations)
|
||||
assert.Equal(t, uint64(3), s.ResourceVersion)
|
||||
assert.Equal(t, "prod", s.Workspace)
|
||||
require.NotNil(t, s.DeletionTimestamp)
|
||||
assert.Equal(t, time.UnixMilli(1700000060000).UTC(), *s.DeletionTimestamp)
|
||||
|
||||
// Spec
|
||||
assert.Equal(t, "debug", s.Spec.LogLevel)
|
||||
assert.Equal(t, map[string]string{"FOO": "bar"}, s.Spec.Environment)
|
||||
assert.Equal(t, []string{"claude", "github"}, s.Spec.Providers)
|
||||
require.NotNil(t, s.Spec.GPUCount)
|
||||
assert.Equal(t, uint32(2), *s.Spec.GPUCount)
|
||||
|
||||
// Template
|
||||
require.NotNil(t, s.Spec.Template)
|
||||
assert.Equal(t, "nvidia/sandbox:latest", s.Spec.Template.Image)
|
||||
assert.Equal(t, "kata", s.Spec.Template.RuntimeClassName)
|
||||
assert.Equal(t, "/var/run/agent.sock", s.Spec.Template.AgentSocket)
|
||||
assert.Equal(t, map[string]string{"app": "test"}, s.Spec.Template.Labels)
|
||||
assert.Equal(t, map[string]string{"note": "hello"}, s.Spec.Template.Annotations)
|
||||
assert.Equal(t, map[string]string{"TMPL_VAR": "val"}, s.Spec.Template.Environment)
|
||||
require.NotNil(t, s.Spec.Template.UserNamespaces)
|
||||
assert.True(t, *s.Spec.Template.UserNamespaces)
|
||||
|
||||
// Status
|
||||
assert.Equal(t, "sb-compute-1", s.Status.SandboxName)
|
||||
assert.Equal(t, "agent-pod-xyz", s.Status.AgentPod)
|
||||
assert.Equal(t, "fd-agent", s.Status.AgentFd)
|
||||
assert.Equal(t, "fd-sandbox", s.Status.SandboxFd)
|
||||
assert.Equal(t, v1.SandboxReady, s.Status.Phase)
|
||||
assert.Equal(t, uint32(7), s.Status.CurrentPolicyVersion)
|
||||
require.Len(t, s.Status.Conditions, 1)
|
||||
assert.Equal(t, "Ready", s.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "True", s.Status.Conditions[0].Status)
|
||||
assert.Equal(t, "AllGood", s.Status.Conditions[0].Reason)
|
||||
assert.Equal(t, "Sandbox is ready", s.Status.Conditions[0].Message)
|
||||
assert.Equal(t, "2024-01-01T00:00:00Z", s.Status.Conditions[0].LastTransitionTime)
|
||||
}
|
||||
|
||||
func TestSandboxFromProto_NilFields(t *testing.T) {
|
||||
proto := &pb.Sandbox{}
|
||||
|
||||
s := SandboxFromProto(proto)
|
||||
|
||||
require.NotNil(t, s)
|
||||
assert.Empty(t, s.ID)
|
||||
assert.Empty(t, s.Name)
|
||||
assert.True(t, s.CreatedAt.IsZero())
|
||||
assert.Nil(t, s.Spec.Template)
|
||||
assert.Nil(t, s.Spec.GPUCount)
|
||||
assert.Equal(t, v1.SandboxUnknown, s.Status.Phase)
|
||||
}
|
||||
|
||||
func TestSandboxFromProto_Nil(t *testing.T) {
|
||||
s := SandboxFromProto(nil)
|
||||
assert.Nil(t, s)
|
||||
}
|
||||
|
||||
func TestSandboxPhaseFromProto(t *testing.T) {
|
||||
tests := []struct {
|
||||
proto pb.SandboxPhase
|
||||
expected v1.SandboxPhase
|
||||
}{
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_PROVISIONING, v1.SandboxProvisioning},
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_READY, v1.SandboxReady},
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_ERROR, v1.SandboxError},
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_DELETING, v1.SandboxDeleting},
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN, v1.SandboxUnknown},
|
||||
{pb.SandboxPhase_SANDBOX_PHASE_UNSPECIFIED, v1.SandboxUnknown},
|
||||
{pb.SandboxPhase(999), v1.SandboxUnknown},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.expected, SandboxPhaseFromProto(tt.proto), "phase %v", tt.proto)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxPhaseToProto(t *testing.T) {
|
||||
tests := []struct {
|
||||
sdk v1.SandboxPhase
|
||||
expected pb.SandboxPhase
|
||||
}{
|
||||
{v1.SandboxProvisioning, pb.SandboxPhase_SANDBOX_PHASE_PROVISIONING},
|
||||
{v1.SandboxReady, pb.SandboxPhase_SANDBOX_PHASE_READY},
|
||||
{v1.SandboxError, pb.SandboxPhase_SANDBOX_PHASE_ERROR},
|
||||
{v1.SandboxDeleting, pb.SandboxPhase_SANDBOX_PHASE_DELETING},
|
||||
{v1.SandboxUnknown, pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN},
|
||||
{v1.SandboxPhase("bogus"), pb.SandboxPhase_SANDBOX_PHASE_UNKNOWN},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
assert.Equal(t, tt.expected, SandboxPhaseToProto(tt.sdk), "phase %v", tt.sdk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSandboxToProto(t *testing.T) {
|
||||
userNS := true
|
||||
gpuCount := uint32(4)
|
||||
delTime := time.UnixMilli(1700000060000).UTC()
|
||||
s := &v1.Sandbox{
|
||||
ID: "sb-1",
|
||||
Name: "my-sandbox",
|
||||
CreatedAt: time.UnixMilli(1700000000000).UTC(),
|
||||
Labels: map[string]string{"env": "dev"},
|
||||
Annotations: map[string]string{"owner": "team-a"},
|
||||
ResourceVersion: 3,
|
||||
Workspace: "prod",
|
||||
DeletionTimestamp: &delTime,
|
||||
Spec: v1.SandboxSpec{
|
||||
LogLevel: "info",
|
||||
Environment: map[string]string{"KEY": "val"},
|
||||
Template: &v1.SandboxTemplate{
|
||||
Image: "img:v1",
|
||||
RuntimeClassName: "runc",
|
||||
AgentSocket: "/sock",
|
||||
Labels: map[string]string{"l": "v"},
|
||||
Annotations: map[string]string{"a": "v"},
|
||||
Environment: map[string]string{"E": "V"},
|
||||
UserNamespaces: &userNS,
|
||||
},
|
||||
Providers: []string{"prov-a"},
|
||||
GPUCount: &gpuCount,
|
||||
},
|
||||
}
|
||||
|
||||
p, err := SandboxToProto(s)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.Metadata)
|
||||
assert.Equal(t, "sb-1", p.Metadata.Id)
|
||||
assert.Equal(t, "my-sandbox", p.Metadata.Name)
|
||||
assert.Equal(t, int64(1700000000000), p.Metadata.CreatedAtMs)
|
||||
assert.Equal(t, map[string]string{"env": "dev"}, p.Metadata.Labels)
|
||||
assert.Equal(t, map[string]string{"owner": "team-a"}, p.Metadata.Annotations)
|
||||
assert.Equal(t, uint64(3), p.Metadata.ResourceVersion)
|
||||
assert.Equal(t, "prod", p.Metadata.Workspace)
|
||||
assert.Equal(t, int64(1700000060000), p.Metadata.DeletionTimestampMs)
|
||||
|
||||
require.NotNil(t, p.Spec)
|
||||
assert.Equal(t, "info", p.Spec.LogLevel)
|
||||
assert.Equal(t, map[string]string{"KEY": "val"}, p.Spec.Environment)
|
||||
assert.Equal(t, []string{"prov-a"}, p.Spec.Providers)
|
||||
|
||||
require.NotNil(t, p.Spec.ResourceRequirements)
|
||||
require.NotNil(t, p.Spec.ResourceRequirements.Gpu)
|
||||
assert.Equal(t, uint32(4), p.Spec.ResourceRequirements.Gpu.GetCount())
|
||||
|
||||
require.NotNil(t, p.Spec.Template)
|
||||
assert.Equal(t, "img:v1", p.Spec.Template.Image)
|
||||
assert.Equal(t, "runc", p.Spec.Template.RuntimeClassName)
|
||||
assert.Equal(t, "/sock", p.Spec.Template.AgentSocket)
|
||||
assert.Equal(t, map[string]string{"l": "v"}, p.Spec.Template.Labels)
|
||||
assert.Equal(t, map[string]string{"a": "v"}, p.Spec.Template.Annotations)
|
||||
assert.Equal(t, map[string]string{"E": "V"}, p.Spec.Template.Environment)
|
||||
require.NotNil(t, p.Spec.Template.UserNamespaces)
|
||||
assert.True(t, *p.Spec.Template.UserNamespaces)
|
||||
}
|
||||
|
||||
func TestSandboxToProto_Nil(t *testing.T) {
|
||||
p, err := SandboxToProto(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
|
||||
func TestSandboxToProto_NilTemplate(t *testing.T) {
|
||||
s := &v1.Sandbox{
|
||||
Spec: v1.SandboxSpec{
|
||||
LogLevel: "warn",
|
||||
},
|
||||
}
|
||||
|
||||
p, err := SandboxToProto(s)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
require.NotNil(t, p.Spec)
|
||||
assert.Nil(t, p.Spec.Template)
|
||||
assert.Nil(t, p.Spec.ResourceRequirements)
|
||||
}
|
||||
|
||||
func TestSandboxRoundTrip(t *testing.T) {
|
||||
userNS := false
|
||||
gpuCount := uint32(1)
|
||||
rtDelTime := time.UnixMilli(1700000090000).UTC()
|
||||
original := &v1.Sandbox{
|
||||
ID: "sb-rt",
|
||||
Name: "round-trip",
|
||||
CreatedAt: time.UnixMilli(1700000000000).UTC(),
|
||||
Labels: map[string]string{"team": "platform"},
|
||||
Annotations: map[string]string{"note": "rt-test"},
|
||||
ResourceVersion: 10,
|
||||
Workspace: "staging",
|
||||
DeletionTimestamp: &rtDelTime,
|
||||
Spec: v1.SandboxSpec{
|
||||
LogLevel: "trace",
|
||||
Environment: map[string]string{"A": "B"},
|
||||
Template: &v1.SandboxTemplate{
|
||||
Image: "img:rt",
|
||||
UserNamespaces: &userNS,
|
||||
},
|
||||
Providers: []string{"p1", "p2"},
|
||||
GPUCount: &gpuCount,
|
||||
Policy: &v1.SandboxPolicy{
|
||||
Version: 3,
|
||||
Filesystem: &v1.FilesystemPolicy{
|
||||
IncludeWorkdir: true,
|
||||
ReadOnly: []string{"/etc", "/usr/share"},
|
||||
ReadWrite: []string{"/tmp"},
|
||||
},
|
||||
Landlock: &v1.LandlockPolicy{
|
||||
Compatibility: "best_effort",
|
||||
},
|
||||
Process: &v1.ProcessPolicy{
|
||||
RunAsUser: "sandbox",
|
||||
RunAsGroup: "sandbox-group",
|
||||
},
|
||||
NetworkPolicies: map[string]v1.NetworkPolicyRule{
|
||||
"web": {
|
||||
Name: "web",
|
||||
Endpoints: []v1.PolicyNetworkEndpoint{
|
||||
{Host: "api.example.com", Port: 443, Protocol: "rest"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := SandboxToProto(original)
|
||||
require.NoError(t, err)
|
||||
back := SandboxFromProto(p)
|
||||
|
||||
assert.Equal(t, original.ID, back.ID)
|
||||
assert.Equal(t, original.Name, back.Name)
|
||||
assert.Equal(t, original.CreatedAt, back.CreatedAt)
|
||||
assert.Equal(t, original.Labels, back.Labels)
|
||||
assert.Equal(t, original.Annotations, back.Annotations)
|
||||
assert.Equal(t, original.ResourceVersion, back.ResourceVersion)
|
||||
assert.Equal(t, original.Workspace, back.Workspace)
|
||||
require.NotNil(t, back.DeletionTimestamp)
|
||||
assert.Equal(t, *original.DeletionTimestamp, *back.DeletionTimestamp)
|
||||
assert.Equal(t, original.Spec.LogLevel, back.Spec.LogLevel)
|
||||
assert.Equal(t, original.Spec.Environment, back.Spec.Environment)
|
||||
assert.Equal(t, original.Spec.Providers, back.Spec.Providers)
|
||||
require.NotNil(t, back.Spec.GPUCount)
|
||||
assert.Equal(t, *original.Spec.GPUCount, *back.Spec.GPUCount)
|
||||
require.NotNil(t, back.Spec.Template)
|
||||
assert.Equal(t, original.Spec.Template.Image, back.Spec.Template.Image)
|
||||
require.NotNil(t, back.Spec.Template.UserNamespaces)
|
||||
assert.Equal(t, *original.Spec.Template.UserNamespaces, *back.Spec.Template.UserNamespaces)
|
||||
|
||||
// Policy round-trip
|
||||
require.NotNil(t, back.Spec.Policy)
|
||||
assert.Equal(t, uint32(3), back.Spec.Policy.Version)
|
||||
require.NotNil(t, back.Spec.Policy.Filesystem)
|
||||
assert.True(t, back.Spec.Policy.Filesystem.IncludeWorkdir)
|
||||
assert.Equal(t, []string{"/etc", "/usr/share"}, back.Spec.Policy.Filesystem.ReadOnly)
|
||||
assert.Equal(t, []string{"/tmp"}, back.Spec.Policy.Filesystem.ReadWrite)
|
||||
require.NotNil(t, back.Spec.Policy.Landlock)
|
||||
assert.Equal(t, "best_effort", back.Spec.Policy.Landlock.Compatibility)
|
||||
require.NotNil(t, back.Spec.Policy.Process)
|
||||
assert.Equal(t, "sandbox", back.Spec.Policy.Process.RunAsUser)
|
||||
assert.Equal(t, "sandbox-group", back.Spec.Policy.Process.RunAsGroup)
|
||||
require.Len(t, back.Spec.Policy.NetworkPolicies, 1)
|
||||
webRule, ok := back.Spec.Policy.NetworkPolicies["web"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "web", webRule.Name)
|
||||
require.Len(t, webRule.Endpoints, 1)
|
||||
assert.Equal(t, "api.example.com", webRule.Endpoints[0].Host)
|
||||
}
|
||||
|
||||
func TestSandboxSpecToProto(t *testing.T) {
|
||||
gpuCount := uint32(3)
|
||||
spec := &v1.SandboxSpec{
|
||||
LogLevel: "debug",
|
||||
Environment: map[string]string{"X": "Y"},
|
||||
Template: &v1.SandboxTemplate{
|
||||
Image: "img:spec",
|
||||
},
|
||||
Providers: []string{"prov"},
|
||||
GPUCount: &gpuCount,
|
||||
Policy: &v1.SandboxPolicy{
|
||||
Version: 2,
|
||||
Filesystem: &v1.FilesystemPolicy{
|
||||
ReadOnly: []string{"/etc"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := SandboxSpecToProto(spec)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, p)
|
||||
assert.Equal(t, "debug", p.LogLevel)
|
||||
assert.Equal(t, map[string]string{"X": "Y"}, p.Environment)
|
||||
assert.Equal(t, []string{"prov"}, p.Providers)
|
||||
require.NotNil(t, p.ResourceRequirements)
|
||||
assert.Equal(t, uint32(3), p.ResourceRequirements.Gpu.GetCount())
|
||||
require.NotNil(t, p.Template)
|
||||
assert.Equal(t, "img:spec", p.Template.Image)
|
||||
|
||||
// Policy conversion
|
||||
require.NotNil(t, p.Policy)
|
||||
assert.Equal(t, uint32(2), p.Policy.Version)
|
||||
require.NotNil(t, p.Policy.Filesystem)
|
||||
assert.Equal(t, []string{"/etc"}, p.Policy.Filesystem.ReadOnly)
|
||||
}
|
||||
|
||||
func TestSandboxSpecToProto_Nil(t *testing.T) {
|
||||
p, err := SandboxSpecToProto(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
|
||||
func TestSandboxSpecToProto_InvalidMapReturnsError(t *testing.T) {
|
||||
spec := &v1.SandboxSpec{
|
||||
Template: &v1.SandboxTemplate{
|
||||
Image: "img:v1",
|
||||
Resources: map[string]any{"bad": make(chan int)},
|
||||
},
|
||||
}
|
||||
|
||||
p, err := SandboxSpecToProto(spec)
|
||||
require.Error(t, err, "SandboxSpecToProto must return an error for unconvertible map values")
|
||||
assert.Nil(t, p)
|
||||
assert.Contains(t, err.Error(), "convert template resources")
|
||||
}
|
||||
|
||||
// Verify proto import is used (suppress unused import warning).
|
||||
var _ = proto.Marshal
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import "time"
|
||||
|
||||
// TimeFromMillis converts a millisecond epoch timestamp to time.Time.
|
||||
// A zero value returns the zero time.
|
||||
func TimeFromMillis(ms int64) time.Time {
|
||||
if ms == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.UnixMilli(ms).UTC()
|
||||
}
|
||||
|
||||
// MillisFromTime converts a time.Time to a millisecond epoch timestamp.
|
||||
// A zero time returns 0.
|
||||
func MillisFromTime(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.UnixMilli()
|
||||
}
|
||||
|
||||
// TimeFromMillisPtr converts a millisecond epoch timestamp to a *time.Time.
|
||||
// A zero value returns nil (the resource is not being deleted).
|
||||
func TimeFromMillisPtr(ms int64) *time.Time {
|
||||
if ms == 0 {
|
||||
return nil
|
||||
}
|
||||
t := time.UnixMilli(ms).UTC()
|
||||
return &t
|
||||
}
|
||||
|
||||
// MillisFromTimePtr converts a *time.Time to a millisecond epoch timestamp.
|
||||
// A nil pointer returns 0.
|
||||
func MillisFromTimePtr(t *time.Time) int64 {
|
||||
if t == nil {
|
||||
return 0
|
||||
}
|
||||
return t.UnixMilli()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTimeFromMillis(t *testing.T) {
|
||||
ms := int64(1719475200000) // 2024-06-27T08:00:00Z
|
||||
tm := TimeFromMillis(ms)
|
||||
assert.Equal(t, 2024, tm.Year())
|
||||
assert.Equal(t, time.June, tm.Month())
|
||||
assert.Equal(t, 27, tm.Day())
|
||||
}
|
||||
|
||||
func TestTimeFromMillis_Zero(t *testing.T) {
|
||||
tm := TimeFromMillis(0)
|
||||
assert.True(t, tm.IsZero())
|
||||
}
|
||||
|
||||
func TestMillisFromTime(t *testing.T) {
|
||||
tm := time.Date(2024, time.June, 27, 12, 0, 0, 0, time.UTC)
|
||||
ms := MillisFromTime(tm)
|
||||
assert.Equal(t, int64(1719489600000), ms)
|
||||
}
|
||||
|
||||
func TestMillisFromTime_Zero(t *testing.T) {
|
||||
ms := MillisFromTime(time.Time{})
|
||||
assert.Equal(t, int64(0), ms)
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
original := time.Date(2025, time.March, 15, 10, 30, 0, 0, time.UTC)
|
||||
ms := MillisFromTime(original)
|
||||
restored := TimeFromMillis(ms)
|
||||
assert.Equal(t, original.Unix(), restored.Unix())
|
||||
}
|
||||
|
||||
func TestTimeFromMillisPtr_NonZero(t *testing.T) {
|
||||
ms := int64(1719475200000)
|
||||
tp := TimeFromMillisPtr(ms)
|
||||
assert.NotNil(t, tp)
|
||||
assert.Equal(t, 2024, tp.Year())
|
||||
}
|
||||
|
||||
func TestTimeFromMillisPtr_Zero(t *testing.T) {
|
||||
tp := TimeFromMillisPtr(0)
|
||||
assert.Nil(t, tp)
|
||||
}
|
||||
|
||||
func TestMillisFromTimePtr_NonNil(t *testing.T) {
|
||||
tm := time.Date(2024, time.June, 27, 12, 0, 0, 0, time.UTC)
|
||||
ms := MillisFromTimePtr(&tm)
|
||||
assert.Equal(t, int64(1719489600000), ms)
|
||||
}
|
||||
|
||||
func TestMillisFromTimePtr_Nil(t *testing.T) {
|
||||
ms := MillisFromTimePtr(nil)
|
||||
assert.Equal(t, int64(0), ms)
|
||||
}
|
||||
|
||||
func TestPtrRoundTrip(t *testing.T) {
|
||||
original := time.Date(2025, time.March, 15, 10, 30, 0, 0, time.UTC)
|
||||
ms := MillisFromTimePtr(&original)
|
||||
restored := TimeFromMillisPtr(ms)
|
||||
assert.NotNil(t, restored)
|
||||
assert.Equal(t, original.Unix(), restored.Unix())
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package grpc provides gRPC connection setup utilities.
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// TLSParams holds TLS settings without importing the v1 package.
|
||||
type TLSParams struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
CAFile string
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// NewConnection creates a gRPC client connection.
|
||||
// The address may include an http:// or https:// scheme (as written by the
|
||||
// upstream gateway). The scheme drives transport selection: http:// uses
|
||||
// plaintext, https:// or no scheme uses TLS.
|
||||
func NewConnection(address string, tlsCfg *TLSParams, auth credentials.PerRPCCredentials) (*grpc.ClientConn, error) {
|
||||
usePlaintext := false
|
||||
if strings.HasPrefix(address, "http://") {
|
||||
usePlaintext = true
|
||||
address = strings.TrimPrefix(address, "http://")
|
||||
} else {
|
||||
address = strings.TrimPrefix(address, "https://")
|
||||
}
|
||||
opts := []grpc.DialOption{}
|
||||
|
||||
if usePlaintext {
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
} else if tlsCfg != nil {
|
||||
creds, err := buildTLSCredentials(tlsCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tls config: %w", err)
|
||||
}
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
} else {
|
||||
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})))
|
||||
}
|
||||
|
||||
if auth != nil {
|
||||
if usePlaintext && auth.RequireTransportSecurity() {
|
||||
return nil, fmt.Errorf("grpc connect: auth provider requires transport security but address uses plaintext (http://)")
|
||||
}
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(auth))
|
||||
}
|
||||
|
||||
conn, err := grpc.NewClient(address, opts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpc connect: %w", err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func buildTLSCredentials(cfg *TLSParams) (credentials.TransportCredentials, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: cfg.Insecure, //nolint:gosec // user-requested skip for dev gateways
|
||||
}
|
||||
|
||||
if cfg.CAFile != "" {
|
||||
caCert, err := os.ReadFile(cfg.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read CA file: %w", err)
|
||||
}
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(caCert) {
|
||||
return nil, fmt.Errorf("invalid CA certificate")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
if cfg.CertFile != "" && cfg.KeyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load client cert: %w", err)
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
} else if cfg.CertFile != "" || cfg.KeyFile != "" {
|
||||
return nil, fmt.Errorf("both CertFile and KeyFile must be provided for client certificate authentication")
|
||||
}
|
||||
|
||||
return credentials.NewTLS(tlsConfig), nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
func TestNewConnectionHTTPSchemeUsesPlaintext(t *testing.T) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer func() { _ = lis.Close() }()
|
||||
|
||||
srv := grpc.NewServer()
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
defer srv.Stop()
|
||||
|
||||
conn, err := NewConnection("http://"+lis.Addr().String(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConnection with http:// scheme failed: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
}
|
||||
|
||||
func TestNewConnectionHTTPSSchemeUsesTLS(t *testing.T) {
|
||||
// https:// with nil TLS config should default to system TLS.
|
||||
// We cannot dial a real TLS server here, but we can verify the
|
||||
// connection is created (it will fail on handshake, not on dial).
|
||||
conn, err := NewConnection("https://127.0.0.1:1", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConnection with https:// scheme should not fail on create: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
}
|
||||
|
||||
func TestNewConnectionNoSchemeUsesTLS(t *testing.T) {
|
||||
conn, err := NewConnection("127.0.0.1:1", nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConnection without scheme should not fail on create: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
}
|
||||
|
||||
func TestNewConnectionInsecureTLSConfig(t *testing.T) {
|
||||
// Insecure: true means TLS with InsecureSkipVerify, not plaintext.
|
||||
// We can verify the connection is created (handshake will fail since
|
||||
// the server is not TLS, but NewClient itself should succeed).
|
||||
conn, err := NewConnection("127.0.0.1:1", &TLSParams{Insecure: true}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConnection with Insecure TLS config failed: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
}
|
||||
|
||||
func TestNewConnectionHTTPWithSecureAuthRejects(t *testing.T) {
|
||||
auth := &testTokenAuth{token: "dev-token", requireSecurity: true}
|
||||
_, err := NewConnection("http://127.0.0.1:1", nil, auth)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when using http:// with auth that requires transport security")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConnectionHTTPWithInsecureAuth(t *testing.T) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer func() { _ = lis.Close() }()
|
||||
|
||||
srv := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
defer srv.Stop()
|
||||
|
||||
auth := &testTokenAuth{token: "dev-token", requireSecurity: false}
|
||||
conn, err := NewConnection("http://"+lis.Addr().String(), nil, auth)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConnection with http:// + insecure auth failed: %v", err)
|
||||
}
|
||||
defer func() { _ = conn.Close() }()
|
||||
}
|
||||
|
||||
type testTokenAuth struct {
|
||||
token string
|
||||
requireSecurity bool
|
||||
}
|
||||
|
||||
func (a *testTokenAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
return map[string]string{"authorization": "Bearer " + a.token}, nil
|
||||
}
|
||||
|
||||
func (a *testTokenAuth) RequireTransportSecurity() bool {
|
||||
return a.requireSecurity
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// Logger defines structured logging for the SDK. Compatible with logr.Logger
|
||||
// and slog.Logger adapters.
|
||||
type Logger = types.Logger
|
||||
@@ -0,0 +1,32 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// CreateOptions configures resource creation.
|
||||
type CreateOptions = types.CreateOptions
|
||||
|
||||
// GetOptions configures resource retrieval.
|
||||
type GetOptions = types.GetOptions
|
||||
|
||||
// ListOptions configures resource listing with pagination and filtering.
|
||||
type ListOptions = types.ListOptions
|
||||
|
||||
// DeleteOptions configures resource deletion.
|
||||
type DeleteOptions = types.DeleteOptions
|
||||
|
||||
// UpdateOptions configures resource updates.
|
||||
type UpdateOptions = types.UpdateOptions
|
||||
|
||||
// WatchOptions configures watch behavior.
|
||||
type WatchOptions = types.WatchOptions
|
||||
|
||||
// WaitOptions configures wait behavior. Use context for timeout control.
|
||||
type WaitOptions = types.WaitOptions
|
||||
|
||||
// ExecOptions configures command execution.
|
||||
type ExecOptions = types.ExecOptions
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// SandboxPolicy is the top-level security policy configuration for a sandbox.
|
||||
type SandboxPolicy = types.SandboxPolicy
|
||||
|
||||
// FilesystemPolicy controls which directories the sandbox can access.
|
||||
type FilesystemPolicy = types.FilesystemPolicy
|
||||
|
||||
// LandlockPolicy configures the Linux Landlock LSM.
|
||||
type LandlockPolicy = types.LandlockPolicy
|
||||
|
||||
// ProcessPolicy controls the user and group identity for sandboxed processes.
|
||||
type ProcessPolicy = types.ProcessPolicy
|
||||
|
||||
// PolicyChunk represents a single proposed policy change in the draft inbox.
|
||||
type PolicyChunk = types.PolicyChunk
|
||||
|
||||
// DraftPolicy contains the full draft policy state returned by GetDraft.
|
||||
type DraftPolicy = types.DraftPolicy
|
||||
|
||||
// PolicyStatusResult contains the status of a sandbox's policy.
|
||||
type PolicyStatusResult = types.PolicyStatusResult
|
||||
|
||||
// SandboxPolicyRevision represents a versioned policy revision for a sandbox.
|
||||
type SandboxPolicyRevision = types.SandboxPolicyRevision
|
||||
|
||||
// PolicyLoadStatus represents the load state of a policy revision.
|
||||
type PolicyLoadStatus = types.PolicyLoadStatus
|
||||
|
||||
// PolicyLoadStatus constants re-exported from types package.
|
||||
const (
|
||||
PolicyLoadStatusUnspecified = types.PolicyLoadStatusUnspecified
|
||||
PolicyLoadStatusPending = types.PolicyLoadStatusPending
|
||||
PolicyLoadStatusLoaded = types.PolicyLoadStatusLoaded
|
||||
PolicyLoadStatusFailed = types.PolicyLoadStatusFailed
|
||||
PolicyLoadStatusSuperseded = types.PolicyLoadStatusSuperseded
|
||||
)
|
||||
|
||||
// ApproveResult contains the result of approving a single draft chunk.
|
||||
type ApproveResult = types.ApproveResult
|
||||
|
||||
// ApproveAllResult contains the result of approving all draft chunks.
|
||||
type ApproveAllResult = types.ApproveAllResult
|
||||
|
||||
// UndoResult contains the result of undoing a draft chunk approval.
|
||||
type UndoResult = types.UndoResult
|
||||
|
||||
// ClearResult contains the result of clearing all draft chunks.
|
||||
type ClearResult = types.ClearResult
|
||||
|
||||
// DraftHistoryEntry represents a single event in the draft policy history.
|
||||
type DraftHistoryEntry = types.DraftHistoryEntry
|
||||
|
||||
// GetDraftOption configures a GetDraft call.
|
||||
type GetDraftOption = types.GetDraftOption
|
||||
|
||||
// WithStatusFilter filters draft chunks by approval status.
|
||||
var WithStatusFilter = types.WithStatusFilter
|
||||
|
||||
// ApproveAllOption configures an ApproveAllDraftChunks call.
|
||||
type ApproveAllOption = types.ApproveAllOption
|
||||
|
||||
// WithIncludeSecurityFlagged includes security-flagged chunks in bulk approval.
|
||||
var WithIncludeSecurityFlagged = types.WithIncludeSecurityFlagged
|
||||
|
||||
// GetStatusOption configures a GetStatus call.
|
||||
type GetStatusOption = types.GetStatusOption
|
||||
|
||||
// WithVersion queries a specific policy version instead of the latest.
|
||||
var WithVersion = types.WithVersion
|
||||
|
||||
// ListPolicyOption configures a List call.
|
||||
type ListPolicyOption = types.ListPolicyOption
|
||||
|
||||
// WithLimit sets the maximum number of revisions to return.
|
||||
var WithLimit = types.WithLimit
|
||||
|
||||
// WithOffset sets the pagination offset.
|
||||
var WithOffset = types.WithOffset
|
||||
|
||||
// PolicyInterface defines operations for managing sandbox policy drafts,
|
||||
// approvals, and revision history.
|
||||
type PolicyInterface interface {
|
||||
// GetDraft retrieves the current draft policy for a sandbox, including
|
||||
// all pending, approved, and rejected chunks. Use WithStatusFilter to
|
||||
// return only chunks matching a specific status.
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if the
|
||||
// sandbox name is empty; Unimplemented by the fake client.
|
||||
GetDraft(ctx context.Context, workspace, sandboxName string, opts ...GetDraftOption) (*DraftPolicy, error)
|
||||
|
||||
// ApproveDraftChunk approves a single pending draft chunk, merging
|
||||
// its proposed rule into the active policy.
|
||||
//
|
||||
// Errors: NotFound if the sandbox or chunk does not exist;
|
||||
// InvalidArgument if the sandbox name or chunk ID is empty;
|
||||
// Conflict if the chunk has already been approved or rejected;
|
||||
// Unimplemented by the fake client.
|
||||
ApproveDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*ApproveResult, error)
|
||||
|
||||
// RejectDraftChunk rejects a single pending draft chunk with an
|
||||
// optional reason that is fed to future LLM analysis context.
|
||||
//
|
||||
// Errors: NotFound if the sandbox or chunk does not exist;
|
||||
// InvalidArgument if the sandbox name or chunk ID is empty;
|
||||
// Conflict if the chunk has already been approved or rejected;
|
||||
// Unimplemented by the fake client.
|
||||
RejectDraftChunk(ctx context.Context, workspace, sandboxName, chunkID, reason string) error
|
||||
|
||||
// ApproveAllDraftChunks approves all pending draft chunks at once.
|
||||
// By default, security-flagged chunks are skipped. Use
|
||||
// WithIncludeSecurityFlagged to include them.
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if
|
||||
// the sandbox name is empty; Unimplemented by the fake client.
|
||||
ApproveAllDraftChunks(ctx context.Context, workspace, sandboxName string, opts ...ApproveAllOption) (*ApproveAllResult, error)
|
||||
|
||||
// ClearDraftChunks removes all pending draft chunks for a sandbox.
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if
|
||||
// the sandbox name is empty; Unimplemented by the fake client.
|
||||
ClearDraftChunks(ctx context.Context, workspace, sandboxName string) (*ClearResult, error)
|
||||
|
||||
// GetDraftHistory returns the chronological decision history for a
|
||||
// sandbox's draft policy (approvals, rejections, edits, undos, clears).
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if
|
||||
// the sandbox name is empty; Unimplemented by the fake client.
|
||||
GetDraftHistory(ctx context.Context, workspace, sandboxName string) ([]DraftHistoryEntry, error)
|
||||
|
||||
// GetStatus retrieves the policy status for a sandbox, including the
|
||||
// queried revision and the active version. Use WithVersion to query a
|
||||
// specific version instead of the latest.
|
||||
//
|
||||
// Errors: NotFound if the sandbox or requested version does not exist;
|
||||
// InvalidArgument if the sandbox name is empty;
|
||||
// Unimplemented by the fake client.
|
||||
GetStatus(ctx context.Context, workspace, sandboxName string, opts ...GetStatusOption) (*PolicyStatusResult, error)
|
||||
|
||||
// List returns policy revisions for a sandbox, ordered by version.
|
||||
// Use WithLimit and WithOffset for pagination.
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if
|
||||
// the sandbox name is empty; Unimplemented by the fake client.
|
||||
List(ctx context.Context, workspace string, opts ...ListPolicyOption) ([]SandboxPolicyRevision, error)
|
||||
|
||||
// EditDraftChunk replaces the proposed rule of a pending draft chunk
|
||||
// with the given network policy rule.
|
||||
//
|
||||
// Errors: NotFound if the sandbox or chunk does not exist;
|
||||
// InvalidArgument if the sandbox name, chunk ID, or proposed rule is
|
||||
// empty/nil; Conflict if the chunk is not in a pending state;
|
||||
// Unimplemented by the fake client.
|
||||
EditDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string, proposedRule *NetworkPolicyRule) error
|
||||
|
||||
// UndoDraftChunk reverses a previously approved chunk, removing its
|
||||
// merged rule from the active policy.
|
||||
//
|
||||
// Errors: NotFound if the sandbox or chunk does not exist;
|
||||
// InvalidArgument if the sandbox name or chunk ID is empty;
|
||||
// Conflict if the chunk has not been approved;
|
||||
// Unimplemented by the fake client.
|
||||
UndoDraftChunk(ctx context.Context, workspace, sandboxName, chunkID string) (*UndoResult, error)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// ProviderProfile represents a provider type template.
|
||||
type ProviderProfile = types.ProviderProfile
|
||||
|
||||
// ProfileCredential defines a single credential required by a provider profile.
|
||||
type ProfileCredential = types.ProfileCredential
|
||||
|
||||
// ProfileCategory classifies a provider profile.
|
||||
type ProfileCategory = types.ProfileCategory
|
||||
|
||||
// NetworkEndpoint describes a network endpoint provided by a profile.
|
||||
type NetworkEndpoint = types.NetworkEndpoint
|
||||
|
||||
// NetworkBinary describes a binary artifact provided by a profile.
|
||||
type NetworkBinary = types.NetworkBinary
|
||||
|
||||
// ProfileDiscovery holds local discovery configuration for a profile.
|
||||
type ProfileDiscovery = types.ProfileDiscovery
|
||||
|
||||
// ProfileImportItem is an item submitted for profile import or lint validation.
|
||||
type ProfileImportItem = types.ProfileImportItem
|
||||
|
||||
// ProfileDiagnostic is a validation finding from Import, Update, or Lint.
|
||||
type ProfileDiagnostic = types.ProfileDiagnostic
|
||||
|
||||
// ImportResult holds the result of a profile import operation.
|
||||
type ImportResult = types.ImportResult
|
||||
|
||||
// UpdateResult holds the result of a profile update operation.
|
||||
type UpdateResult = types.UpdateResult
|
||||
|
||||
// LintResult holds the result of a profile lint operation.
|
||||
type LintResult = types.LintResult
|
||||
|
||||
// ProfileCategory values.
|
||||
const (
|
||||
ProfileCategoryOther = types.ProfileCategoryOther
|
||||
ProfileCategoryInference = types.ProfileCategoryInference
|
||||
ProfileCategoryAgent = types.ProfileCategoryAgent
|
||||
ProfileCategorySourceControl = types.ProfileCategorySourceControl
|
||||
ProfileCategoryMessaging = types.ProfileCategoryMessaging
|
||||
ProfileCategoryData = types.ProfileCategoryData
|
||||
ProfileCategoryKnowledge = types.ProfileCategoryKnowledge
|
||||
)
|
||||
|
||||
// ProfileInterface defines operations for managing provider profiles.
|
||||
type ProfileInterface interface {
|
||||
// List returns all provider profiles.
|
||||
List(ctx context.Context, workspace string, opts ...ListOptions) ([]*ProviderProfile, error)
|
||||
// Get retrieves a provider profile by ID.
|
||||
Get(ctx context.Context, workspace, id string) (*ProviderProfile, error)
|
||||
// Import submits profiles for import and returns the result with diagnostics.
|
||||
Import(ctx context.Context, workspace string, items []ProfileImportItem) (*ImportResult, error)
|
||||
// Update replaces an existing profile identified by ID and expected resource version.
|
||||
Update(ctx context.Context, workspace, id string, expectedResourceVersion uint64, item ProfileImportItem) (*UpdateResult, error)
|
||||
// Lint validates profiles without persisting them and returns diagnostics.
|
||||
Lint(ctx context.Context, workspace string, items []ProfileImportItem) (*LintResult, error)
|
||||
// Delete removes a provider profile by ID. Returns true if deleted.
|
||||
Delete(ctx context.Context, workspace, id string) (bool, error)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// Provider represents an AI provider registration.
|
||||
type Provider = types.Provider
|
||||
|
||||
// ProviderSpec holds provider-specific configuration and credentials.
|
||||
type ProviderSpec = types.ProviderSpec
|
||||
|
||||
// ProviderInterface defines CRUD and Ensure operations on providers,
|
||||
// plus sub-client accessors for profiles and credential refresh.
|
||||
type ProviderInterface interface {
|
||||
Create(ctx context.Context, workspace string, provider *Provider) (*Provider, error)
|
||||
Get(ctx context.Context, workspace, name string) (*Provider, error)
|
||||
List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Provider, error)
|
||||
Update(ctx context.Context, workspace string, provider *Provider) (*Provider, error)
|
||||
Delete(ctx context.Context, workspace, name string) error
|
||||
Ensure(ctx context.Context, workspace string, provider *Provider) (*Provider, error)
|
||||
Profiles() ProfileInterface
|
||||
Refresh() RefreshInterface
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// RefreshStrategy describes how credentials are refreshed.
|
||||
type RefreshStrategy = types.RefreshStrategy
|
||||
|
||||
// RefreshStatus reports the current state of credential refresh for a provider credential.
|
||||
type RefreshStatus = types.RefreshStatus
|
||||
|
||||
// RefreshConfig holds configuration parameters for credential refresh.
|
||||
type RefreshConfig = types.RefreshConfig
|
||||
|
||||
// RefreshStrategy values.
|
||||
const (
|
||||
RefreshStrategyStatic = types.RefreshStrategyStatic
|
||||
RefreshStrategyExternal = types.RefreshStrategyExternal
|
||||
RefreshStrategyOAuth2RefreshToken = types.RefreshStrategyOAuth2RefreshToken
|
||||
RefreshStrategyOAuth2ClientCredentials = types.RefreshStrategyOAuth2ClientCredentials
|
||||
RefreshStrategyGoogleServiceAccountJWT = types.RefreshStrategyGoogleServiceAccountJWT
|
||||
RefreshStrategyAWSStsAssumeRole = types.RefreshStrategyAWSStsAssumeRole
|
||||
)
|
||||
|
||||
// RefreshInterface defines operations for managing provider credential refresh.
|
||||
type RefreshInterface interface {
|
||||
// GetStatus returns the refresh status for a provider's credential.
|
||||
// If credentialKey is empty, statuses for all credentials are returned.
|
||||
GetStatus(ctx context.Context, workspace, provider, credentialKey string) ([]*RefreshStatus, error)
|
||||
// Configure sets up credential refresh for a provider credential.
|
||||
Configure(ctx context.Context, workspace string, config *RefreshConfig) (*RefreshStatus, error)
|
||||
// Rotate triggers an immediate credential rotation.
|
||||
Rotate(ctx context.Context, workspace, provider, credentialKey string) (*RefreshStatus, error)
|
||||
// Delete removes credential refresh configuration. Returns true if deleted.
|
||||
Delete(ctx context.Context, workspace, provider, credentialKey string) (bool, error)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// Sandbox represents a sandbox instance.
|
||||
type Sandbox = types.Sandbox
|
||||
|
||||
// SandboxSpec holds the desired state of a sandbox.
|
||||
type SandboxSpec = types.SandboxSpec
|
||||
|
||||
// SandboxTemplate defines the container template for a sandbox.
|
||||
type SandboxTemplate = types.SandboxTemplate
|
||||
|
||||
// SandboxStatus holds the observed state of a sandbox.
|
||||
type SandboxStatus = types.SandboxStatus
|
||||
|
||||
// SandboxCondition describes an observed condition of a sandbox.
|
||||
type SandboxCondition = types.SandboxCondition
|
||||
|
||||
// AttachProviderResult holds the result of attaching a provider to a sandbox.
|
||||
type AttachProviderResult = types.AttachProviderResult
|
||||
|
||||
// DetachProviderResult holds the result of detaching a provider from a sandbox.
|
||||
type DetachProviderResult = types.DetachProviderResult
|
||||
|
||||
// LogLine represents a single log entry from a sandbox.
|
||||
type LogLine = types.LogLine
|
||||
|
||||
// LogResult contains the result of a GetLogs call.
|
||||
type LogResult = types.LogResult
|
||||
|
||||
// LogOption configures a GetLogs call.
|
||||
type LogOption = types.LogOption
|
||||
|
||||
// WithLogLines sets the maximum number of log lines to return.
|
||||
var WithLogLines = types.WithLogLines
|
||||
|
||||
// WithLogSince filters logs to entries at or after the given time.
|
||||
var WithLogSince = types.WithLogSince
|
||||
|
||||
// WithLogSources filters logs by source (e.g., "gateway", "sandbox").
|
||||
var WithLogSources = types.WithLogSources
|
||||
|
||||
// WithLogMinLevel sets the minimum log level to include.
|
||||
var WithLogMinLevel = types.WithLogMinLevel
|
||||
|
||||
// SandboxInterface defines lifecycle operations on sandboxes.
|
||||
type SandboxInterface interface {
|
||||
Create(ctx context.Context, workspace, name string, spec *SandboxSpec, labels map[string]string) (*Sandbox, error)
|
||||
Get(ctx context.Context, workspace, name string) (*Sandbox, error)
|
||||
List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error)
|
||||
Delete(ctx context.Context, workspace, name string) error
|
||||
AttachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*AttachProviderResult, error)
|
||||
DetachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*DetachProviderResult, error)
|
||||
ListProviders(ctx context.Context, workspace, sandboxName string) ([]*Provider, error)
|
||||
WaitReady(ctx context.Context, workspace, name string, opts ...WaitOptions) (*Sandbox, error)
|
||||
Watch(ctx context.Context, workspace, name string, opts ...WatchOptions) (WatchInterface[*Sandbox], error)
|
||||
// GetLogs retrieves log entries for a sandbox. The sandbox is resolved
|
||||
// by name (an internal Get call translates name to ID). Use
|
||||
// WithLogLines, WithLogSince, WithLogSources, and WithLogMinLevel to
|
||||
// filter the results.
|
||||
//
|
||||
// Errors: NotFound if the sandbox does not exist; InvalidArgument if
|
||||
// the sandbox name is empty; Unimplemented by the fake client.
|
||||
GetLogs(ctx context.Context, workspace, sandboxName string, opts ...LogOption) (*LogResult, error)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/internal/converter"
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
pb "github.com/NVIDIA/OpenShell/sdk/go/proto/openshellv1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const defaultPollInterval = 500 * time.Millisecond
|
||||
|
||||
type sandboxClient struct {
|
||||
client pb.OpenShellClient
|
||||
}
|
||||
|
||||
func newSandboxClient(conn grpc.ClientConnInterface) *sandboxClient {
|
||||
return &sandboxClient{client: pb.NewOpenShellClient(conn)}
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Create(ctx context.Context, workspace, name string, spec *SandboxSpec, labels map[string]string) (*Sandbox, error) {
|
||||
pbSpec, err := converter.SandboxSpecToProto(spec)
|
||||
if err != nil {
|
||||
return nil, &StatusError{Code: ErrorInvalidArgument, Message: err.Error()}
|
||||
}
|
||||
resp, err := s.client.CreateSandbox(ctx, &pb.CreateSandboxRequest{
|
||||
Name: name,
|
||||
Spec: pbSpec,
|
||||
Labels: labels,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
return converter.SandboxFromProto(resp.GetSandbox()), nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Get(ctx context.Context, workspace, name string) (*Sandbox, error) {
|
||||
resp, err := s.client.GetSandbox(ctx, &pb.GetSandboxRequest{
|
||||
Name: name,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
return converter.SandboxFromProto(resp.GetSandbox()), nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) List(ctx context.Context, workspace string, opts ...ListOptions) ([]*Sandbox, error) {
|
||||
req := &pb.ListSandboxesRequest{
|
||||
Workspace: workspace,
|
||||
}
|
||||
if len(opts) > 0 {
|
||||
if opts[0].Limit > 0 {
|
||||
req.Limit = uint32(opts[0].Limit)
|
||||
}
|
||||
if opts[0].Offset > 0 {
|
||||
req.Offset = uint32(opts[0].Offset)
|
||||
}
|
||||
req.LabelSelector = opts[0].LabelSelector
|
||||
req.AllWorkspaces = opts[0].AllWorkspaces
|
||||
}
|
||||
|
||||
resp, err := s.client.ListSandboxes(ctx, req)
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
|
||||
sandboxes := make([]*Sandbox, 0, len(resp.GetSandboxes()))
|
||||
for _, proto := range resp.GetSandboxes() {
|
||||
sandboxes = append(sandboxes, converter.SandboxFromProto(proto))
|
||||
}
|
||||
return sandboxes, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Delete(ctx context.Context, workspace, name string) error {
|
||||
_, err := s.client.DeleteSandbox(ctx, &pb.DeleteSandboxRequest{
|
||||
Name: name,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return converter.FromGRPCError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) AttachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*AttachProviderResult, error) {
|
||||
resp, err := s.client.AttachSandboxProvider(ctx, &pb.AttachSandboxProviderRequest{
|
||||
SandboxName: sandboxName,
|
||||
ProviderName: providerName,
|
||||
ExpectedResourceVersion: expectedResourceVersion,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
return &AttachProviderResult{
|
||||
Sandbox: converter.SandboxFromProto(resp.GetSandbox()),
|
||||
Attached: resp.GetAttached(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) DetachProvider(ctx context.Context, workspace, sandboxName, providerName string, expectedResourceVersion uint64) (*DetachProviderResult, error) {
|
||||
resp, err := s.client.DetachSandboxProvider(ctx, &pb.DetachSandboxProviderRequest{
|
||||
SandboxName: sandboxName,
|
||||
ProviderName: providerName,
|
||||
ExpectedResourceVersion: expectedResourceVersion,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
return &DetachProviderResult{
|
||||
Sandbox: converter.SandboxFromProto(resp.GetSandbox()),
|
||||
Detached: resp.GetDetached(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) ListProviders(ctx context.Context, workspace, sandboxName string) ([]*Provider, error) {
|
||||
resp, err := s.client.ListSandboxProviders(ctx, &pb.ListSandboxProvidersRequest{
|
||||
SandboxName: sandboxName,
|
||||
Workspace: workspace,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
|
||||
providers := make([]*Provider, 0, len(resp.GetProviders()))
|
||||
for _, proto := range resp.GetProviders() {
|
||||
providers = append(providers, converter.ProviderFromProto(proto))
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) WaitReady(ctx context.Context, workspace, name string, opts ...WaitOptions) (*Sandbox, error) {
|
||||
interval := defaultPollInterval
|
||||
if len(opts) > 0 && opts[0].PollInterval > 0 {
|
||||
interval = opts[0].PollInterval
|
||||
}
|
||||
|
||||
sb, err := s.Get(ctx, workspace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if sb.Status.Phase == SandboxReady {
|
||||
return sb, nil
|
||||
}
|
||||
if sb.Status.Phase == SandboxError {
|
||||
return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q is in error state", name)}
|
||||
}
|
||||
if sb.Status.Phase == SandboxDeleting {
|
||||
return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q is being deleted", name)}
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, contextError(ctx.Err())
|
||||
case <-ticker.C:
|
||||
sb, err = s.Get(ctx, workspace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sb.Status.Phase == SandboxReady {
|
||||
return sb, nil
|
||||
}
|
||||
if sb.Status.Phase == SandboxError {
|
||||
return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q is in error state", name)}
|
||||
}
|
||||
if sb.Status.Phase == SandboxDeleting {
|
||||
return nil, &StatusError{Code: ErrorInternal, Message: fmt.Sprintf("sandbox %q is being deleted", name)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts ...WatchOptions) (WatchInterface[*Sandbox], error) {
|
||||
if name == "" {
|
||||
return nil, &StatusError{Code: ErrorInvalidArgument, Message: "sandbox name must not be empty"}
|
||||
}
|
||||
|
||||
var watchOpts WatchOptions
|
||||
if len(opts) > 0 {
|
||||
watchOpts = opts[0]
|
||||
}
|
||||
|
||||
// Resolve sandbox name to ID — the proto RPC takes Id, not name.
|
||||
sb, err := s.Get(ctx, workspace, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
streamCtx, streamCancel := context.WithCancel(ctx)
|
||||
stream, err := s.client.WatchSandbox(streamCtx, &pb.WatchSandboxRequest{
|
||||
Id: sb.ID,
|
||||
FollowStatus: true,
|
||||
StopOnTerminal: watchOpts.StopOnTerminal,
|
||||
})
|
||||
if err != nil {
|
||||
streamCancel()
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
|
||||
first, err := stream.Recv()
|
||||
if err != nil {
|
||||
streamCancel()
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
|
||||
ch := make(chan Event[*Sandbox], 64)
|
||||
w := newWatcher(ch, streamCancel)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer streamCancel()
|
||||
ev := first
|
||||
isFirst := true
|
||||
for {
|
||||
if sbPayload, ok := ev.Payload.(*pb.SandboxStreamEvent_Sandbox); ok && sbPayload.Sandbox != nil {
|
||||
sandbox := converter.SandboxFromProto(sbPayload.Sandbox)
|
||||
eventType := EventModified
|
||||
if isFirst {
|
||||
eventType = EventAdded
|
||||
isFirst = false
|
||||
} else if sandbox.Status.Phase == SandboxDeleting {
|
||||
eventType = EventDeleted
|
||||
}
|
||||
select {
|
||||
case ch <- Event[*Sandbox]{Type: eventType, Object: sandbox}:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
// StopOnTerminal: close watcher after delivering a terminal phase event
|
||||
if watchOpts.StopOnTerminal && (sandbox.Status.Phase == SandboxReady || sandbox.Status.Phase == SandboxError) {
|
||||
w.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
var recvErr error
|
||||
ev, recvErr = stream.Recv()
|
||||
if recvErr != nil {
|
||||
if recvErr != io.EOF {
|
||||
select {
|
||||
case ch <- Event[*Sandbox]{Type: EventError, Err: converter.FromGRPCError(recvErr)}:
|
||||
case <-w.done:
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) GetLogs(ctx context.Context, workspace, sandboxName string, opts ...LogOption) (*LogResult, error) {
|
||||
// Resolve sandbox name to ID — the proto RPC takes SandboxId, not name.
|
||||
sb, err := s.Get(ctx, workspace, sandboxName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := types.ApplyLogOptions(opts)
|
||||
req := &pb.GetSandboxLogsRequest{
|
||||
SandboxId: sb.ID,
|
||||
Lines: cfg.Lines(),
|
||||
Sources: cfg.Sources(),
|
||||
MinLevel: cfg.MinLevel(),
|
||||
Workspace: workspace,
|
||||
}
|
||||
if !cfg.Since().IsZero() {
|
||||
req.SinceMs = converter.MillisFromTime(cfg.Since())
|
||||
}
|
||||
|
||||
resp, err := s.client.GetSandboxLogs(ctx, req)
|
||||
if err != nil {
|
||||
return nil, converter.FromGRPCError(err)
|
||||
}
|
||||
return converter.LogResultFromProto(resp), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// ServiceEndpoint represents an exposed HTTP service endpoint within a sandbox.
|
||||
type ServiceEndpoint = types.ServiceEndpoint
|
||||
|
||||
// ServiceInterface defines operations for managing sandbox service endpoints.
|
||||
type ServiceInterface interface {
|
||||
// Expose creates a new service endpoint in the given sandbox.
|
||||
Expose(ctx context.Context, workspace, sandboxName, serviceName string, targetPort uint32, domain bool) (*ServiceEndpoint, error)
|
||||
// Get retrieves a service endpoint by sandbox and service name.
|
||||
Get(ctx context.Context, workspace, sandboxName, serviceName string) (*ServiceEndpoint, error)
|
||||
// List returns all service endpoints for a sandbox. An empty sandboxName returns endpoints across all sandboxes.
|
||||
List(ctx context.Context, workspace, sandboxName string, opts ...ListOptions) ([]*ServiceEndpoint, error)
|
||||
// Delete removes a service endpoint by sandbox and service name.
|
||||
Delete(ctx context.Context, workspace, sandboxName, serviceName string) error
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// SSHSession represents an SSH session created for a sandbox.
|
||||
type SSHSession = types.SSHSession
|
||||
|
||||
// tunnelConfig accumulates options for the Tunnel method.
|
||||
type tunnelConfig struct {
|
||||
serviceID string
|
||||
}
|
||||
|
||||
// TunnelOption configures an SSH tunnel opened via [SSHInterface.Tunnel].
|
||||
type TunnelOption func(*tunnelConfig)
|
||||
|
||||
// WithTunnelServiceID sets an optional service identifier on the tunnel's
|
||||
// init frame for audit and correlation purposes.
|
||||
func WithTunnelServiceID(id string) TunnelOption {
|
||||
return func(c *tunnelConfig) {
|
||||
c.serviceID = id
|
||||
}
|
||||
}
|
||||
|
||||
// SSHInterface defines operations for managing SSH sessions.
|
||||
type SSHInterface interface {
|
||||
// CreateSession creates a new SSH session for the given sandbox.
|
||||
// The returned SSHSession contains connection details including the
|
||||
// sensitive Token field that must not be logged.
|
||||
//
|
||||
// Note: CreateSession accepts a raw sandbox ID, not a name.
|
||||
// For name-based access with automatic session lifecycle management,
|
||||
// prefer [SSHInterface.Tunnel] which resolves sandbox names internally
|
||||
// and revokes the session on Close.
|
||||
CreateSession(ctx context.Context, workspace, sandboxID string) (*SSHSession, error)
|
||||
// RevokeSession revokes an existing SSH session by its token.
|
||||
// Returns true if the session was actively revoked, false if it was
|
||||
// already expired or not found.
|
||||
RevokeSession(ctx context.Context, workspace, token string) (bool, error)
|
||||
// Tunnel opens a bidirectional SSH tunnel to the given port inside a
|
||||
// sandbox. It combines CreateSession and ForwardTcp(SshRelayTarget)
|
||||
// into a single call with automatic session cleanup on Close.
|
||||
//
|
||||
// The sandboxName is resolved to a sandbox ID internally. Port must
|
||||
// be in the range 1-65535.
|
||||
//
|
||||
// Errors: InvalidArgument if port is out of range or sandboxName is
|
||||
// empty; NotFound if the sandbox does not exist; Unimplemented by
|
||||
// the fake client; Unavailable if the client is closed.
|
||||
Tunnel(ctx context.Context, workspace, sandboxName string, port uint32, opts ...TunnelOption) (io.ReadWriteCloser, error)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
func stubError(method string) error {
|
||||
return &StatusError{
|
||||
Code: ErrorUnimplemented,
|
||||
Message: method + " not yet available - see https://github.com/NVIDIA/OpenShell/issues/2270",
|
||||
}
|
||||
}
|
||||
|
||||
// stubExec implements ExecInterface as a placeholder.
|
||||
type stubExec struct{}
|
||||
|
||||
func (s *stubExec) Run(_ context.Context, _, _ string, _ []string, _ ...ExecOptions) (*ExecResult, error) {
|
||||
return nil, stubError("Exec.Run")
|
||||
}
|
||||
func (s *stubExec) Stream(_ context.Context, _, _ string, _ []string, _ ...ExecOptions) (ExecStream, error) {
|
||||
return nil, stubError("Exec.Stream")
|
||||
}
|
||||
func (s *stubExec) Interactive(_ context.Context, _, _ string, _ []string, _, _ uint32, _ ...ExecOptions) (InteractiveSession, error) {
|
||||
return nil, stubError("Exec.Interactive")
|
||||
}
|
||||
|
||||
// stubFiles implements FileInterface as a placeholder.
|
||||
type stubFiles struct{}
|
||||
|
||||
func (s *stubFiles) Upload(_ context.Context, _, _, _, _ string) error {
|
||||
return stubError("Files.Upload")
|
||||
}
|
||||
func (s *stubFiles) Download(_ context.Context, _, _, _, _ string) error {
|
||||
return stubError("Files.Download")
|
||||
}
|
||||
|
||||
// stubHealth implements HealthInterface as a placeholder.
|
||||
type stubHealth struct{}
|
||||
|
||||
func (s *stubHealth) Check(_ context.Context) (*HealthResult, error) {
|
||||
return nil, stubError("Health.Check")
|
||||
}
|
||||
|
||||
// stubProviders implements ProviderInterface as a placeholder.
|
||||
type stubProviders struct{}
|
||||
|
||||
func (s *stubProviders) Create(_ context.Context, _ string, _ *Provider) (*Provider, error) {
|
||||
return nil, stubError("Providers.Create")
|
||||
}
|
||||
func (s *stubProviders) Get(_ context.Context, _, _ string) (*Provider, error) {
|
||||
return nil, stubError("Providers.Get")
|
||||
}
|
||||
func (s *stubProviders) List(_ context.Context, _ string, _ ...ListOptions) ([]*Provider, error) {
|
||||
return nil, stubError("Providers.List")
|
||||
}
|
||||
func (s *stubProviders) Update(_ context.Context, _ string, _ *Provider) (*Provider, error) {
|
||||
return nil, stubError("Providers.Update")
|
||||
}
|
||||
func (s *stubProviders) Delete(_ context.Context, _, _ string) error {
|
||||
return stubError("Providers.Delete")
|
||||
}
|
||||
func (s *stubProviders) Ensure(_ context.Context, _ string, _ *Provider) (*Provider, error) {
|
||||
return nil, stubError("Providers.Ensure")
|
||||
}
|
||||
func (s *stubProviders) Profiles() ProfileInterface { return &stubProfiles{} }
|
||||
func (s *stubProviders) Refresh() RefreshInterface { return &stubRefresh{} }
|
||||
|
||||
// stubProfiles implements ProfileInterface as a placeholder.
|
||||
type stubProfiles struct{}
|
||||
|
||||
func (s *stubProfiles) List(_ context.Context, _ string, _ ...ListOptions) ([]*ProviderProfile, error) {
|
||||
return nil, stubError("Profiles.List")
|
||||
}
|
||||
func (s *stubProfiles) Get(_ context.Context, _, _ string) (*ProviderProfile, error) {
|
||||
return nil, stubError("Profiles.Get")
|
||||
}
|
||||
func (s *stubProfiles) Import(_ context.Context, _ string, _ []ProfileImportItem) (*ImportResult, error) {
|
||||
return nil, stubError("Profiles.Import")
|
||||
}
|
||||
func (s *stubProfiles) Update(_ context.Context, _, _ string, _ uint64, _ ProfileImportItem) (*UpdateResult, error) {
|
||||
return nil, stubError("Profiles.Update")
|
||||
}
|
||||
func (s *stubProfiles) Lint(_ context.Context, _ string, _ []ProfileImportItem) (*LintResult, error) {
|
||||
return nil, stubError("Profiles.Lint")
|
||||
}
|
||||
func (s *stubProfiles) Delete(_ context.Context, _, _ string) (bool, error) {
|
||||
return false, stubError("Profiles.Delete")
|
||||
}
|
||||
|
||||
// stubRefresh implements RefreshInterface as a placeholder.
|
||||
type stubRefresh struct{}
|
||||
|
||||
func (s *stubRefresh) GetStatus(_ context.Context, _, _, _ string) ([]*RefreshStatus, error) {
|
||||
return nil, stubError("Refresh.GetStatus")
|
||||
}
|
||||
func (s *stubRefresh) Configure(_ context.Context, _ string, _ *RefreshConfig) (*RefreshStatus, error) {
|
||||
return nil, stubError("Refresh.Configure")
|
||||
}
|
||||
func (s *stubRefresh) Rotate(_ context.Context, _, _, _ string) (*RefreshStatus, error) {
|
||||
return nil, stubError("Refresh.Rotate")
|
||||
}
|
||||
func (s *stubRefresh) Delete(_ context.Context, _, _, _ string) (bool, error) {
|
||||
return false, stubError("Refresh.Delete")
|
||||
}
|
||||
|
||||
// stubServices implements ServiceInterface as a placeholder.
|
||||
type stubServices struct{}
|
||||
|
||||
func (s *stubServices) Expose(_ context.Context, _, _, _ string, _ uint32, _ bool) (*ServiceEndpoint, error) {
|
||||
return nil, stubError("Services.Expose")
|
||||
}
|
||||
func (s *stubServices) Get(_ context.Context, _, _, _ string) (*ServiceEndpoint, error) {
|
||||
return nil, stubError("Services.Get")
|
||||
}
|
||||
func (s *stubServices) List(_ context.Context, _, _ string, _ ...ListOptions) ([]*ServiceEndpoint, error) {
|
||||
return nil, stubError("Services.List")
|
||||
}
|
||||
func (s *stubServices) Delete(_ context.Context, _, _, _ string) error {
|
||||
return stubError("Services.Delete")
|
||||
}
|
||||
|
||||
// stubSSH implements SSHInterface as a placeholder.
|
||||
type stubSSH struct{}
|
||||
|
||||
func (s *stubSSH) CreateSession(_ context.Context, _, _ string) (*SSHSession, error) {
|
||||
return nil, stubError("SSH.CreateSession")
|
||||
}
|
||||
func (s *stubSSH) RevokeSession(_ context.Context, _, _ string) (bool, error) {
|
||||
return false, stubError("SSH.RevokeSession")
|
||||
}
|
||||
func (s *stubSSH) Tunnel(_ context.Context, _, _ string, _ uint32, _ ...TunnelOption) (io.ReadWriteCloser, error) {
|
||||
return nil, stubError("SSH.Tunnel")
|
||||
}
|
||||
|
||||
// stubTCP implements TCPInterface as a placeholder.
|
||||
type stubTCP struct{}
|
||||
|
||||
func (s *stubTCP) Forward(_ context.Context, _, _ string, _ uint32, _ ...ForwardOption) (io.ReadWriteCloser, error) {
|
||||
return nil, stubError("TCP.Forward")
|
||||
}
|
||||
func (s *stubTCP) Listen(_ context.Context, _, _ string, _, _ uint32, _ ...ListenOption) (net.Listener, error) {
|
||||
return nil, stubError("TCP.Listen")
|
||||
}
|
||||
|
||||
// stubConfig implements ConfigInterface as a placeholder.
|
||||
type stubConfig struct{}
|
||||
|
||||
func (s *stubConfig) GetSandbox(_ context.Context, _, _ string) (*SandboxConfig, error) {
|
||||
return nil, stubError("Config.GetSandbox")
|
||||
}
|
||||
func (s *stubConfig) GetGateway(_ context.Context) (*GatewayConfig, error) {
|
||||
return nil, stubError("Config.GetGateway")
|
||||
}
|
||||
func (s *stubConfig) Update(_ context.Context, _ string, _ *ConfigUpdate) (*ConfigUpdateResult, error) {
|
||||
return nil, stubError("Config.Update")
|
||||
}
|
||||
|
||||
// stubPolicy implements PolicyInterface as a placeholder.
|
||||
type stubPolicy struct{}
|
||||
|
||||
func (s *stubPolicy) GetDraft(_ context.Context, _, _ string, _ ...GetDraftOption) (*DraftPolicy, error) {
|
||||
return nil, stubError("Policy.GetDraft")
|
||||
}
|
||||
func (s *stubPolicy) ApproveDraftChunk(_ context.Context, _, _, _ string) (*ApproveResult, error) {
|
||||
return nil, stubError("Policy.ApproveDraftChunk")
|
||||
}
|
||||
func (s *stubPolicy) RejectDraftChunk(_ context.Context, _, _, _, _ string) error {
|
||||
return stubError("Policy.RejectDraftChunk")
|
||||
}
|
||||
func (s *stubPolicy) ApproveAllDraftChunks(_ context.Context, _, _ string, _ ...ApproveAllOption) (*ApproveAllResult, error) {
|
||||
return nil, stubError("Policy.ApproveAllDraftChunks")
|
||||
}
|
||||
func (s *stubPolicy) ClearDraftChunks(_ context.Context, _, _ string) (*ClearResult, error) {
|
||||
return nil, stubError("Policy.ClearDraftChunks")
|
||||
}
|
||||
func (s *stubPolicy) GetDraftHistory(_ context.Context, _, _ string) ([]DraftHistoryEntry, error) {
|
||||
return nil, stubError("Policy.GetDraftHistory")
|
||||
}
|
||||
func (s *stubPolicy) GetStatus(_ context.Context, _, _ string, _ ...GetStatusOption) (*PolicyStatusResult, error) {
|
||||
return nil, stubError("Policy.GetStatus")
|
||||
}
|
||||
func (s *stubPolicy) List(_ context.Context, _ string, _ ...ListPolicyOption) ([]SandboxPolicyRevision, error) {
|
||||
return nil, stubError("Policy.List")
|
||||
}
|
||||
func (s *stubPolicy) EditDraftChunk(_ context.Context, _, _, _ string, _ *NetworkPolicyRule) error {
|
||||
return stubError("Policy.EditDraftChunk")
|
||||
}
|
||||
func (s *stubPolicy) UndoDraftChunk(_ context.Context, _, _, _ string) (*UndoResult, error) {
|
||||
return nil, stubError("Policy.UndoDraftChunk")
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
// forwardConfig accumulates options for the Forward method.
|
||||
type forwardConfig struct {
|
||||
serviceID string
|
||||
}
|
||||
|
||||
// ForwardOption configures a TCP forward opened via [TCPInterface.Forward].
|
||||
type ForwardOption func(*forwardConfig)
|
||||
|
||||
// WithForwardServiceID sets an optional service identifier on the forward's
|
||||
// init frame for audit and correlation purposes.
|
||||
func WithForwardServiceID(id string) ForwardOption {
|
||||
return func(c *forwardConfig) {
|
||||
c.serviceID = id
|
||||
}
|
||||
}
|
||||
|
||||
// listenConfig accumulates options for the Listen method.
|
||||
type listenConfig struct {
|
||||
bindAddress string
|
||||
useSSHTunnel bool
|
||||
serviceID string
|
||||
}
|
||||
|
||||
// ListenOption configures a local listener opened via [TCPInterface.Listen].
|
||||
type ListenOption func(*listenConfig)
|
||||
|
||||
// WithBindAddress overrides the default local bind address ("127.0.0.1").
|
||||
// Pass "0.0.0.0" to accept connections from any interface.
|
||||
func WithBindAddress(addr string) ListenOption {
|
||||
return func(c *listenConfig) {
|
||||
c.bindAddress = addr
|
||||
}
|
||||
}
|
||||
|
||||
// WithSSHTunnel routes each accepted connection through an SSH tunnel
|
||||
// ([SSHInterface.Tunnel]) instead of the default TCP forward
|
||||
// ([TCPInterface.Forward]).
|
||||
func WithSSHTunnel() ListenOption {
|
||||
return func(c *listenConfig) {
|
||||
c.useSSHTunnel = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithListenServiceID sets an optional service identifier on each tunneled
|
||||
// connection's init frame for audit and correlation purposes.
|
||||
func WithListenServiceID(id string) ListenOption {
|
||||
return func(c *listenConfig) {
|
||||
c.serviceID = id
|
||||
}
|
||||
}
|
||||
|
||||
// TCPInterface defines operations for TCP port forwarding to sandboxes.
|
||||
// Methods accept a sandbox name and resolve it to an ID internally.
|
||||
type TCPInterface interface {
|
||||
// Forward opens a bidirectional TCP connection to the given port inside a
|
||||
// sandbox. The sandbox is identified by name; the SDK resolves it to an
|
||||
// ID internally. The returned io.ReadWriteCloser wraps the underlying
|
||||
// gRPC stream; closing it terminates the stream. Port must be in the
|
||||
// range 1-65535; out-of-range values are rejected client-side with an
|
||||
// InvalidArgument error before opening the gRPC stream.
|
||||
//
|
||||
// The connection respects context cancellation: if ctx is cancelled,
|
||||
// the stream is closed and pending Read/Write calls return a context error.
|
||||
Forward(ctx context.Context, workspace, sandboxName string, port uint32, opts ...ForwardOption) (io.ReadWriteCloser, error)
|
||||
|
||||
// Listen binds a local TCP port and tunnels every accepted connection to
|
||||
// the given port inside a sandbox, returning a standard [net.Listener].
|
||||
// Each call to Accept on the returned listener establishes a new tunnel
|
||||
// to the sandbox port, bridging data bidirectionally.
|
||||
//
|
||||
// The sandbox is identified by name; the SDK resolves it to an ID
|
||||
// internally. remotePort must be in the range 1-65535; localPort must be
|
||||
// in the range 0-65535, where 0 lets the OS assign an ephemeral port
|
||||
// (discoverable via Addr).
|
||||
//
|
||||
// Closing the listener stops accepting new connections, tears down all
|
||||
// active tunnels, and blocks until all bridge goroutines finish.
|
||||
// Cancelling ctx triggers the same shutdown behavior.
|
||||
//
|
||||
// Errors:
|
||||
// - InvalidArgument: sandboxName is empty, remotePort is 0 or > 65535,
|
||||
// or localPort is > 65535
|
||||
// - Unimplemented: returned by the fake client
|
||||
// - Unavailable: client is closed
|
||||
Listen(ctx context.Context, workspace, sandboxName string, remotePort uint32, localPort uint32, opts ...ListenOption) (net.Listener, error)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// SandboxPhase represents the lifecycle phase of a sandbox.
|
||||
type SandboxPhase = types.SandboxPhase
|
||||
|
||||
// SandboxPhase values for sandbox lifecycle.
|
||||
const (
|
||||
SandboxProvisioning = types.SandboxProvisioning
|
||||
SandboxReady = types.SandboxReady
|
||||
SandboxError = types.SandboxError
|
||||
SandboxDeleting = types.SandboxDeleting
|
||||
SandboxUnknown = types.SandboxUnknown
|
||||
)
|
||||
|
||||
// EventType classifies watch events.
|
||||
type EventType = types.EventType
|
||||
|
||||
// EventType values for watch events.
|
||||
const (
|
||||
EventAdded = types.EventAdded
|
||||
EventModified = types.EventModified
|
||||
EventDeleted = types.EventDeleted
|
||||
EventError = types.EventError
|
||||
)
|
||||
|
||||
// StreamType identifies which output stream a chunk belongs to.
|
||||
type StreamType = types.StreamType
|
||||
|
||||
// StreamType values for exec output.
|
||||
const (
|
||||
StreamStdout = types.StreamStdout
|
||||
StreamStderr = types.StreamStderr
|
||||
)
|
||||
|
||||
// TLSConfig holds TLS connection settings.
|
||||
type TLSConfig = types.TLSConfig
|
||||
|
||||
// RetryPolicy configures automatic retry behavior for failed RPCs.
|
||||
type RetryPolicy = types.RetryPolicy
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "context"
|
||||
|
||||
// AuthProvider supplies per-RPC credentials. It implements the
|
||||
// grpc credentials.PerRPCCredentials interface.
|
||||
type AuthProvider interface {
|
||||
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
|
||||
RequireTransportSecurity() bool
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// Config holds all settings needed to create a Client.
|
||||
type Config struct {
|
||||
Address string
|
||||
TLS *TLSConfig
|
||||
Auth AuthProvider
|
||||
// Timeout is reserved for future use. It is not yet applied.
|
||||
Timeout time.Duration
|
||||
// RetryPolicy is reserved for future use. It is not yet applied.
|
||||
RetryPolicy *RetryPolicy
|
||||
// Logger is reserved for future use. It is not yet applied.
|
||||
Logger Logger
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package types defines all domain data types for the OpenShell SDK v1 API.
|
||||
//
|
||||
// These types are the canonical definitions used by both the client layer
|
||||
// (openshell/v1) and the converter layer (openshell/v1/internal/converter).
|
||||
// The v1 package re-exports all types via type aliases for backward
|
||||
// compatibility.
|
||||
package types
|
||||
@@ -0,0 +1,134 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrorCode classifies SDK errors by their gRPC origin.
|
||||
type ErrorCode int
|
||||
|
||||
// ErrorCode values for classifying gRPC errors.
|
||||
const (
|
||||
ErrorNotFound ErrorCode = iota + 1
|
||||
ErrorAlreadyExists
|
||||
ErrorUnavailable
|
||||
ErrorPermissionDenied
|
||||
ErrorInvalidArgument
|
||||
ErrorDeadlineExceeded
|
||||
ErrorCancelled
|
||||
ErrorInternal
|
||||
ErrorUnimplemented
|
||||
ErrorConflict
|
||||
ErrorUnauthenticated
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the error code.
|
||||
func (c ErrorCode) String() string {
|
||||
switch c {
|
||||
case ErrorNotFound:
|
||||
return "NotFound"
|
||||
case ErrorAlreadyExists:
|
||||
return "AlreadyExists"
|
||||
case ErrorUnavailable:
|
||||
return "Unavailable"
|
||||
case ErrorPermissionDenied:
|
||||
return "PermissionDenied"
|
||||
case ErrorInvalidArgument:
|
||||
return "InvalidArgument"
|
||||
case ErrorDeadlineExceeded:
|
||||
return "DeadlineExceeded"
|
||||
case ErrorCancelled:
|
||||
return "Cancelled"
|
||||
case ErrorInternal:
|
||||
return "Internal"
|
||||
case ErrorUnimplemented:
|
||||
return "Unimplemented"
|
||||
case ErrorConflict:
|
||||
return "Conflict"
|
||||
case ErrorUnauthenticated:
|
||||
return "Unauthenticated"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown(%d)", int(c))
|
||||
}
|
||||
}
|
||||
|
||||
// StatusError is the typed error returned by all SDK operations.
|
||||
type StatusError struct {
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *StatusError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (e *StatusError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// IsNotFound returns true if the error indicates a resource was not found.
|
||||
func IsNotFound(err error) bool {
|
||||
return hasCode(err, ErrorNotFound)
|
||||
}
|
||||
|
||||
// IsAlreadyExists returns true if the error indicates a resource already exists.
|
||||
func IsAlreadyExists(err error) bool {
|
||||
return hasCode(err, ErrorAlreadyExists)
|
||||
}
|
||||
|
||||
// IsUnavailable returns true if the error indicates the service is unavailable.
|
||||
func IsUnavailable(err error) bool {
|
||||
return hasCode(err, ErrorUnavailable)
|
||||
}
|
||||
|
||||
// IsPermissionDenied returns true if the error indicates insufficient permissions.
|
||||
func IsPermissionDenied(err error) bool {
|
||||
return hasCode(err, ErrorPermissionDenied)
|
||||
}
|
||||
|
||||
// IsInvalidArgument returns true if the error indicates an invalid argument.
|
||||
func IsInvalidArgument(err error) bool {
|
||||
return hasCode(err, ErrorInvalidArgument)
|
||||
}
|
||||
|
||||
// IsDeadlineExceeded returns true if the error indicates a deadline was exceeded.
|
||||
func IsDeadlineExceeded(err error) bool {
|
||||
return hasCode(err, ErrorDeadlineExceeded)
|
||||
}
|
||||
|
||||
// IsCancelled returns true if the error indicates the operation was cancelled.
|
||||
func IsCancelled(err error) bool {
|
||||
return hasCode(err, ErrorCancelled)
|
||||
}
|
||||
|
||||
// IsUnimplemented returns true if the error indicates the operation is not implemented.
|
||||
func IsUnimplemented(err error) bool {
|
||||
return hasCode(err, ErrorUnimplemented)
|
||||
}
|
||||
|
||||
// IsConflict returns true if the error indicates a conflict, such as
|
||||
// optimistic concurrency or an invalid state transition.
|
||||
func IsConflict(err error) bool {
|
||||
return hasCode(err, ErrorConflict)
|
||||
}
|
||||
|
||||
// IsUnauthenticated returns true if the error indicates missing or invalid credentials.
|
||||
func IsUnauthenticated(err error) bool {
|
||||
return hasCode(err, ErrorUnauthenticated)
|
||||
}
|
||||
|
||||
func hasCode(err error, code ErrorCode) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var se *StatusError
|
||||
if errors.As(err, &se) {
|
||||
return se.Code == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// ExecResult holds the collected output of a completed command execution.
|
||||
type ExecResult struct {
|
||||
ExitCode int
|
||||
Stdout []byte
|
||||
Stderr []byte
|
||||
}
|
||||
|
||||
// ExecChunk represents a single chunk of output from a streaming command execution.
|
||||
type ExecChunk struct {
|
||||
Stream StreamType
|
||||
Data []byte
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// HealthResult holds the result of a health check.
|
||||
type HealthResult struct {
|
||||
Healthy bool
|
||||
Version string
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// LogLine represents a single log entry from a sandbox.
|
||||
type LogLine struct {
|
||||
// Timestamp is when the log entry was recorded.
|
||||
Timestamp time.Time
|
||||
// Level is the log severity level (e.g., "INFO", "WARN", "ERROR").
|
||||
Level string
|
||||
// Target is the log target/module.
|
||||
Target string
|
||||
// Message is the log message text.
|
||||
Message string
|
||||
// Source is the log source: "gateway" or "sandbox".
|
||||
Source string
|
||||
// Fields contains structured key-value fields from the tracing event.
|
||||
Fields map[string]string
|
||||
}
|
||||
|
||||
// LogResult contains the result of a GetLogs call.
|
||||
type LogResult struct {
|
||||
// Lines contains the log entries in chronological order.
|
||||
Lines []LogLine
|
||||
// BufferTotal is the total number of lines in the server's buffer.
|
||||
BufferTotal uint32
|
||||
}
|
||||
|
||||
// logConfig holds configuration for GetLogs calls.
|
||||
type logConfig struct {
|
||||
lines uint32
|
||||
since time.Time
|
||||
sources []string
|
||||
minLevel string
|
||||
}
|
||||
|
||||
// LogOption configures a GetLogs call.
|
||||
type LogOption func(*logConfig)
|
||||
|
||||
// WithLogLines sets the maximum number of log lines to return.
|
||||
func WithLogLines(n uint32) LogOption {
|
||||
return func(c *logConfig) {
|
||||
c.lines = n
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogSince filters logs to entries at or after the given time.
|
||||
func WithLogSince(t time.Time) LogOption {
|
||||
return func(c *logConfig) {
|
||||
c.since = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogSources filters logs by source (e.g., "gateway", "sandbox").
|
||||
func WithLogSources(sources ...string) LogOption {
|
||||
return func(c *logConfig) {
|
||||
c.sources = sources
|
||||
}
|
||||
}
|
||||
|
||||
// WithLogMinLevel sets the minimum log level to include.
|
||||
func WithLogMinLevel(level string) LogOption {
|
||||
return func(c *logConfig) {
|
||||
c.minLevel = level
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyLogOptions applies options and returns the config.
|
||||
func ApplyLogOptions(opts []LogOption) logConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
|
||||
var cfg logConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Lines returns the configured max lines (0 means server default).
|
||||
func (c *logConfig) Lines() uint32 {
|
||||
return c.lines
|
||||
}
|
||||
|
||||
// Since returns the configured since timestamp (zero means no filter).
|
||||
func (c *logConfig) Since() time.Time {
|
||||
return c.since
|
||||
}
|
||||
|
||||
// Sources returns the configured source filters.
|
||||
func (c *logConfig) Sources() []string {
|
||||
return c.sources
|
||||
}
|
||||
|
||||
// MinLevel returns the configured minimum log level.
|
||||
func (c *logConfig) MinLevel() string {
|
||||
return c.minLevel
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// Logger defines structured logging for the SDK. Compatible with logr.Logger
|
||||
// and slog.Logger adapters.
|
||||
type Logger interface {
|
||||
Debug(msg string, keysAndValues ...any)
|
||||
Info(msg string, keysAndValues ...any)
|
||||
Error(err error, msg string, keysAndValues ...any)
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// NetworkPolicyRule defines a named network policy rule containing endpoints and binaries.
|
||||
type NetworkPolicyRule struct {
|
||||
// Name is the map key for this rule in the sandbox policy.
|
||||
Name string
|
||||
// Endpoints lists the network endpoints governed by this rule.
|
||||
Endpoints []PolicyNetworkEndpoint
|
||||
// Binaries lists the binaries governed by this rule.
|
||||
Binaries []PolicyNetworkBinary
|
||||
}
|
||||
|
||||
// PolicyNetworkEndpoint describes a full network endpoint with its access controls
|
||||
// as used in sandbox network policy rules. This is distinct from [NetworkEndpoint]
|
||||
// which is the simplified profile-level endpoint (Host, Port, Protocol only).
|
||||
type PolicyNetworkEndpoint struct {
|
||||
Host string
|
||||
Port uint32
|
||||
Ports []uint32
|
||||
Protocol string
|
||||
TLS string
|
||||
Enforcement string
|
||||
Access string
|
||||
Rules []L7Rule
|
||||
AllowedIPs []string
|
||||
DenyRules []L7DenyRule
|
||||
AllowEncodedSlash bool
|
||||
PersistedQueries string
|
||||
GraphqlPersistedQueries map[string]GraphqlOperation
|
||||
GraphqlMaxBodyBytes uint32
|
||||
Path string
|
||||
WebsocketCredentialRewrite bool
|
||||
RequestBodyCredentialRewrite bool
|
||||
AdvisorProposed bool
|
||||
CredentialSigning string
|
||||
SigningService string
|
||||
SigningRegion string
|
||||
JsonRpcMaxBodyBytes uint32
|
||||
Mcp *McpOptions
|
||||
}
|
||||
|
||||
// PolicyNetworkBinary identifies a binary subject to network policy enforcement.
|
||||
// This is distinct from [NetworkBinary] which is the simplified profile-level binary.
|
||||
type PolicyNetworkBinary struct {
|
||||
// Path is the filesystem path to the binary.
|
||||
Path string
|
||||
}
|
||||
|
||||
// L7Rule wraps an L7 allow rule.
|
||||
type L7Rule struct {
|
||||
// Allow holds the layer-7 allow criteria.
|
||||
Allow *L7Allow
|
||||
}
|
||||
|
||||
// L7Allow specifies layer-7 allow criteria for HTTP/GraphQL/MCP traffic.
|
||||
type L7Allow struct {
|
||||
Method string
|
||||
Path string
|
||||
Command string
|
||||
Query map[string]L7QueryMatcher
|
||||
OperationType string
|
||||
OperationName string
|
||||
Fields []string
|
||||
Params map[string]L7QueryMatcher
|
||||
}
|
||||
|
||||
// L7DenyRule specifies layer-7 deny criteria for HTTP/GraphQL/MCP traffic.
|
||||
type L7DenyRule struct {
|
||||
Method string
|
||||
Path string
|
||||
Command string
|
||||
Query map[string]L7QueryMatcher
|
||||
OperationType string
|
||||
OperationName string
|
||||
Fields []string
|
||||
Params map[string]L7QueryMatcher
|
||||
}
|
||||
|
||||
// McpOptions holds MCP-specific policy and inspection options.
|
||||
type McpOptions struct {
|
||||
StrictToolNames *bool
|
||||
AllowAllKnownMcpMethods *bool
|
||||
}
|
||||
|
||||
// L7QueryMatcher matches query parameters by glob pattern or exact values.
|
||||
type L7QueryMatcher struct {
|
||||
Glob string
|
||||
Any []string
|
||||
}
|
||||
|
||||
// GraphqlOperation describes a GraphQL operation for persisted-query validation.
|
||||
type GraphqlOperation struct {
|
||||
OperationType string
|
||||
OperationName string
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// --- MergeOperation types ---
|
||||
|
||||
// PolicyMergeOperation represents a single atomic policy mutation.
|
||||
// Exactly one of the pointer fields must be non-nil, modelling the proto oneof.
|
||||
type PolicyMergeOperation struct {
|
||||
// AddRule adds a new named network policy rule.
|
||||
AddRule *AddNetworkRule
|
||||
// RemoveEndpoint removes a single endpoint from a rule.
|
||||
RemoveEndpoint *RemoveNetworkEndpoint
|
||||
// RemoveRule removes an entire named rule.
|
||||
RemoveRule *RemoveNetworkRule
|
||||
// AddDenyRules appends deny rules to an endpoint.
|
||||
AddDenyRules *AddDenyRules
|
||||
// AddAllowRules appends allow rules to an endpoint.
|
||||
AddAllowRules *AddAllowRules
|
||||
// RemoveBinary removes a binary from a rule.
|
||||
RemoveBinary *RemoveNetworkBinary
|
||||
}
|
||||
|
||||
// AddNetworkRule adds a named network policy rule with a full rule definition.
|
||||
type AddNetworkRule struct {
|
||||
// RuleName is the name key for the rule.
|
||||
RuleName string
|
||||
// Rule is the full network policy rule to add.
|
||||
Rule NetworkPolicyRule
|
||||
}
|
||||
|
||||
// RemoveNetworkEndpoint removes a specific endpoint from a named rule.
|
||||
type RemoveNetworkEndpoint struct {
|
||||
// RuleName is the name of the rule containing the endpoint.
|
||||
RuleName string
|
||||
// Host is the endpoint host to remove.
|
||||
Host string
|
||||
// Port is the endpoint port to remove.
|
||||
Port uint32
|
||||
}
|
||||
|
||||
// RemoveNetworkRule removes an entire named rule from the policy.
|
||||
type RemoveNetworkRule struct {
|
||||
// RuleName is the name of the rule to remove.
|
||||
RuleName string
|
||||
}
|
||||
|
||||
// AddDenyRules appends layer-7 deny rules to a specific endpoint.
|
||||
type AddDenyRules struct {
|
||||
// Host identifies the target endpoint host.
|
||||
Host string
|
||||
// Port identifies the target endpoint port.
|
||||
Port uint32
|
||||
// DenyRules are the deny rules to append.
|
||||
DenyRules []L7DenyRule
|
||||
}
|
||||
|
||||
// AddAllowRules appends layer-7 allow rules to a specific endpoint.
|
||||
type AddAllowRules struct {
|
||||
// Host identifies the target endpoint host.
|
||||
Host string
|
||||
// Port identifies the target endpoint port.
|
||||
Port uint32
|
||||
// Rules are the allow rules to append.
|
||||
Rules []L7Rule
|
||||
}
|
||||
|
||||
// RemoveNetworkBinary removes a binary from a named rule.
|
||||
type RemoveNetworkBinary struct {
|
||||
// RuleName is the name of the rule containing the binary.
|
||||
RuleName string
|
||||
// BinaryPath is the filesystem path of the binary to remove.
|
||||
BinaryPath string
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// CreateOptions configures resource creation.
|
||||
type CreateOptions struct{}
|
||||
|
||||
// GetOptions configures resource retrieval.
|
||||
type GetOptions struct{}
|
||||
|
||||
// ListOptions configures resource listing with pagination and filtering.
|
||||
type ListOptions struct {
|
||||
Limit int
|
||||
Offset int
|
||||
LabelSelector string
|
||||
AllWorkspaces bool
|
||||
}
|
||||
|
||||
// DeleteOptions configures resource deletion.
|
||||
type DeleteOptions struct{}
|
||||
|
||||
// UpdateOptions configures resource updates.
|
||||
type UpdateOptions struct{}
|
||||
|
||||
// WatchOptions configures watch behavior.
|
||||
type WatchOptions struct {
|
||||
// TimeoutSeconds is reserved for future use. Use context for timeout control.
|
||||
TimeoutSeconds int64
|
||||
// LabelSelector is reserved for future use.
|
||||
LabelSelector string
|
||||
// StopOnTerminal causes the watch to close automatically when the sandbox
|
||||
// reaches a terminal phase (Ready or Error).
|
||||
StopOnTerminal bool
|
||||
}
|
||||
|
||||
// WaitOptions configures wait behavior. Use context for timeout control.
|
||||
type WaitOptions struct {
|
||||
PollInterval time.Duration
|
||||
}
|
||||
|
||||
// ExecOptions configures command execution.
|
||||
type ExecOptions struct {
|
||||
Env map[string]string
|
||||
WorkDir string
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// PolicyLoadStatus represents the load state of a policy revision.
|
||||
type PolicyLoadStatus int
|
||||
|
||||
const (
|
||||
// PolicyLoadStatusUnspecified is the default zero value.
|
||||
PolicyLoadStatusUnspecified PolicyLoadStatus = iota
|
||||
// PolicyLoadStatusPending means the policy is queued for loading.
|
||||
PolicyLoadStatusPending
|
||||
// PolicyLoadStatusLoaded means the policy was successfully loaded.
|
||||
PolicyLoadStatusLoaded
|
||||
// PolicyLoadStatusFailed means the policy failed to load.
|
||||
PolicyLoadStatusFailed
|
||||
// PolicyLoadStatusSuperseded means a newer revision replaced this one.
|
||||
PolicyLoadStatusSuperseded
|
||||
)
|
||||
|
||||
// String returns the human-readable name of the load status.
|
||||
func (s PolicyLoadStatus) String() string {
|
||||
switch s {
|
||||
case PolicyLoadStatusUnspecified:
|
||||
return "Unspecified"
|
||||
case PolicyLoadStatusPending:
|
||||
return "Pending"
|
||||
case PolicyLoadStatusLoaded:
|
||||
return "Loaded"
|
||||
case PolicyLoadStatusFailed:
|
||||
return "Failed"
|
||||
case PolicyLoadStatusSuperseded:
|
||||
return "Superseded"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// PolicyChunk represents a single proposed policy change in the draft inbox.
|
||||
type PolicyChunk struct {
|
||||
// ID is the unique chunk identifier.
|
||||
ID string
|
||||
// Status is the approval status: "pending", "approved", "rejected".
|
||||
Status string
|
||||
// RuleName is the proposed network_policies map key.
|
||||
RuleName string
|
||||
// ProposedRule is the proposed network policy rule.
|
||||
ProposedRule *NetworkPolicyRule
|
||||
|
||||
// Rationale is a human-readable explanation of why this rule is proposed.
|
||||
Rationale string
|
||||
// SecurityNotes contains security concerns flagged by analysis (empty if none).
|
||||
SecurityNotes string
|
||||
// Confidence is the analysis confidence score (0.0-1.0).
|
||||
Confidence float32
|
||||
// DenialSummaryIDs lists the IDs of denial summaries that led to this chunk.
|
||||
DenialSummaryIDs []string
|
||||
// CreatedAt is when the chunk was created.
|
||||
CreatedAt time.Time
|
||||
// DecidedAt is when the user approved/rejected (zero if undecided).
|
||||
DecidedAt time.Time
|
||||
// Stage is the recommendation stage: "initial" or "refined".
|
||||
Stage string
|
||||
// SupersedesChunkID is the initial chunk ID this refined chunk replaces.
|
||||
SupersedesChunkID string
|
||||
// HitCount is how many times this endpoint was seen across denial flush cycles.
|
||||
HitCount int32
|
||||
// FirstSeen is the first time this endpoint was proposed.
|
||||
FirstSeen time.Time
|
||||
// LastSeen is the most recent time this endpoint was re-proposed.
|
||||
LastSeen time.Time
|
||||
// Binary is the binary path that triggered the denial.
|
||||
Binary string
|
||||
// ValidationResult is the prover output from gateway-side static checks.
|
||||
ValidationResult string
|
||||
// RejectionReason is the operator-supplied text accompanying a rejection.
|
||||
RejectionReason string
|
||||
}
|
||||
|
||||
// DraftPolicy contains the full draft policy state returned by GetDraft.
|
||||
type DraftPolicy struct {
|
||||
// Chunks contains the draft policy chunks.
|
||||
Chunks []PolicyChunk
|
||||
// RollingSummary is an LLM-generated summary of all analysis.
|
||||
RollingSummary string
|
||||
// DraftVersion is the current draft version number.
|
||||
DraftVersion uint64
|
||||
// LastAnalyzedAt is when the last analysis completed.
|
||||
LastAnalyzedAt time.Time
|
||||
}
|
||||
|
||||
// SandboxPolicy is the top-level security policy configuration for a sandbox.
|
||||
// It contains filesystem access rules, Landlock LSM configuration, process
|
||||
// identity rules, and named network access policies.
|
||||
type SandboxPolicy struct {
|
||||
// Version is the policy version number. The server may override this on write.
|
||||
Version uint32
|
||||
// Filesystem controls which directories the sandbox can access.
|
||||
// Nil means no filesystem policy is specified.
|
||||
Filesystem *FilesystemPolicy
|
||||
// Landlock configures the Linux Landlock LSM.
|
||||
// Nil means no landlock policy is specified.
|
||||
Landlock *LandlockPolicy
|
||||
// Process controls the user and group identity for sandboxed processes.
|
||||
// Nil means no process policy is specified.
|
||||
Process *ProcessPolicy
|
||||
// NetworkPolicies contains named network access rules.
|
||||
// Nil means no network policies are specified; an empty map is distinct from nil.
|
||||
NetworkPolicies map[string]NetworkPolicyRule
|
||||
}
|
||||
|
||||
// FilesystemPolicy controls which directories the sandbox can access
|
||||
// in read-only or read-write mode.
|
||||
type FilesystemPolicy struct {
|
||||
// IncludeWorkdir auto-includes the working directory as read-write.
|
||||
IncludeWorkdir bool
|
||||
// ReadOnly is the list of read-only directory paths.
|
||||
// Nil means no read-only directories; an empty slice is distinct from nil.
|
||||
ReadOnly []string
|
||||
// ReadWrite is the list of read-write directory paths.
|
||||
// Nil means no read-write directories; an empty slice is distinct from nil.
|
||||
ReadWrite []string
|
||||
}
|
||||
|
||||
// LandlockPolicy configures the Linux Landlock LSM for filesystem restriction enforcement.
|
||||
type LandlockPolicy struct {
|
||||
// Compatibility is the compatibility mode (e.g., "best_effort", "hard_requirement").
|
||||
Compatibility string
|
||||
}
|
||||
|
||||
// ProcessPolicy controls the user and group identity under which sandboxed processes execute.
|
||||
type ProcessPolicy struct {
|
||||
// RunAsUser is the user name for sandboxed processes.
|
||||
RunAsUser string
|
||||
// RunAsGroup is the group name for sandboxed processes.
|
||||
RunAsGroup string
|
||||
}
|
||||
|
||||
// SandboxPolicyRevision represents a versioned policy revision for a sandbox.
|
||||
type SandboxPolicyRevision struct {
|
||||
// Version is the policy version (monotonically increasing per sandbox).
|
||||
Version uint32
|
||||
// PolicyHash is the SHA-256 hash of the serialized policy payload.
|
||||
PolicyHash string
|
||||
// Status is the load status of this revision.
|
||||
Status PolicyLoadStatus
|
||||
// LoadError is the error message if status is Failed.
|
||||
LoadError string
|
||||
// CreatedAt is when this revision was created.
|
||||
CreatedAt time.Time
|
||||
// LoadedAt is when this revision was loaded by the sandbox.
|
||||
LoadedAt time.Time
|
||||
// Policy is the typed security policy for this revision. Nil when not requested or absent.
|
||||
Policy *SandboxPolicy
|
||||
}
|
||||
|
||||
// PolicyStatusResult contains the status of a sandbox's policy.
|
||||
type PolicyStatusResult struct {
|
||||
// Revision is the queried policy revision.
|
||||
Revision SandboxPolicyRevision
|
||||
// ActiveVersion is the currently active (loaded) policy version.
|
||||
ActiveVersion uint32
|
||||
}
|
||||
|
||||
// ApproveResult contains the result of approving a single draft chunk.
|
||||
type ApproveResult struct {
|
||||
// PolicyVersion is the new policy version after merge.
|
||||
PolicyVersion uint32
|
||||
// PolicyHash is the SHA-256 hash of the new policy.
|
||||
PolicyHash string
|
||||
}
|
||||
|
||||
// ApproveAllResult contains the result of approving all draft chunks.
|
||||
type ApproveAllResult struct {
|
||||
// PolicyVersion is the new policy version after merge.
|
||||
PolicyVersion uint32
|
||||
// PolicyHash is the SHA-256 hash of the new policy.
|
||||
PolicyHash string
|
||||
// ChunksApproved is the number of chunks approved.
|
||||
ChunksApproved uint32
|
||||
// ChunksSkipped is the number of chunks skipped (security-flagged).
|
||||
ChunksSkipped uint32
|
||||
}
|
||||
|
||||
// UndoResult contains the result of undoing a draft chunk approval.
|
||||
type UndoResult struct {
|
||||
// PolicyVersion is the new policy version after removal.
|
||||
PolicyVersion uint32
|
||||
// PolicyHash is the SHA-256 hash of the updated policy.
|
||||
PolicyHash string
|
||||
}
|
||||
|
||||
// ClearResult contains the result of clearing all draft chunks.
|
||||
type ClearResult struct {
|
||||
// ChunksCleared is the number of chunks cleared.
|
||||
ChunksCleared uint32
|
||||
}
|
||||
|
||||
// DraftHistoryEntry represents a single event in the draft policy history.
|
||||
type DraftHistoryEntry struct {
|
||||
// Timestamp is when the event occurred.
|
||||
Timestamp time.Time
|
||||
// EventType is the event type (e.g., "approved", "rejected", "cleared").
|
||||
EventType string
|
||||
// Description is a human-readable description.
|
||||
Description string
|
||||
// ChunkID is the associated chunk ID (if applicable).
|
||||
ChunkID string
|
||||
}
|
||||
|
||||
// getDraftConfig holds configuration for GetDraft calls.
|
||||
type getDraftConfig struct {
|
||||
statusFilter string
|
||||
}
|
||||
|
||||
// GetDraftOption configures a GetDraft call.
|
||||
type GetDraftOption func(*getDraftConfig)
|
||||
|
||||
// WithStatusFilter filters draft chunks by approval status.
|
||||
func WithStatusFilter(status string) GetDraftOption {
|
||||
return func(c *getDraftConfig) {
|
||||
c.statusFilter = status
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyGetDraftOptions applies options and returns the config.
|
||||
func ApplyGetDraftOptions(opts []GetDraftOption) getDraftConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
|
||||
var cfg getDraftConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// StatusFilter returns the configured status filter.
|
||||
func (c *getDraftConfig) StatusFilter() string {
|
||||
return c.statusFilter
|
||||
}
|
||||
|
||||
// approveAllConfig holds configuration for ApproveAllDraftChunks calls.
|
||||
type approveAllConfig struct {
|
||||
includeSecurityFlagged bool
|
||||
}
|
||||
|
||||
// ApproveAllOption configures an ApproveAllDraftChunks call.
|
||||
type ApproveAllOption func(*approveAllConfig)
|
||||
|
||||
// WithIncludeSecurityFlagged includes security-flagged chunks in bulk approval.
|
||||
func WithIncludeSecurityFlagged() ApproveAllOption {
|
||||
return func(c *approveAllConfig) {
|
||||
c.includeSecurityFlagged = true
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyApproveAllOptions applies options and returns the config.
|
||||
func ApplyApproveAllOptions(opts []ApproveAllOption) approveAllConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
|
||||
var cfg approveAllConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// IncludeSecurityFlagged returns whether security-flagged chunks are included.
|
||||
func (c *approveAllConfig) IncludeSecurityFlagged() bool {
|
||||
return c.includeSecurityFlagged
|
||||
}
|
||||
|
||||
// getStatusConfig holds configuration for GetStatus calls.
|
||||
type getStatusConfig struct {
|
||||
version uint32
|
||||
}
|
||||
|
||||
// GetStatusOption configures a GetStatus call.
|
||||
type GetStatusOption func(*getStatusConfig)
|
||||
|
||||
// WithVersion queries a specific policy version instead of the latest.
|
||||
func WithVersion(version uint32) GetStatusOption {
|
||||
return func(c *getStatusConfig) {
|
||||
c.version = version
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyGetStatusOptions applies options and returns the config.
|
||||
func ApplyGetStatusOptions(opts []GetStatusOption) getStatusConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
|
||||
var cfg getStatusConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Version returns the configured version (0 means latest).
|
||||
func (c *getStatusConfig) Version() uint32 {
|
||||
return c.version
|
||||
}
|
||||
|
||||
// listPolicyConfig holds configuration for List calls.
|
||||
type listPolicyConfig struct {
|
||||
limit uint32
|
||||
offset uint32
|
||||
}
|
||||
|
||||
// ListPolicyOption configures a List call.
|
||||
type ListPolicyOption func(*listPolicyConfig)
|
||||
|
||||
// WithLimit sets the maximum number of revisions to return.
|
||||
func WithLimit(limit uint32) ListPolicyOption {
|
||||
return func(c *listPolicyConfig) {
|
||||
c.limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
// WithOffset sets the pagination offset.
|
||||
func WithOffset(offset uint32) ListPolicyOption {
|
||||
return func(c *listPolicyConfig) {
|
||||
c.offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyListPolicyOptions applies options and returns the config.
|
||||
func ApplyListPolicyOptions(opts []ListPolicyOption) listPolicyConfig { //nolint:revive // unexported return is intentional; consumed only by v1 package
|
||||
var cfg listPolicyConfig
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// Limit returns the configured limit (0 means server default).
|
||||
func (c *listPolicyConfig) Limit() uint32 {
|
||||
return c.limit
|
||||
}
|
||||
|
||||
// Offset returns the configured offset.
|
||||
func (c *listPolicyConfig) Offset() uint32 {
|
||||
return c.offset
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// ProfileCategory classifies a provider profile.
|
||||
type ProfileCategory string
|
||||
|
||||
// ProfileCategory values.
|
||||
const (
|
||||
ProfileCategoryOther ProfileCategory = "Other"
|
||||
ProfileCategoryInference ProfileCategory = "Inference"
|
||||
ProfileCategoryAgent ProfileCategory = "Agent"
|
||||
ProfileCategorySourceControl ProfileCategory = "SourceControl"
|
||||
ProfileCategoryMessaging ProfileCategory = "Messaging"
|
||||
ProfileCategoryData ProfileCategory = "Data"
|
||||
ProfileCategoryKnowledge ProfileCategory = "Knowledge"
|
||||
)
|
||||
|
||||
// ProviderProfile defines a provider type template with credentials schema,
|
||||
// endpoints, binaries, and discovery configuration.
|
||||
type ProviderProfile struct {
|
||||
ID string
|
||||
DisplayName string
|
||||
Description string
|
||||
Category ProfileCategory
|
||||
Credentials []ProfileCredential
|
||||
Endpoints []NetworkEndpoint
|
||||
Binaries []NetworkBinary
|
||||
InferenceCapable bool
|
||||
Discovery ProfileDiscovery
|
||||
ResourceVersion uint64
|
||||
}
|
||||
|
||||
// ProfileCredential defines a single credential required by a provider profile.
|
||||
type ProfileCredential struct {
|
||||
Name string
|
||||
Description string
|
||||
Required bool
|
||||
Secret bool
|
||||
}
|
||||
|
||||
// NetworkEndpoint describes a network endpoint provided by a profile.
|
||||
type NetworkEndpoint struct {
|
||||
Host string
|
||||
Port uint32
|
||||
Protocol string
|
||||
}
|
||||
|
||||
// NetworkBinary describes a binary artifact provided by a profile.
|
||||
type NetworkBinary struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// ProfileDiscovery holds local discovery configuration for a profile.
|
||||
type ProfileDiscovery struct {
|
||||
Credentials []string
|
||||
}
|
||||
|
||||
// ProfileImportItem is an item submitted for profile import or lint validation.
|
||||
type ProfileImportItem struct {
|
||||
Profile ProviderProfile
|
||||
Source string
|
||||
}
|
||||
|
||||
// ProfileDiagnostic is a validation finding from Import, Update, or Lint.
|
||||
type ProfileDiagnostic struct {
|
||||
Source string
|
||||
ProfileID string
|
||||
Field string
|
||||
Message string
|
||||
Severity string
|
||||
}
|
||||
|
||||
// ImportResult holds the result of a profile import operation.
|
||||
type ImportResult struct {
|
||||
Diagnostics []ProfileDiagnostic
|
||||
Profiles []ProviderProfile
|
||||
Imported bool
|
||||
}
|
||||
|
||||
// UpdateResult holds the result of a profile update operation.
|
||||
type UpdateResult struct {
|
||||
Diagnostics []ProfileDiagnostic
|
||||
Profile *ProviderProfile
|
||||
Updated bool
|
||||
}
|
||||
|
||||
// LintResult holds the result of a profile lint operation.
|
||||
type LintResult struct {
|
||||
Diagnostics []ProfileDiagnostic
|
||||
Valid bool
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// Provider represents an AI provider registration.
|
||||
type Provider struct {
|
||||
ID string
|
||||
Name string
|
||||
Type string
|
||||
CreatedAt time.Time
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
ResourceVersion uint64
|
||||
Workspace string
|
||||
DeletionTimestamp *time.Time
|
||||
Spec ProviderSpec
|
||||
}
|
||||
|
||||
// ProviderSpec holds provider-specific configuration and credentials.
|
||||
type ProviderSpec struct {
|
||||
Credentials map[string]string
|
||||
Config map[string]string
|
||||
CredentialExpiresAt map[string]time.Time
|
||||
ProfileWorkspace string
|
||||
CredentialHandles map[string]CredentialHandle
|
||||
}
|
||||
|
||||
// CredentialHandle is an opaque handle for a provider credential stored by
|
||||
// gateway credential storage. Handles are created by OpenShell and are not
|
||||
// accepted as user-authored input.
|
||||
type CredentialHandle struct {
|
||||
Driver string
|
||||
Handle string
|
||||
Metadata map[string]string
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// RefreshStrategy describes how credentials are refreshed.
|
||||
type RefreshStrategy string
|
||||
|
||||
// RefreshStrategy values.
|
||||
const (
|
||||
RefreshStrategyStatic RefreshStrategy = "Static"
|
||||
RefreshStrategyExternal RefreshStrategy = "External"
|
||||
RefreshStrategyOAuth2RefreshToken RefreshStrategy = "OAuth2RefreshToken"
|
||||
RefreshStrategyOAuth2ClientCredentials RefreshStrategy = "OAuth2ClientCredentials"
|
||||
RefreshStrategyGoogleServiceAccountJWT RefreshStrategy = "GoogleServiceAccountJWT"
|
||||
RefreshStrategyAWSStsAssumeRole RefreshStrategy = "AWSStsAssumeRole"
|
||||
)
|
||||
|
||||
// RefreshStatus reports the current state of credential refresh for a specific
|
||||
// provider credential.
|
||||
type RefreshStatus struct {
|
||||
ProviderName string
|
||||
ProviderID string
|
||||
CredentialKey string
|
||||
Strategy RefreshStrategy
|
||||
Status string
|
||||
ExpiresAt time.Time
|
||||
NextRefreshAt time.Time
|
||||
LastRefreshAt time.Time
|
||||
LastError string
|
||||
}
|
||||
|
||||
// RefreshConfig holds configuration parameters for gateway-owned credential
|
||||
// refresh on a provider credential.
|
||||
type RefreshConfig struct {
|
||||
Provider string
|
||||
CredentialKey string
|
||||
Strategy RefreshStrategy
|
||||
Material map[string]string
|
||||
SecretMaterialKeys []string
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// Sandbox represents a sandbox instance.
|
||||
type Sandbox struct {
|
||||
ID string
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
ResourceVersion uint64
|
||||
Workspace string
|
||||
DeletionTimestamp *time.Time
|
||||
Spec SandboxSpec
|
||||
Status SandboxStatus
|
||||
}
|
||||
|
||||
// SandboxSpec holds the desired state of a sandbox.
|
||||
type SandboxSpec struct {
|
||||
LogLevel string
|
||||
Environment map[string]string
|
||||
Template *SandboxTemplate
|
||||
Providers []string
|
||||
GPUCount *uint32
|
||||
// Policy is the security policy for the sandbox. Nil means no policy specified.
|
||||
Policy *SandboxPolicy
|
||||
}
|
||||
|
||||
// SandboxTemplate defines the container template for a sandbox.
|
||||
type SandboxTemplate struct {
|
||||
Image string
|
||||
RuntimeClassName string
|
||||
AgentSocket string
|
||||
Labels map[string]string
|
||||
Annotations map[string]string
|
||||
Environment map[string]string
|
||||
Resources map[string]any
|
||||
UserNamespaces *bool
|
||||
DriverConfig map[string]any
|
||||
}
|
||||
|
||||
// SandboxStatus holds the observed state of a sandbox.
|
||||
type SandboxStatus struct {
|
||||
SandboxName string
|
||||
AgentPod string
|
||||
AgentFd string
|
||||
SandboxFd string
|
||||
Phase SandboxPhase
|
||||
Conditions []SandboxCondition
|
||||
CurrentPolicyVersion uint32
|
||||
}
|
||||
|
||||
// SandboxCondition describes an observed condition of a sandbox.
|
||||
type SandboxCondition struct {
|
||||
Type string
|
||||
Status string
|
||||
Reason string
|
||||
Message string
|
||||
LastTransitionTime string
|
||||
}
|
||||
|
||||
// AttachProviderResult holds the result of attaching a provider to a sandbox.
|
||||
type AttachProviderResult struct {
|
||||
Sandbox *Sandbox
|
||||
Attached bool
|
||||
}
|
||||
|
||||
// DetachProviderResult holds the result of detaching a provider from a sandbox.
|
||||
type DetachProviderResult struct {
|
||||
Sandbox *Sandbox
|
||||
Detached bool
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// ServiceEndpoint represents an exposed HTTP service on a sandbox.
|
||||
type ServiceEndpoint struct {
|
||||
ID string
|
||||
SandboxID string
|
||||
SandboxName string
|
||||
ServiceName string
|
||||
TargetPort uint32
|
||||
Domain bool
|
||||
URL string
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// SettingValueType identifies which typed field of a SettingValue is active.
|
||||
type SettingValueType string
|
||||
|
||||
// SettingValueType constants.
|
||||
const (
|
||||
SettingValueString SettingValueType = "string"
|
||||
SettingValueBool SettingValueType = "bool"
|
||||
SettingValueInt SettingValueType = "int"
|
||||
SettingValueBytes SettingValueType = "bytes"
|
||||
)
|
||||
|
||||
// SettingValue is a typed setting value supporting string, bool, int64, and bytes variants.
|
||||
// The Type field indicates which value field is populated.
|
||||
type SettingValue struct {
|
||||
Type SettingValueType
|
||||
StringVal string
|
||||
BoolVal bool
|
||||
IntVal int64
|
||||
BytesVal []byte
|
||||
}
|
||||
|
||||
// SettingScope indicates whether a setting is controlled at sandbox or global level.
|
||||
type SettingScope string
|
||||
|
||||
// SettingScope constants.
|
||||
const (
|
||||
SettingScopeUnspecified SettingScope = ""
|
||||
SettingScopeSandbox SettingScope = "sandbox"
|
||||
SettingScopeGlobal SettingScope = "global"
|
||||
)
|
||||
|
||||
// PolicySource indicates the source of the policy payload in a SandboxConfig response.
|
||||
type PolicySource string
|
||||
|
||||
// PolicySource constants.
|
||||
const (
|
||||
PolicySourceUnspecified PolicySource = ""
|
||||
PolicySourceSandbox PolicySource = "sandbox"
|
||||
PolicySourceGlobal PolicySource = "global"
|
||||
)
|
||||
|
||||
// EffectiveSetting is a setting value paired with the scope it was resolved from.
|
||||
type EffectiveSetting struct {
|
||||
Value SettingValue
|
||||
Scope SettingScope
|
||||
}
|
||||
|
||||
// SandboxConfig represents the full configuration state of a sandbox,
|
||||
// including policy, effective settings, and revision metadata.
|
||||
type SandboxConfig struct {
|
||||
// Policy is the typed security policy for this sandbox. Nil means no policy in the response.
|
||||
Policy *SandboxPolicy
|
||||
// PolicyVersion is monotonically increasing per sandbox.
|
||||
PolicyVersion uint32
|
||||
// PolicyHash is the SHA-256 of the serialized policy payload.
|
||||
PolicyHash string
|
||||
// Settings is the effective settings resolved for this sandbox.
|
||||
Settings map[string]EffectiveSetting
|
||||
// ConfigRevision is the fingerprint for effective config (policy + settings).
|
||||
ConfigRevision uint64
|
||||
// PolicySource indicates where the policy came from (sandbox or global).
|
||||
PolicySource PolicySource
|
||||
// GlobalPolicyVersion is the global policy version (0 if not applicable).
|
||||
GlobalPolicyVersion uint32
|
||||
// ProviderEnvRevision is the fingerprint for provider credential inputs.
|
||||
ProviderEnvRevision uint64
|
||||
}
|
||||
|
||||
// GatewayConfig represents gateway-global settings.
|
||||
type GatewayConfig struct {
|
||||
// Settings is the global settings map.
|
||||
Settings map[string]SettingValue
|
||||
// SettingsRevision is a monotonically increasing revision for gateway-global settings.
|
||||
SettingsRevision uint64
|
||||
}
|
||||
|
||||
// ConfigUpdate represents a configuration mutation request.
|
||||
// For sandbox-scoped updates, set Name to the sandbox name.
|
||||
// For global-scoped updates, set Global to true.
|
||||
type ConfigUpdate struct {
|
||||
// Name is the sandbox name (required for sandbox-scoped updates).
|
||||
Name string
|
||||
// Policy is the typed security policy for a full policy replacement. Nil means no policy change.
|
||||
Policy *SandboxPolicy
|
||||
// SettingKey is a single setting key to mutate.
|
||||
SettingKey string
|
||||
// SettingValue is the setting value for upsert. Nil means no value change.
|
||||
SettingValue *SettingValue
|
||||
// DeleteSetting deletes the setting key when true.
|
||||
DeleteSetting bool
|
||||
// Global applies the update at gateway-global scope when true.
|
||||
Global bool
|
||||
// MergeOperations is a list of typed policy merge operations.
|
||||
MergeOperations []PolicyMergeOperation
|
||||
// ExpectedResourceVersion is for optimistic concurrency (0 = skip check).
|
||||
ExpectedResourceVersion uint64
|
||||
}
|
||||
|
||||
// ConfigUpdateResult holds the result of a configuration update operation.
|
||||
// Named ConfigUpdateResult to avoid collision with profile.UpdateResult.
|
||||
type ConfigUpdateResult struct {
|
||||
// Version is the assigned policy version.
|
||||
Version uint32
|
||||
// PolicyHash is the SHA-256 of the serialized policy.
|
||||
PolicyHash string
|
||||
// SettingsRevision is the settings revision for the modified scope.
|
||||
SettingsRevision uint64
|
||||
// Deleted is true when a setting delete removed an existing key.
|
||||
Deleted bool
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// SSHSession represents an SSH session created for a sandbox.
|
||||
// The Token field is sensitive and MUST NOT be logged or included in error messages.
|
||||
// The String() method redacts the token to prevent accidental exposure via fmt or logging.
|
||||
type SSHSession struct {
|
||||
// SandboxID is the sandbox this session connects to.
|
||||
SandboxID string
|
||||
// Token is the session token for gateway tunnel authentication.
|
||||
// This is a sensitive credential — treat it like an API key.
|
||||
Token string
|
||||
// GatewayHost is the host for SSH proxy connection.
|
||||
GatewayHost string
|
||||
// GatewayPort is the gateway port (1-65535).
|
||||
GatewayPort uint32
|
||||
// GatewayScheme is the gateway protocol scheme ("http" or "https").
|
||||
GatewayScheme string
|
||||
// HostKeyFingerprint is the optional host key fingerprint.
|
||||
HostKeyFingerprint string
|
||||
// ExpiresAtMs is the session expiry in milliseconds since epoch.
|
||||
// Zero means no expiry.
|
||||
ExpiresAtMs int64
|
||||
}
|
||||
|
||||
// String returns a human-readable representation with the Token redacted.
|
||||
func (s SSHSession) String() string {
|
||||
return fmt.Sprintf("SSHSession{SandboxID:%s, GatewayHost:%s, GatewayPort:%d, GatewayScheme:%s, Token:[REDACTED]}",
|
||||
s.SandboxID, s.GatewayHost, s.GatewayPort, s.GatewayScheme)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
// SandboxPhase represents the lifecycle phase of a sandbox.
|
||||
type SandboxPhase string
|
||||
|
||||
// SandboxPhase values for sandbox lifecycle.
|
||||
const (
|
||||
SandboxProvisioning SandboxPhase = "Provisioning"
|
||||
SandboxReady SandboxPhase = "Ready"
|
||||
SandboxError SandboxPhase = "Error"
|
||||
SandboxDeleting SandboxPhase = "Deleting"
|
||||
SandboxUnknown SandboxPhase = "Unknown"
|
||||
)
|
||||
|
||||
// EventType classifies watch events.
|
||||
type EventType string
|
||||
|
||||
// EventType values for watch events.
|
||||
const (
|
||||
EventAdded EventType = "ADDED"
|
||||
EventModified EventType = "MODIFIED"
|
||||
EventDeleted EventType = "DELETED"
|
||||
EventError EventType = "ERROR"
|
||||
)
|
||||
|
||||
// StreamType identifies which output stream a chunk belongs to.
|
||||
type StreamType string
|
||||
|
||||
// StreamType values for exec output.
|
||||
const (
|
||||
StreamStdout StreamType = "stdout"
|
||||
StreamStderr StreamType = "stderr"
|
||||
)
|
||||
|
||||
// TLSConfig holds TLS connection settings.
|
||||
type TLSConfig struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
CAFile string
|
||||
// Insecure skips TLS certificate verification. Use http:// for plaintext.
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// RetryPolicy configures automatic retry behavior for failed RPCs.
|
||||
type RetryPolicy struct {
|
||||
MaxRetries int
|
||||
InitialWait time.Duration
|
||||
MaxWait time.Duration
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package types
|
||||
|
||||
// Event represents a watch event carrying a resource that changed.
|
||||
type Event[T any] struct {
|
||||
Type EventType
|
||||
Object T
|
||||
Err error
|
||||
}
|
||||
|
||||
// WatchInterface delivers a stream of typed events. Modeled after
|
||||
// k8s.io/apimachinery/pkg/watch.Interface.
|
||||
type WatchInterface[T any] interface {
|
||||
ResultChan() <-chan Event[T]
|
||||
Stop()
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// --- Network Policy types ---
|
||||
|
||||
// NetworkPolicyRule defines a named network policy rule containing endpoints and binaries.
|
||||
type NetworkPolicyRule = types.NetworkPolicyRule
|
||||
|
||||
// PolicyNetworkEndpoint describes a full network endpoint in a sandbox network policy rule.
|
||||
type PolicyNetworkEndpoint = types.PolicyNetworkEndpoint
|
||||
|
||||
// PolicyNetworkBinary identifies a binary subject to network policy enforcement.
|
||||
type PolicyNetworkBinary = types.PolicyNetworkBinary
|
||||
|
||||
// L7Rule wraps an L7 allow rule.
|
||||
type L7Rule = types.L7Rule
|
||||
|
||||
// L7Allow specifies layer-7 allow criteria for HTTP/GraphQL traffic.
|
||||
type L7Allow = types.L7Allow
|
||||
|
||||
// L7DenyRule specifies layer-7 deny criteria for HTTP/GraphQL traffic.
|
||||
type L7DenyRule = types.L7DenyRule
|
||||
|
||||
// L7QueryMatcher matches query parameters by glob pattern or exact values.
|
||||
type L7QueryMatcher = types.L7QueryMatcher
|
||||
|
||||
// GraphqlOperation describes a GraphQL operation for persisted-query validation.
|
||||
type GraphqlOperation = types.GraphqlOperation
|
||||
|
||||
// --- MergeOperation types ---
|
||||
|
||||
// PolicyMergeOperation represents a single atomic policy mutation.
|
||||
type PolicyMergeOperation = types.PolicyMergeOperation
|
||||
|
||||
// AddNetworkRule adds a named network policy rule with a full rule definition.
|
||||
type AddNetworkRule = types.AddNetworkRule
|
||||
|
||||
// RemoveNetworkEndpoint removes a specific endpoint from a named rule.
|
||||
type RemoveNetworkEndpoint = types.RemoveNetworkEndpoint
|
||||
|
||||
// RemoveNetworkRule removes an entire named rule from the policy.
|
||||
type RemoveNetworkRule = types.RemoveNetworkRule
|
||||
|
||||
// AddDenyRules appends layer-7 deny rules to a specific endpoint.
|
||||
type AddDenyRules = types.AddDenyRules
|
||||
|
||||
// AddAllowRules appends layer-7 allow rules to a specific endpoint.
|
||||
type AddAllowRules = types.AddAllowRules
|
||||
|
||||
// RemoveNetworkBinary removes a binary from a named rule.
|
||||
type RemoveNetworkBinary = types.RemoveNetworkBinary
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/types"
|
||||
)
|
||||
|
||||
// Event represents a watch event carrying a resource that changed.
|
||||
type Event[T any] = types.Event[T]
|
||||
|
||||
// WatchInterface delivers a stream of typed events. Modeled after
|
||||
// k8s.io/apimachinery/pkg/watch.Interface.
|
||||
type WatchInterface[T any] = types.WatchInterface[T]
|
||||
|
||||
type watcher[T any] struct {
|
||||
result chan Event[T]
|
||||
done chan struct{}
|
||||
cancel context.CancelFunc
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newWatcher[T any](ch chan Event[T], cancel context.CancelFunc) *watcher[T] {
|
||||
return &watcher[T]{
|
||||
result: ch,
|
||||
done: make(chan struct{}),
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher[T]) ResultChan() <-chan Event[T] {
|
||||
return w.result
|
||||
}
|
||||
|
||||
func (w *watcher[T]) Stop() {
|
||||
w.stopOnce.Do(func() {
|
||||
close(w.done)
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestWatcher creates a watcher with a simulated producer goroutine that
|
||||
// forwards events from src to the watcher's channel and closes it when the
|
||||
// producer finishes or Stop is called.
|
||||
func newTestWatcher(src <-chan Event[string]) *watcher[string] {
|
||||
ch := make(chan Event[string], 10)
|
||||
w := newWatcher(ch, nil)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-src:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case ch <- ev:
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
case <-w.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return w
|
||||
}
|
||||
|
||||
// --- T038: WatchInterface event delivery, Stop, and error handling ---
|
||||
|
||||
func TestWatcher_DeliversEvents(t *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
src <- Event[string]{Type: EventAdded, Object: "sandbox-1"}
|
||||
src <- Event[string]{Type: EventModified, Object: "sandbox-1"}
|
||||
|
||||
resultCh := w.ResultChan()
|
||||
|
||||
ev1 := <-resultCh
|
||||
assert.Equal(t, EventAdded, ev1.Type)
|
||||
assert.Equal(t, "sandbox-1", ev1.Object)
|
||||
|
||||
ev2 := <-resultCh
|
||||
assert.Equal(t, EventModified, ev2.Type)
|
||||
assert.Equal(t, "sandbox-1", ev2.Object)
|
||||
}
|
||||
|
||||
func TestWatcher_StopClosesChannel(t *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
w.Stop()
|
||||
|
||||
select {
|
||||
case _, ok := <-w.ResultChan():
|
||||
assert.False(t, ok, "channel should be closed after Stop")
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for channel close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_StopIsIdempotent(_ *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
w.Stop()
|
||||
w.Stop() // must not panic
|
||||
}
|
||||
|
||||
func TestWatcher_ErrorEvent(t *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
src <- Event[string]{Type: EventError, Object: "error details"}
|
||||
|
||||
ev := <-w.ResultChan()
|
||||
assert.Equal(t, EventError, ev.Type)
|
||||
assert.Equal(t, "error details", ev.Object)
|
||||
}
|
||||
|
||||
func TestWatcher_ChannelClosesWhenSourceEnds(t *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
src <- Event[string]{Type: EventAdded, Object: "sb-1"}
|
||||
close(src)
|
||||
|
||||
ev := <-w.ResultChan()
|
||||
require.Equal(t, "sb-1", ev.Object)
|
||||
|
||||
select {
|
||||
case _, ok := <-w.ResultChan():
|
||||
assert.False(t, ok, "channel should close when source ends")
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for channel to close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatcher_DrainAfterStop(t *testing.T) {
|
||||
src := make(chan Event[string], 10)
|
||||
w := newTestWatcher(src)
|
||||
|
||||
src <- Event[string]{Type: EventAdded, Object: "sb-1"}
|
||||
|
||||
ev := <-w.ResultChan()
|
||||
require.Equal(t, "sb-1", ev.Object)
|
||||
|
||||
w.Stop()
|
||||
|
||||
timeout := time.After(time.Second)
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-w.ResultChan():
|
||||
if !ok {
|
||||
return // success: channel closed
|
||||
}
|
||||
case <-timeout:
|
||||
t.Fatal("timed out waiting for channel to close after Stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: datamodel.proto
|
||||
|
||||
package datamodelv1
|
||||
|
||||
import (
|
||||
_ "github.com/NVIDIA/OpenShell/sdk/go/proto/optionsv1"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Phase of a workspace's lifecycle.
|
||||
type WorkspacePhase int32
|
||||
|
||||
const (
|
||||
WorkspacePhase_WORKSPACE_PHASE_UNSPECIFIED WorkspacePhase = 0
|
||||
WorkspacePhase_WORKSPACE_PHASE_ACTIVE WorkspacePhase = 1
|
||||
WorkspacePhase_WORKSPACE_PHASE_TERMINATING WorkspacePhase = 2
|
||||
)
|
||||
|
||||
// Enum value maps for WorkspacePhase.
|
||||
var (
|
||||
WorkspacePhase_name = map[int32]string{
|
||||
0: "WORKSPACE_PHASE_UNSPECIFIED",
|
||||
1: "WORKSPACE_PHASE_ACTIVE",
|
||||
2: "WORKSPACE_PHASE_TERMINATING",
|
||||
}
|
||||
WorkspacePhase_value = map[string]int32{
|
||||
"WORKSPACE_PHASE_UNSPECIFIED": 0,
|
||||
"WORKSPACE_PHASE_ACTIVE": 1,
|
||||
"WORKSPACE_PHASE_TERMINATING": 2,
|
||||
}
|
||||
)
|
||||
|
||||
func (x WorkspacePhase) Enum() *WorkspacePhase {
|
||||
p := new(WorkspacePhase)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x WorkspacePhase) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (WorkspacePhase) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_datamodel_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (WorkspacePhase) Type() protoreflect.EnumType {
|
||||
return &file_datamodel_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x WorkspacePhase) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use WorkspacePhase.Descriptor instead.
|
||||
func (WorkspacePhase) EnumDescriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
// Kubernetes-style metadata shared by all top-level OpenShell domain objects.
|
||||
//
|
||||
// This structure provides consistent metadata (identity, labels, annotations,
|
||||
// timestamps, resource versioning) across Sandbox, Provider, SshSession, and
|
||||
// other resources.
|
||||
type ObjectMeta struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Stable object ID generated by the gateway.
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// Human-readable object name (unique per object type).
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
// Milliseconds since Unix epoch when the object was created.
|
||||
CreatedAtMs int64 `protobuf:"varint,3,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
// Key-value labels for filtering and organization.
|
||||
// Labels must follow Kubernetes conventions: alphanumeric + `-._/`, max 63 chars per segment.
|
||||
Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// Optimistic concurrency control version.
|
||||
// Incremented by the gateway on each update. Clients can use this for compare-and-swap operations.
|
||||
ResourceVersion uint64 `protobuf:"varint,5,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
|
||||
// Opaque key-value metadata that is not used for selectors.
|
||||
// Annotation keys use the same qualified-key shape as labels, but values may be longer.
|
||||
Annotations map[string]string `protobuf:"bytes,6,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// Workspace that owns this resource. Empty is normalized to "default" by the
|
||||
// gateway. Immutable after creation.
|
||||
Workspace string `protobuf:"bytes,7,opt,name=workspace,proto3" json:"workspace,omitempty"`
|
||||
// Milliseconds since Unix epoch when graceful deletion was initiated.
|
||||
// Zero means the object is not being deleted. Once set, this field is
|
||||
// immutable — the only path forward is completing deletion.
|
||||
DeletionTimestampMs int64 `protobuf:"varint,8,opt,name=deletion_timestamp_ms,json=deletionTimestampMs,proto3" json:"deletion_timestamp_ms,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) Reset() {
|
||||
*x = ObjectMeta{}
|
||||
mi := &file_datamodel_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ObjectMeta) ProtoMessage() {}
|
||||
|
||||
func (x *ObjectMeta) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_datamodel_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ObjectMeta.ProtoReflect.Descriptor instead.
|
||||
func (*ObjectMeta) Descriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetId() string {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetCreatedAtMs() int64 {
|
||||
if x != nil {
|
||||
return x.CreatedAtMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetLabels() map[string]string {
|
||||
if x != nil {
|
||||
return x.Labels
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetResourceVersion() uint64 {
|
||||
if x != nil {
|
||||
return x.ResourceVersion
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetAnnotations() map[string]string {
|
||||
if x != nil {
|
||||
return x.Annotations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetWorkspace() string {
|
||||
if x != nil {
|
||||
return x.Workspace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ObjectMeta) GetDeletionTimestampMs() int64 {
|
||||
if x != nil {
|
||||
return x.DeletionTimestampMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Status of a workspace.
|
||||
type WorkspaceStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Phase WorkspacePhase `protobuf:"varint,1,opt,name=phase,proto3,enum=openshell.datamodel.v1.WorkspacePhase" json:"phase,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WorkspaceStatus) Reset() {
|
||||
*x = WorkspaceStatus{}
|
||||
mi := &file_datamodel_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *WorkspaceStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*WorkspaceStatus) ProtoMessage() {}
|
||||
|
||||
func (x *WorkspaceStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_datamodel_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use WorkspaceStatus.ProtoReflect.Descriptor instead.
|
||||
func (*WorkspaceStatus) Descriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *WorkspaceStatus) GetPhase() WorkspacePhase {
|
||||
if x != nil {
|
||||
return x.Phase
|
||||
}
|
||||
return WorkspacePhase_WORKSPACE_PHASE_UNSPECIFIED
|
||||
}
|
||||
|
||||
// Workspace resource. A hard isolation boundary for sandboxes, providers, and
|
||||
// other workspace-scoped resources.
|
||||
type Workspace struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Kubernetes-style metadata (id, name, labels, timestamps, resource version).
|
||||
// The workspace field in this ObjectMeta is unused (a workspace does not
|
||||
// belong to another workspace).
|
||||
Metadata *ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
// Current lifecycle status.
|
||||
Status *WorkspaceStatus `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Workspace) Reset() {
|
||||
*x = Workspace{}
|
||||
mi := &file_datamodel_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Workspace) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Workspace) ProtoMessage() {}
|
||||
|
||||
func (x *Workspace) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_datamodel_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Workspace.ProtoReflect.Descriptor instead.
|
||||
func (*Workspace) Descriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Workspace) GetMetadata() *ObjectMeta {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Workspace) GetStatus() *WorkspaceStatus {
|
||||
if x != nil {
|
||||
return x.Status
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Opaque handle for a provider credential stored by gateway credential storage.
|
||||
// Handles are created by OpenShell and must not be authored by users.
|
||||
type CredentialHandle struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Internal storage owner or credential driver that owns this handle.
|
||||
Driver string `protobuf:"bytes,1,opt,name=driver,proto3" json:"driver,omitempty"`
|
||||
// Owner-owned opaque handle string.
|
||||
Handle string `protobuf:"bytes,2,opt,name=handle,proto3" json:"handle,omitempty"`
|
||||
// Owner-owned non-secret metadata.
|
||||
Metadata map[string]string `protobuf:"bytes,3,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *CredentialHandle) Reset() {
|
||||
*x = CredentialHandle{}
|
||||
mi := &file_datamodel_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *CredentialHandle) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CredentialHandle) ProtoMessage() {}
|
||||
|
||||
func (x *CredentialHandle) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_datamodel_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CredentialHandle.ProtoReflect.Descriptor instead.
|
||||
func (*CredentialHandle) Descriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *CredentialHandle) GetDriver() string {
|
||||
if x != nil {
|
||||
return x.Driver
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CredentialHandle) GetHandle() string {
|
||||
if x != nil {
|
||||
return x.Handle
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *CredentialHandle) GetMetadata() map[string]string {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Provider model stored by OpenShell.
|
||||
type Provider struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Kubernetes-style metadata (id, name, labels, timestamps, resource version).
|
||||
Metadata *ObjectMeta `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"`
|
||||
// Canonical provider type slug (for example: "claude", "gitlab").
|
||||
Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
|
||||
// Secret values used for authentication.
|
||||
Credentials map[string]string `protobuf:"bytes,3,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// Non-secret provider configuration.
|
||||
Config map[string]string `protobuf:"bytes,4,rep,name=config,proto3" json:"config,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
// Expiration timestamps for credential values, keyed by credential/env var
|
||||
// name. A zero or missing value means the credential does not expire.
|
||||
CredentialExpiresAtMs map[string]int64 `protobuf:"bytes,5,rep,name=credential_expires_at_ms,json=credentialExpiresAtMs,proto3" json:"credential_expires_at_ms,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
|
||||
// Workspace where this provider's type profile is stored.
|
||||
// Empty string = platform/global scope. Must be empty or match
|
||||
// metadata.workspace; cross-workspace references are rejected.
|
||||
ProfileWorkspace string `protobuf:"bytes,6,opt,name=profile_workspace,json=profileWorkspace,proto3" json:"profile_workspace,omitempty"`
|
||||
// Opaque handles for secret values stored through gateway credential storage.
|
||||
// This map is internal gateway state and is not accepted as user-authored input.
|
||||
CredentialHandles map[string]*CredentialHandle `protobuf:"bytes,7,rep,name=credential_handles,json=credentialHandles,proto3" json:"credential_handles,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Provider) Reset() {
|
||||
*x = Provider{}
|
||||
mi := &file_datamodel_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Provider) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Provider) ProtoMessage() {}
|
||||
|
||||
func (x *Provider) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_datamodel_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Provider.ProtoReflect.Descriptor instead.
|
||||
func (*Provider) Descriptor() ([]byte, []int) {
|
||||
return file_datamodel_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *Provider) GetMetadata() *ObjectMeta {
|
||||
if x != nil {
|
||||
return x.Metadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Provider) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Provider) GetCredentials() map[string]string {
|
||||
if x != nil {
|
||||
return x.Credentials
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Provider) GetConfig() map[string]string {
|
||||
if x != nil {
|
||||
return x.Config
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Provider) GetCredentialExpiresAtMs() map[string]int64 {
|
||||
if x != nil {
|
||||
return x.CredentialExpiresAtMs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Provider) GetProfileWorkspace() string {
|
||||
if x != nil {
|
||||
return x.ProfileWorkspace
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Provider) GetCredentialHandles() map[string]*CredentialHandle {
|
||||
if x != nil {
|
||||
return x.CredentialHandles
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_datamodel_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_datamodel_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x0fdatamodel.proto\x12\x16openshell.datamodel.v1\x1a\roptions.proto\"\xeb\x03\n" +
|
||||
"\n" +
|
||||
"ObjectMeta\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\"\n" +
|
||||
"\rcreated_at_ms\x18\x03 \x01(\x03R\vcreatedAtMs\x12F\n" +
|
||||
"\x06labels\x18\x04 \x03(\v2..openshell.datamodel.v1.ObjectMeta.LabelsEntryR\x06labels\x12)\n" +
|
||||
"\x10resource_version\x18\x05 \x01(\x04R\x0fresourceVersion\x12U\n" +
|
||||
"\vannotations\x18\x06 \x03(\v23.openshell.datamodel.v1.ObjectMeta.AnnotationsEntryR\vannotations\x12\x1c\n" +
|
||||
"\tworkspace\x18\a \x01(\tR\tworkspace\x122\n" +
|
||||
"\x15deletion_timestamp_ms\x18\b \x01(\x03R\x13deletionTimestampMs\x1a9\n" +
|
||||
"\vLabelsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a>\n" +
|
||||
"\x10AnnotationsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"O\n" +
|
||||
"\x0fWorkspaceStatus\x12<\n" +
|
||||
"\x05phase\x18\x01 \x01(\x0e2&.openshell.datamodel.v1.WorkspacePhaseR\x05phase\"\x8c\x01\n" +
|
||||
"\tWorkspace\x12>\n" +
|
||||
"\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12?\n" +
|
||||
"\x06status\x18\x02 \x01(\v2'.openshell.datamodel.v1.WorkspaceStatusR\x06status\"\xd3\x01\n" +
|
||||
"\x10CredentialHandle\x12\x16\n" +
|
||||
"\x06driver\x18\x01 \x01(\tR\x06driver\x12\x16\n" +
|
||||
"\x06handle\x18\x02 \x01(\tR\x06handle\x12R\n" +
|
||||
"\bmetadata\x18\x03 \x03(\v26.openshell.datamodel.v1.CredentialHandle.MetadataEntryR\bmetadata\x1a;\n" +
|
||||
"\rMetadataEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbf\x06\n" +
|
||||
"\bProvider\x12>\n" +
|
||||
"\bmetadata\x18\x01 \x01(\v2\".openshell.datamodel.v1.ObjectMetaR\bmetadata\x12\x12\n" +
|
||||
"\x04type\x18\x02 \x01(\tR\x04type\x12Y\n" +
|
||||
"\vcredentials\x18\x03 \x03(\v21.openshell.datamodel.v1.Provider.CredentialsEntryB\x04\x88\xb5\x18\x01R\vcredentials\x12D\n" +
|
||||
"\x06config\x18\x04 \x03(\v2,.openshell.datamodel.v1.Provider.ConfigEntryR\x06config\x12t\n" +
|
||||
"\x18credential_expires_at_ms\x18\x05 \x03(\v2;.openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntryR\x15credentialExpiresAtMs\x12+\n" +
|
||||
"\x11profile_workspace\x18\x06 \x01(\tR\x10profileWorkspace\x12f\n" +
|
||||
"\x12credential_handles\x18\a \x03(\v27.openshell.datamodel.v1.Provider.CredentialHandlesEntryR\x11credentialHandles\x1a>\n" +
|
||||
"\x10CredentialsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1a9\n" +
|
||||
"\vConfigEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\x1aH\n" +
|
||||
"\x1aCredentialExpiresAtMsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\x1an\n" +
|
||||
"\x16CredentialHandlesEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12>\n" +
|
||||
"\x05value\x18\x02 \x01(\v2(.openshell.datamodel.v1.CredentialHandleR\x05value:\x028\x01*n\n" +
|
||||
"\x0eWorkspacePhase\x12\x1f\n" +
|
||||
"\x1bWORKSPACE_PHASE_UNSPECIFIED\x10\x00\x12\x1a\n" +
|
||||
"\x16WORKSPACE_PHASE_ACTIVE\x10\x01\x12\x1f\n" +
|
||||
"\x1bWORKSPACE_PHASE_TERMINATING\x10\x02b\x06proto3"
|
||||
|
||||
var (
|
||||
file_datamodel_proto_rawDescOnce sync.Once
|
||||
file_datamodel_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_datamodel_proto_rawDescGZIP() []byte {
|
||||
file_datamodel_proto_rawDescOnce.Do(func() {
|
||||
file_datamodel_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_datamodel_proto_rawDesc), len(file_datamodel_proto_rawDesc)))
|
||||
})
|
||||
return file_datamodel_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_datamodel_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_datamodel_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
|
||||
var file_datamodel_proto_goTypes = []any{
|
||||
(WorkspacePhase)(0), // 0: openshell.datamodel.v1.WorkspacePhase
|
||||
(*ObjectMeta)(nil), // 1: openshell.datamodel.v1.ObjectMeta
|
||||
(*WorkspaceStatus)(nil), // 2: openshell.datamodel.v1.WorkspaceStatus
|
||||
(*Workspace)(nil), // 3: openshell.datamodel.v1.Workspace
|
||||
(*CredentialHandle)(nil), // 4: openshell.datamodel.v1.CredentialHandle
|
||||
(*Provider)(nil), // 5: openshell.datamodel.v1.Provider
|
||||
nil, // 6: openshell.datamodel.v1.ObjectMeta.LabelsEntry
|
||||
nil, // 7: openshell.datamodel.v1.ObjectMeta.AnnotationsEntry
|
||||
nil, // 8: openshell.datamodel.v1.CredentialHandle.MetadataEntry
|
||||
nil, // 9: openshell.datamodel.v1.Provider.CredentialsEntry
|
||||
nil, // 10: openshell.datamodel.v1.Provider.ConfigEntry
|
||||
nil, // 11: openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry
|
||||
nil, // 12: openshell.datamodel.v1.Provider.CredentialHandlesEntry
|
||||
}
|
||||
var file_datamodel_proto_depIdxs = []int32{
|
||||
6, // 0: openshell.datamodel.v1.ObjectMeta.labels:type_name -> openshell.datamodel.v1.ObjectMeta.LabelsEntry
|
||||
7, // 1: openshell.datamodel.v1.ObjectMeta.annotations:type_name -> openshell.datamodel.v1.ObjectMeta.AnnotationsEntry
|
||||
0, // 2: openshell.datamodel.v1.WorkspaceStatus.phase:type_name -> openshell.datamodel.v1.WorkspacePhase
|
||||
1, // 3: openshell.datamodel.v1.Workspace.metadata:type_name -> openshell.datamodel.v1.ObjectMeta
|
||||
2, // 4: openshell.datamodel.v1.Workspace.status:type_name -> openshell.datamodel.v1.WorkspaceStatus
|
||||
8, // 5: openshell.datamodel.v1.CredentialHandle.metadata:type_name -> openshell.datamodel.v1.CredentialHandle.MetadataEntry
|
||||
1, // 6: openshell.datamodel.v1.Provider.metadata:type_name -> openshell.datamodel.v1.ObjectMeta
|
||||
9, // 7: openshell.datamodel.v1.Provider.credentials:type_name -> openshell.datamodel.v1.Provider.CredentialsEntry
|
||||
10, // 8: openshell.datamodel.v1.Provider.config:type_name -> openshell.datamodel.v1.Provider.ConfigEntry
|
||||
11, // 9: openshell.datamodel.v1.Provider.credential_expires_at_ms:type_name -> openshell.datamodel.v1.Provider.CredentialExpiresAtMsEntry
|
||||
12, // 10: openshell.datamodel.v1.Provider.credential_handles:type_name -> openshell.datamodel.v1.Provider.CredentialHandlesEntry
|
||||
4, // 11: openshell.datamodel.v1.Provider.CredentialHandlesEntry.value:type_name -> openshell.datamodel.v1.CredentialHandle
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_datamodel_proto_init() }
|
||||
func file_datamodel_proto_init() {
|
||||
if File_datamodel_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_datamodel_proto_rawDesc), len(file_datamodel_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 12,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_datamodel_proto_goTypes,
|
||||
DependencyIndexes: file_datamodel_proto_depIdxs,
|
||||
EnumInfos: file_datamodel_proto_enumTypes,
|
||||
MessageInfos: file_datamodel_proto_msgTypes,
|
||||
}.Build()
|
||||
File_datamodel_proto = out.File
|
||||
file_datamodel_proto_goTypes = nil
|
||||
file_datamodel_proto_depIdxs = nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,204 @@
|
||||
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc (unknown)
|
||||
// source: options.proto
|
||||
|
||||
package optionsv1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Per-method authorization rule. Consumed at runtime by the gateway's
|
||||
// descriptor-pool-based auth table to enforce auth mode, role, and scope.
|
||||
type AuthorizationRule struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Authentication mode: "bearer", "sandbox", "dual", or "unauthenticated".
|
||||
AuthMode string `protobuf:"bytes,1,opt,name=auth_mode,json=authMode,proto3" json:"auth_mode,omitempty"`
|
||||
// Minimum workspace-level role required (checked by handler via
|
||||
// authorize_workspace): "user" or "admin". Mutually exclusive with
|
||||
// global_role.
|
||||
WorkspaceRole string `protobuf:"bytes,2,opt,name=workspace_role,json=workspaceRole,proto3" json:"workspace_role,omitempty"`
|
||||
// Global role required (checked by middleware via OIDC claims):
|
||||
// "platform_admin". Mutually exclusive with workspace_role.
|
||||
GlobalRole string `protobuf:"bytes,3,opt,name=global_role,json=globalRole,proto3" json:"global_role,omitempty"`
|
||||
// Required OIDC scope on the bearer path (e.g. "sandbox:read").
|
||||
Scope string `protobuf:"bytes,4,opt,name=scope,proto3" json:"scope,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) Reset() {
|
||||
*x = AuthorizationRule{}
|
||||
mi := &file_options_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*AuthorizationRule) ProtoMessage() {}
|
||||
|
||||
func (x *AuthorizationRule) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_options_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use AuthorizationRule.ProtoReflect.Descriptor instead.
|
||||
func (*AuthorizationRule) Descriptor() ([]byte, []int) {
|
||||
return file_options_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) GetAuthMode() string {
|
||||
if x != nil {
|
||||
return x.AuthMode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) GetWorkspaceRole() string {
|
||||
if x != nil {
|
||||
return x.WorkspaceRole
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) GetGlobalRole() string {
|
||||
if x != nil {
|
||||
return x.GlobalRole
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *AuthorizationRule) GetScope() string {
|
||||
if x != nil {
|
||||
return x.Scope
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var file_options_proto_extTypes = []protoimpl.ExtensionInfo{
|
||||
{
|
||||
ExtendedType: (*descriptorpb.MethodOptions)(nil),
|
||||
ExtensionType: (*AuthorizationRule)(nil),
|
||||
Field: 50000,
|
||||
Name: "openshell.options.v1.authorization",
|
||||
Tag: "bytes,50000,opt,name=authorization",
|
||||
Filename: "options.proto",
|
||||
},
|
||||
{
|
||||
ExtendedType: (*descriptorpb.FieldOptions)(nil),
|
||||
ExtensionType: (*bool)(nil),
|
||||
Field: 50001,
|
||||
Name: "openshell.options.v1.secret",
|
||||
Tag: "varint,50001,opt,name=secret",
|
||||
Filename: "options.proto",
|
||||
},
|
||||
}
|
||||
|
||||
// Extension fields to descriptorpb.MethodOptions.
|
||||
var (
|
||||
// Authorization metadata for a gRPC method.
|
||||
//
|
||||
// optional openshell.options.v1.AuthorizationRule authorization = 50000;
|
||||
E_Authorization = &file_options_proto_extTypes[0]
|
||||
)
|
||||
|
||||
// Extension fields to descriptorpb.FieldOptions.
|
||||
var (
|
||||
// optional bool secret = 50001;
|
||||
E_Secret = &file_options_proto_extTypes[1]
|
||||
)
|
||||
|
||||
var File_options_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_options_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\roptions.proto\x12\x14openshell.options.v1\x1a google/protobuf/descriptor.proto\"\x8e\x01\n" +
|
||||
"\x11AuthorizationRule\x12\x1b\n" +
|
||||
"\tauth_mode\x18\x01 \x01(\tR\bauthMode\x12%\n" +
|
||||
"\x0eworkspace_role\x18\x02 \x01(\tR\rworkspaceRole\x12\x1f\n" +
|
||||
"\vglobal_role\x18\x03 \x01(\tR\n" +
|
||||
"globalRole\x12\x14\n" +
|
||||
"\x05scope\x18\x04 \x01(\tR\x05scope:o\n" +
|
||||
"\rauthorization\x12\x1e.google.protobuf.MethodOptions\x18І\x03 \x01(\v2'.openshell.options.v1.AuthorizationRuleR\rauthorization:7\n" +
|
||||
"\x06secret\x12\x1d.google.protobuf.FieldOptions\x18ц\x03 \x01(\bR\x06secretb\x06proto3"
|
||||
|
||||
var (
|
||||
file_options_proto_rawDescOnce sync.Once
|
||||
file_options_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_options_proto_rawDescGZIP() []byte {
|
||||
file_options_proto_rawDescOnce.Do(func() {
|
||||
file_options_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_options_proto_rawDesc), len(file_options_proto_rawDesc)))
|
||||
})
|
||||
return file_options_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_options_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_options_proto_goTypes = []any{
|
||||
(*AuthorizationRule)(nil), // 0: openshell.options.v1.AuthorizationRule
|
||||
(*descriptorpb.MethodOptions)(nil), // 1: google.protobuf.MethodOptions
|
||||
(*descriptorpb.FieldOptions)(nil), // 2: google.protobuf.FieldOptions
|
||||
}
|
||||
var file_options_proto_depIdxs = []int32{
|
||||
1, // 0: openshell.options.v1.authorization:extendee -> google.protobuf.MethodOptions
|
||||
2, // 1: openshell.options.v1.secret:extendee -> google.protobuf.FieldOptions
|
||||
0, // 2: openshell.options.v1.authorization:type_name -> openshell.options.v1.AuthorizationRule
|
||||
3, // [3:3] is the sub-list for method output_type
|
||||
3, // [3:3] is the sub-list for method input_type
|
||||
2, // [2:3] is the sub-list for extension type_name
|
||||
0, // [0:2] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_options_proto_init() }
|
||||
func file_options_proto_init() {
|
||||
if File_options_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_options_proto_rawDesc), len(file_options_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 2,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_options_proto_goTypes,
|
||||
DependencyIndexes: file_options_proto_depIdxs,
|
||||
MessageInfos: file_options_proto_msgTypes,
|
||||
ExtensionInfos: file_options_proto_extTypes,
|
||||
}.Build()
|
||||
File_options_proto = out.File
|
||||
file_options_proto_goTypes = nil
|
||||
file_options_proto_depIdxs = nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -56,7 +56,7 @@ hide = true
|
||||
|
||||
[ci]
|
||||
description = "Run full checks (lint, compile/type checks, and tests)"
|
||||
depends = ["lint", "check", "test"]
|
||||
depends = ["lint", "check", "test", "go:ci"]
|
||||
|
||||
[all]
|
||||
description = "Alias for ci"
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Go SDK development, build, lint, and format tasks
|
||||
|
||||
["go:test"]
|
||||
description = "Run Go SDK unit tests with coverage"
|
||||
dir = "sdk/go"
|
||||
run = "go test -coverprofile=coverage.out -coverpkg=./openshell/... -race ./..."
|
||||
hide = true
|
||||
|
||||
["go:test:integration"]
|
||||
description = "Run Go SDK integration tests"
|
||||
dir = "sdk/go"
|
||||
run = "go test -tags=integration -race ./..."
|
||||
hide = true
|
||||
|
||||
["go:lint"]
|
||||
description = "Run Go SDK linter"
|
||||
dir = "sdk/go"
|
||||
run = "golangci-lint run ./..."
|
||||
hide = true
|
||||
|
||||
["go:fmt"]
|
||||
description = "Format Go SDK code"
|
||||
dir = "sdk/go"
|
||||
run = "goimports -w . && go fmt ./..."
|
||||
hide = true
|
||||
|
||||
["go:build"]
|
||||
description = "Build Go SDK packages"
|
||||
dir = "sdk/go"
|
||||
run = "go build ./..."
|
||||
hide = true
|
||||
|
||||
["go:format:check"]
|
||||
description = "Verify Go SDK code is gofmt-formatted"
|
||||
dir = "sdk/go"
|
||||
run = """
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
UNFORMATTED=$(gofmt -l . 2>/dev/null || true)
|
||||
if [ -n "$UNFORMATTED" ]; then
|
||||
echo "ERROR: The following files are not gofmt-formatted:"
|
||||
echo "$UNFORMATTED"
|
||||
exit 1
|
||||
fi
|
||||
"""
|
||||
hide = true
|
||||
|
||||
["go:ci"]
|
||||
description = "Run Go SDK full CI pipeline"
|
||||
depends = ["go:format:check", "go:lint", "go:build", "go:test", "go:proto:check", "go:docs:check"]
|
||||
|
||||
["go:docs:check"]
|
||||
description = "Verify every public Go SDK package has a docs page"
|
||||
dir = "sdk/go"
|
||||
run = """
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
DOCS_DIR="docs/src/api"
|
||||
SUMMARY="docs/src/SUMMARY.md"
|
||||
MISSING=0
|
||||
|
||||
# Find all public packages with a doc.go (excluding internal, proto, types)
|
||||
for docfile in openshell/v1/*/doc.go; do
|
||||
pkg=$(basename "$(dirname "$docfile")")
|
||||
|
||||
# Skip internal packages and types (no user-facing docs needed)
|
||||
case "$pkg" in
|
||||
internal|types) continue ;;
|
||||
esac
|
||||
|
||||
# Check for matching docs page
|
||||
if [ ! -f "$DOCS_DIR/$pkg.md" ]; then
|
||||
echo "MISSING: $DOCS_DIR/$pkg.md (package openshell/v1/$pkg has doc.go but no docs page)"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
|
||||
# Check for SUMMARY.md entry
|
||||
if ! grep -q "api/$pkg.md" "$SUMMARY" 2>/dev/null; then
|
||||
echo "MISSING: SUMMARY.md entry for api/$pkg.md"
|
||||
MISSING=$((MISSING + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$MISSING" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "ERROR: $MISSING documentation gaps found."
|
||||
echo "Every public package with doc.go needs a docs/src/api/<pkg>.md page"
|
||||
echo "and a SUMMARY.md entry. See Constitution XIII."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Docs check passed: all public packages have documentation."
|
||||
"""
|
||||
hide = true
|
||||
|
||||
["go:proto:gen"]
|
||||
description = "Generate Go bindings from proto files using buf"
|
||||
dir = "sdk/go"
|
||||
run = """
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
for tool in buf protoc-gen-go protoc-gen-go-grpc; do
|
||||
if ! command -v "$tool" &>/dev/null; then
|
||||
echo "ERROR: $tool not found. Run 'mise install' to install it."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean previous output before regeneration
|
||||
find proto -name '*.pb.go' -delete 2>/dev/null || true
|
||||
|
||||
buf generate
|
||||
|
||||
echo "Proto generation complete."
|
||||
echo "Generated packages:"
|
||||
for pkg in openshellv1 datamodelv1 sandboxv1 optionsv1; do
|
||||
count=$(find "proto/$pkg" -name '*.go' 2>/dev/null | wc -l | tr -d ' ')
|
||||
echo " proto/$pkg/: $count files"
|
||||
done
|
||||
"""
|
||||
hide = true
|
||||
|
||||
["go:proto:check"]
|
||||
description = "Verify generated Go SDK proto files are up to date"
|
||||
dir = "sdk/go"
|
||||
run = """
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
for tool in buf protoc-gen-go protoc-gen-go-grpc; do
|
||||
if ! command -v "$tool" &>/dev/null; then
|
||||
echo "ERROR: $tool not found. Run 'mise install' to install it."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
WORK_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
# Generate to temp directory with adjusted output path
|
||||
sed "s|out: \\.|out: $WORK_DIR|" buf.gen.yaml > "$WORK_DIR/buf.gen.yaml"
|
||||
buf generate --template "$WORK_DIR/buf.gen.yaml"
|
||||
|
||||
DIFF_OUTPUT=$(diff -r "$WORK_DIR/proto" "proto" \
|
||||
--exclude="*.proto" \
|
||||
2>&1) || true
|
||||
|
||||
if [ -n "$DIFF_OUTPUT" ]; then
|
||||
echo "ERROR: Generated proto files are out of date."
|
||||
echo "Run 'mise run go:proto:gen' to regenerate."
|
||||
echo ""
|
||||
echo "$DIFF_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Proto check passed: generated files are up to date."
|
||||
"""
|
||||
hide = true
|
||||
Reference in New Issue
Block a user