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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ an implementation that depends on workspace ownership. These fields describe
provisioning intent; they do not authorize resource replacement. Duplicate
creation and explicit Delete remain separate operations.

### Reuse validation in API 1.2

A runtime advertising `capabilities.reuse_preflight` implements `ReusePreflight`.
Before reusing an existing workspace, the host supplies its workspace ID and
resolved, nonempty developer identity in `remote_user`. The runtime validates
its current creation-time contract, including ownership or mount policy, without
starting, stopping, deleting, or changing the workspace.

An incompatible contract returns structured `FailedPrecondition` with a reason
and explicit recreation guidance. This failure does not authorize automatic
replacement. The host propagates it and leaves the workspace intact; explicit
recreation follows the separate provisioning checks and negotiated recreate mode.
Missing workspaces return `NotFound`; backend and context failures remain errors.
Hosts skip this optional RPC when it is not advertised. The conformance suite
checks successful reuse and cancellation without resource mutation; backend
suites must cover their own incompatible contracts.

## Development

```sh
Expand Down
2 changes: 1 addition & 1 deletion conformance/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (d *Driver) Info(context.Context, *runtimev1.InfoRequest) (*runtimev1.InfoR
Capabilities: &runtimev1.Capabilities{
MountTypes: slices.Clone(d.config.MountTypes),
RecreateMode: runtimev1.RecreateMode_RECREATE_MODE_STOP,
ProvisioningPreflight: true, Logs: true,
ProvisioningPreflight: true, Logs: true, ReusePreflight: true,
},
}
if d.config.Mode == IncompatibleVersion {
Expand Down
6 changes: 4 additions & 2 deletions conformance/fake/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import (
)

const (
running = "running"
stopped = "stopped"
running = "running"
stopped = "stopped"
remoteUserLabel = "fake.remote-user"
)

// Find returns ordinary absence without a gRPC error.
Expand Down Expand Up @@ -162,6 +163,7 @@ func newContainer(req *runtimev1.RunImageRequest) *runtimev1.ContainerDetails {
key, value, _ := strings.Cut(label, "=")
container.Config.Labels[key] = value
}
container.Config.Labels[remoteUserLabel] = effectiveRemoteUser(req)
mounts := append([]*runtimev1.Mount{}, req.GetMounts()...)
if req.GetWorkspaceMount() != nil {
mounts = append(mounts, req.GetWorkspaceMount())
Expand Down
48 changes: 48 additions & 0 deletions conformance/fake/reuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package fake

import (
"context"

"github.com/devsy-org/devsy-runtime-sdk/runtimev1"
"google.golang.org/grpc/codes"
)

// ReusePreflight checks the saved developer identity without changing resource state.
func (d *Driver) ReusePreflight(
ctx context.Context,
req *runtimev1.ReusePreflightRequest,
) (*runtimev1.ReusePreflightResponse, error) {
d.mu.Lock()
defer d.mu.Unlock()
if err := validateWorkspace(ctx, req.GetWorkspaceId()); err != nil {
return nil, err
}
if req.GetRemoteUser() == "" {
return nil, runtimeError(codes.InvalidArgument,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_INVALID_ARGUMENT,
"remote_user is required", false)
}
container, err := d.load(req.GetWorkspaceId())
if err != nil {
return nil, storageError()
}
if container == nil {
return nil, missingWorkspace()
}
if container.GetConfig().GetLabels()[remoteUserLabel] != req.GetRemoteUser() {
return nil, runtimeError(codes.FailedPrecondition,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_FAILED_PRECONDITION,
"workspace developer identity changed; rerun with --recreate", false)
}
return &runtimev1.ReusePreflightResponse{}, nil
}

func effectiveRemoteUser(req *runtimev1.RunImageRequest) string {
if req.GetRemoteUser() != "" {
return req.GetRemoteUser()
}
if req.GetUser() != "" {
return req.GetUser()
}
return "root"
}
78 changes: 78 additions & 0 deletions conformance/fake/reuse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package fake_test

import (
"context"
"testing"

"github.com/devsy-org/devsy-runtime-sdk/conformance/fake"
"github.com/devsy-org/devsy-runtime-sdk/runtimev1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)

func TestReusePreflightPreservesWorkspace(t *testing.T) {
driver := connect(t, launch(t, t.TempDir(), fake.Normal))
createWorkspace(t, driver)
before, err := driver.Find(testContext(t), &runtimev1.FindRequest{WorkspaceId: workspaceID})
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name, id, user string
code codes.Code
category runtimev1.RuntimeErrorCode
}{
{name: "same identity", id: workspaceID, user: "1000", code: codes.OK},
{
name: "changed identity", id: workspaceID, user: "root", code: codes.FailedPrecondition,
category: runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_FAILED_PRECONDITION,
},
{
name: "missing identity", id: workspaceID, code: codes.InvalidArgument,
category: runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_INVALID_ARGUMENT,
},
{
name: "missing workspace", id: "absent", user: "1000", code: codes.NotFound,
category: runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_NOT_FOUND,
},
{
name: "missing workspace id", user: "1000", code: codes.InvalidArgument,
category: runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_INVALID_ARGUMENT,
},
} {
t.Run(tc.name, func(t *testing.T) {
_, err := driver.ReusePreflight(testContext(t), &runtimev1.ReusePreflightRequest{
WorkspaceId: tc.id, RemoteUser: tc.user,
})
if tc.code == codes.OK {
if err != nil {
t.Fatal(err)
}
} else {
assertRuntimeError(t, err, tc.code, tc.category)
}
after, err := driver.Find(
testContext(t),
&runtimev1.FindRequest{WorkspaceId: workspaceID},
)
if err != nil || !proto.Equal(before, after) {
t.Fatalf("reuse preflight changed workspace: %v, %v", after, err)
}
})
}
}

func TestReusePreflightCancellation(t *testing.T) {
driver := connect(t, launch(t, t.TempDir(), fake.Normal))
createWorkspace(t, driver)
ctx, cancel := context.WithCancel(testContext(t))
cancel()
_, err := driver.ReusePreflight(ctx, &runtimev1.ReusePreflightRequest{
WorkspaceId: workspaceID, RemoteUser: "1000",
})
if status.Code(err) != codes.Canceled {
t.Fatalf("canceled preflight returned %v", err)
}
assertState(t, driver, "running")
}
54 changes: 54 additions & 0 deletions conformance/reuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package conformance

import (
"context"
"testing"

"github.com/devsy-org/devsy-runtime-sdk/runtimev1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)

func reusePreflight(t *testing.T, options Options) {
s := open(t, options)
if !s.info.GetCapabilities().GetReusePreflight() {
t.Skip("runtime does not advertise reuse preflight")
}
s.create(t)
before, err := s.client.Find(
s.ctx,
&runtimev1.FindRequest{WorkspaceId: s.request.GetWorkspaceId()},
)
if err != nil {
t.Fatal(err)
}
request := &runtimev1.ReusePreflightRequest{
WorkspaceId: s.request.GetWorkspaceId(), RemoteUser: reuseRemoteUser(s.request),
}
if _, err := s.client.ReusePreflight(s.ctx, request); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(s.ctx)
cancel()
if _, err := s.client.ReusePreflight(ctx, request); status.Code(err) != codes.Canceled {
t.Fatalf("canceled reuse preflight returned %v", err)
}
after, err := s.client.Find(
s.ctx,
&runtimev1.FindRequest{WorkspaceId: s.request.GetWorkspaceId()},
)
if err != nil || !proto.Equal(before, after) {
t.Fatalf("reuse preflight changed workspace: %v, %v", after, err)
}
}

func reuseRemoteUser(request *runtimev1.RunImageRequest) string {
if request.GetRemoteUser() != "" {
return request.GetRemoteUser()
}
if request.GetUser() != "" {
return request.GetUser()
}
return "root"
}
1 change: 1 addition & 0 deletions conformance/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func Run(t *testing.T, options Options) {
}{
{"discovery", discovery},
{"lifecycle", lifecycle},
{"reuse-preflight", reusePreflight},
{"binary-echo", binaryEcho},
{"empty-stdin", emptyInput},
{"arguments", arguments},
Expand Down
14 changes: 14 additions & 0 deletions proto/devsy/runtime/v1/runtime.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ service RuntimeDriver {
rpc Info(InfoRequest) returns (InfoResponse);
rpc Preflight(PreflightRequest) returns (PreflightResponse);
rpc ProvisioningPreflight(ProvisioningPreflightRequest) returns (ProvisioningPreflightResponse);
// Read-only validation before reusing an existing workspace (API 1.2).
// FailedPrecondition requires explicit recreation; never mutate the workspace.
rpc ReusePreflight(ReusePreflightRequest) returns (ReusePreflightResponse);

rpc Find(FindRequest) returns (FindResponse);
rpc TargetArchitecture(TargetArchitectureRequest) returns (TargetArchitectureResponse);
Expand Down Expand Up @@ -50,6 +53,9 @@ message Capabilities {
bool reprovision = 6;

bool logs = 7;

// Validate an existing workspace's creation-time contract without mutation.
bool reuse_preflight = 8;
}

enum MountType {
Expand All @@ -75,6 +81,14 @@ message ProvisioningPreflightRequest {}

message ProvisioningPreflightResponse {}

message ReusePreflightRequest {
string workspace_id = 1;
// Resolved, nonempty developer identity for the requested reuse.
string remote_user = 2;
}

message ReusePreflightResponse {}

message FindRequest {
string workspace_id = 1;
}
Expand Down
Loading
Loading