Skip to content

Latest commit

 

History

History
237 lines (208 loc) · 9.66 KB

File metadata and controls

237 lines (208 loc) · 9.66 KB

Hosted SDK And API Clients

Port now publishes a supported typed client surface in the port-sdk crate for hosted machine, guest, and service operations.

The crate now covers both typed request construction and live JSON response execution against the hosted control-plane transport that Port ships for the single-node demo lane.

Scope

Shipped today:

  • HostedClient::from_machine derives hosted endpoint, audience, and auth header shape from the shared Port model plus port-hosted-protocol
  • HostedClient::from_machine_env and HostedClient::from_control_plane_env derive the auth token source from the model and read the configured environment variable automatically
  • machines() mirrors port machine list|status|monitor|top|stop
  • guest() mirrors port guest exec|copy|pty|logs|forward using the existing port-agent-protocol request payloads
  • guest().pty_stream(), logs_stream(), copy_stream(), and forward_stream() publish the streamed hosted request builders when the caller needs explicit control over the transport contract instead of the completed JSON helper
  • guest().forward_detached_start(), forward_detached_list(), and forward_detached_stop() mirror hosted port guest forward --lifecycle detached, --list, and --stop --name ... through the same control-plane route family
  • services() mirrors port service secret put|list|remove plus port service apply|list|status|stop
  • those service calls execute through the same live hosted control-plane and node-agent path as the CLI demo lane; they do not define a second hosted-only service surface
  • managed guest-process start|list|status|stop stays internal to the shared guest/runtime contract, so the SDK does not add a second hosted-only service client family
  • HostedClient::execute_json performs the live HTTP request and decodes structured success or hosted route errors
  • port-hosted-protocol publishes the shared hosted HTTP route, auth-header, and route-context contract that the SDK now uses directly
  • hosted guest responses surface HostedSuccess::route.guest_session, which carries the stable machine-scoped session identifier plus the canonical Port shell-driver metadata for guest-backed hosted exec, copy, pty, logs, and forward
  • guest().shell_driver_contract(machine) derives that same canonical machine-scoped shell-driver contract ahead of time for streamed pty and forward integrations that need a stable identity before opening the hosted byte stream

Still planned:

  • retries and richer client policies on top of the shipped transport
  • generated or versioned external API packages beyond the in-repo Rust crate
  • advanced auth, RBAC, and multi-tenant concerns
  • external secret-manager integrations and broader orchestration policy beyond the shipped restart/health/runtime-file service contract

Example

use port_agent_protocol::ExecRequest;
use port_model::PortConfig;
use port_sdk::{HostedClient, ServiceApplyRequest, ServiceKind, ServiceSecretBinding};

let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;

let status = client.machines().status("cloud-aws");
let exec = client.guest().exec(
    "cloud-aws",
    ExecRequest {
        command: vec!["/bin/echo".into(), "hello".into()],
        cwd: None,
        env: Default::default(),
    },
)?;
let service = client.services().apply(
    "cloud-aws",
    ServiceApplyRequest {
        name: "buildbox".into(),
        kind: ServiceKind::Sandbox,
        command: vec!["/bin/sh".into(), "-lc".into(), "make test".into()],
        secret_bindings: vec![ServiceSecretBinding {
            env: "API_TOKEN".into(),
            secret: "demo-token".into(),
        }],
    },
)?;

assert_eq!(status.url, "https://port.example.internal/v1/machines/cloud-aws");
assert_eq!(exec.url, "https://port.example.internal/v1/machines/cloud-aws/guest:exec");
assert_eq!(service.url, "https://port.example.internal/v1/machines/cloud-aws/services");
# Ok::<(), anyhow::Error>(())

If a hosted control plane is running and the token source is configured in the environment, the same client can execute the request directly:

# use port_hosted_protocol::HostedSuccess;
# use port_sdk::HostedClient;
# use port_model::PortConfig;
let config = PortConfig::sample();
let client = HostedClient::from_machine_env(&config, "cloud-aws")?;
let status: HostedSuccess<serde_json::Value> =
    client.execute_json(client.machines().status("cloud-aws"))?;
# Ok::<(), anyhow::Error>(())

Hosted guest requests expose the session and driver contract through the same route context:

# use port_agent_protocol::ExecRequest;
# use port_hosted_protocol::HostedSuccess;
# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let exec: HostedSuccess<serde_json::Value> = client.execute_json(
    client.guest().exec(
        "cloud-aws",
        ExecRequest {
            command: vec!["/bin/echo".into(), "hello".into()],
            cwd: None,
            env: Default::default(),
        },
    )?,
)?;
let session = exec
    .route
    .guest_session
    .expect("hosted guest session contract");
assert_eq!(session.id, "port-hosted://demo/machines/cloud-aws/guest-session");
assert_eq!(session.driver.id, "port-guest-shell-driver-v1");
# Ok::<(), anyhow::Error>(())

For streamed hosted requests, derive the same contract before opening the transport:

# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let session = client.guest().shell_driver_contract("cloud-aws")?;
assert_eq!(session.id, "port-hosted://demo/machines/cloud-aws/guest-session");
assert_eq!(session.driver.id, "port-guest-shell-driver-v1");
# Ok::<(), anyhow::Error>(())

That helper is the canonical upstream shell-driver contract for hosted guest integration:

Operation Canonical Port surface Lifecycle model Stable contract surface
exec guest().exec() and port guest exec Request/response via HostedClient::execute_json HostedSuccess::route.guest_session on hosted responses
pty guest().pty_stream() and port guest pty Bidirectional stream until guest exit guest().shell_driver_contract(machine) before stream open
forward guest().forward_stream() or detached helpers and port guest forward Byte stream or detached listener lifecycle guest().shell_driver_contract(machine) before start/list/stop

The helper is only available when the client is created from Port control-plane configuration, because generic HostedClient::new(...) callers do not know the stable control-plane name needed for the session identifier.

See crates/port-sdk/examples/hosted-sdk.rs for the in-repo sample program.

The streamed hosted request builders stay explicit:

# use port_agent_protocol::{ForwardRequest, LogsRequest};
# use port_hosted_protocol::HostedDetachedForwardStartRequest;
# use port_model::PortConfig;
# use port_sdk::HostedClient;
let config = PortConfig::sample();
let client = HostedClient::from_machine(&config, "cloud-aws", "demo-token")?;
let logs = client.guest().logs_stream(
    "cloud-aws",
    LogsRequest {
        path: "/var/log/app.log".into(),
        follow: true,
        tail_lines: None,
    },
)?;
let forward = client.guest().forward_stream(
    "cloud-aws",
    ForwardRequest {
        listen: "127.0.0.1:8081".into(),
        target: "127.0.0.1:80".into(),
    },
)?;
let detached = client.guest().forward_detached_start(
    "cloud-aws",
    HostedDetachedForwardStartRequest {
        listen: "unix:/tmp/cloud-aws.sock".into(),
        target: "unix:/var/run/app.sock".into(),
        forward_name: "demo-sock".into(),
    },
)?;
let detached_list = client.guest().forward_detached_list("cloud-aws");
let detached_stop = client
    .guest()
    .forward_detached_stop("cloud-aws", "demo-sock");
assert_eq!(logs.request.url, "https://port.example.internal/v1/machines/cloud-aws/guest:logs:stream");
assert_eq!(forward.request.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:stream");
assert_eq!(detached.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached");
assert_eq!(detached_list.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached");
assert_eq!(detached_stop.url, "https://port.example.internal/v1/machines/cloud-aws/guest:forward:detached/demo-sock/stop");
# Ok::<(), anyhow::Error>(())

HostedClient::execute_json remains the live helper for the shipped JSON machine, guest, and service routes. Stream requests stay typed and explicit so the caller owns any long-lived byte-stream transport policy layered on top.

API Shape

Canonical hosted request paths now documented by Port:

  • GET /v1/machines
  • GET /v1/machines/{machine}
  • GET /v1/machines/{machine}/monitor
  • GET /v1/machines/{machine}/top
  • POST /v1/machines/{machine}:stop
  • POST /v1/machines/{machine}/guest:exec|copy|pty|logs|forward
  • PUT /v1/machines/{machine}/secrets/{secret}
  • GET /v1/machines/{machine}/secrets
  • DELETE /v1/machines/{machine}/secrets/{secret}
  • POST /v1/machines/{machine}/services
  • GET /v1/machines/{machine}/services
  • GET /v1/machines/{machine}/services/{name}
  • POST /v1/machines/{machine}/services/{name}:stop

The SDK mirrors those paths exactly so later transport work can build on a stable typed surface instead of inventing a second client model. The route and header definitions themselves live in crates/port-hosted-protocol.