Skip to content

Commit 69a2bc1

Browse files
committed
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>
1 parent c8c65fb commit 69a2bc1

8 files changed

Lines changed: 50 additions & 30 deletions

File tree

‎sdk/go/openshell/v1/doc.go‎

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,37 +21,37 @@
2121
//
2222
// # Sandbox Lifecycle
2323
//
24-
// sandbox, err := client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{
24+
// sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
2525
// Template: &v1.SandboxTemplate{Image: "python:3.12"},
2626
// Environment: map[string]string{"LANG": "en_US.UTF-8"},
2727
// }, nil)
2828
// if err != nil {
2929
// log.Fatal(err)
3030
// }
3131
//
32-
// sandbox, err = client.Sandboxes().WaitReady(ctx, sandbox.Name)
32+
// sandbox, err = client.Sandboxes().WaitReady(ctx, "default", sandbox.Name)
3333
// if err != nil {
3434
// log.Fatal(err)
3535
// }
3636
//
3737
// # Command Execution (available in a future release)
3838
//
39-
// result, err := client.Exec().Run(ctx, sandbox.Name, []string{"echo", "hello"}, v1.ExecOptions{})
39+
// result, err := client.Exec().Run(ctx, "default", sandbox.Name, []string{"echo", "hello"}, v1.ExecOptions{})
4040
// if err != nil {
4141
// log.Fatal(err)
4242
// }
4343
// fmt.Println(string(result.Stdout)) // "hello\n"
4444
//
4545
// # Error Handling
4646
//
47-
// _, err = client.Sandboxes().Get(ctx, "missing")
47+
// _, err = client.Sandboxes().Get(ctx, "default", "missing")
4848
// if v1.IsNotFound(err) {
4949
// // handle not found
5050
// }
5151
//
5252
// # Watching
5353
//
54-
// watcher, err := client.Sandboxes().Watch(ctx, sandbox.Name)
54+
// watcher, err := client.Sandboxes().Watch(ctx, "default", sandbox.Name)
5555
// if err != nil {
5656
// log.Fatal(err)
5757
// }
@@ -65,7 +65,7 @@
6565
// Use StopOnTerminal to auto-close the watcher when the sandbox reaches a
6666
// terminal phase (Ready or Error):
6767
//
68-
// watcher, err := client.Sandboxes().Watch(ctx, sandbox.Name,
68+
// watcher, err := client.Sandboxes().Watch(ctx, "default", sandbox.Name,
6969
// v1.WatchOptions{StopOnTerminal: true},
7070
// )
7171
// if err != nil {
@@ -80,13 +80,13 @@
8080
//
8181
// Expose an HTTP service running inside a sandbox and retrieve its public URL:
8282
//
83-
// endpoint, err := client.Services().Expose(ctx, "my-sandbox", "api", 8080, true)
83+
// endpoint, err := client.Services().Expose(ctx, "default", "my-sandbox", "api", 8080, true)
8484
// if err != nil {
8585
// log.Fatal(err)
8686
// }
8787
// fmt.Printf("Service URL: %s\n", endpoint.URL)
8888
//
89-
// endpoints, err := client.Services().List(ctx, "my-sandbox")
89+
// endpoints, err := client.Services().List(ctx, "default", "my-sandbox")
9090
// if err != nil {
9191
// log.Fatal(err)
9292
// }
@@ -98,15 +98,15 @@
9898
//
9999
// List available provider profiles and import new ones:
100100
//
101-
// profiles, err := client.Providers().Profiles().List(ctx)
101+
// profiles, err := client.Providers().Profiles().List(ctx, "default")
102102
// if err != nil {
103103
// log.Fatal(err)
104104
// }
105105
// for _, p := range profiles {
106106
// fmt.Printf("%s (%s): %s\n", p.DisplayName, p.Category, p.Description)
107107
// }
108108
//
109-
// result, err := client.Providers().Profiles().Import(ctx, []v1.ProfileImportItem{
109+
// result, err := client.Providers().Profiles().Import(ctx, "default", []v1.ProfileImportItem{
110110
// {Source: "openai-profile.yaml", Profile: v1.ProviderProfile{
111111
// DisplayName: "OpenAI",
112112
// Category: v1.ProfileCategoryInference,
@@ -123,7 +123,7 @@
123123
//
124124
// Configure gateway-owned credential refresh for a provider:
125125
//
126-
// status, err := client.Providers().Refresh().Configure(ctx, &v1.RefreshConfig{
126+
// status, err := client.Providers().Refresh().Configure(ctx, "default", &v1.RefreshConfig{
127127
// Provider: "openai",
128128
// CredentialKey: "api-key",
129129
// Strategy: v1.RefreshStrategyOAuth2ClientCredentials,
@@ -198,7 +198,7 @@
198198
// Note: CreateSession accepts a sandbox ID, not a name. For name-based access
199199
// with automatic session cleanup, prefer SSH().Tunnel() instead.
200200
//
201-
// session, err := client.SSH().CreateSession(ctx, sandbox.ID)
201+
// session, err := client.SSH().CreateSession(ctx, "default", sandbox.ID)
202202
// if err != nil {
203203
// log.Fatal(err)
204204
// }
@@ -207,7 +207,7 @@
207207
// fmt.Printf("Host key: %s\n", session.HostKeyFingerprint)
208208
// // Use session.Token to authenticate the SSH connection.
209209
//
210-
// revoked, err := client.SSH().RevokeSession(ctx, session.Token)
210+
// revoked, err := client.SSH().RevokeSession(ctx, "default", session.Token)
211211
// if err != nil {
212212
// log.Fatal(err)
213213
// }
@@ -217,7 +217,7 @@
217217
//
218218
// Forward a local connection to a port inside a sandbox:
219219
//
220-
// conn, err := client.TCP().Forward(ctx, "my-sandbox", 5432)
220+
// conn, err := client.TCP().Forward(ctx, "default", "my-sandbox", 5432)
221221
// if err != nil {
222222
// log.Fatal(err)
223223
// }
@@ -238,7 +238,7 @@
238238
// Use WithForwardServiceID to tag the forwarding session with a service
239239
// identifier for audit logging:
240240
//
241-
// conn, err := client.TCP().Forward(ctx, "my-sandbox", 5432,
241+
// conn, err := client.TCP().Forward(ctx, "default", "my-sandbox", 5432,
242242
// v1.WithForwardServiceID("billing-db"),
243243
// )
244244
//
@@ -248,7 +248,7 @@
248248
// session creation, TCP forwarding with an SSH relay target, and automatic
249249
// session cleanup into one operation:
250250
//
251-
// tunnel, err := client.SSH().Tunnel(ctx, "my-sandbox", 22)
251+
// tunnel, err := client.SSH().Tunnel(ctx, "default", "my-sandbox", 22)
252252
// if err != nil {
253253
// log.Fatal(err)
254254
// }
@@ -269,15 +269,15 @@
269269
//
270270
// Use WithTunnelServiceID to associate a service identifier with the tunnel:
271271
//
272-
// tunnel, err := client.SSH().Tunnel(ctx, "my-sandbox", 22,
272+
// tunnel, err := client.SSH().Tunnel(ctx, "default", "my-sandbox", 22,
273273
// v1.WithTunnelServiceID("dev-ssh"),
274274
// )
275275
//
276276
// # Sandbox Policy
277277
//
278278
// Set an initial security policy when creating a sandbox:
279279
//
280-
// sandbox, err := client.Sandboxes().Create(ctx, "secure-sandbox", &v1.SandboxSpec{
280+
// sandbox, err := client.Sandboxes().Create(ctx, "default", "secure-sandbox", &v1.SandboxSpec{
281281
// Template: &v1.SandboxTemplate{Image: "python:3.12"},
282282
// Policy: &v1.SandboxPolicy{
283283
// Version: 1,
@@ -302,7 +302,7 @@
302302
//
303303
// Replace the full policy at runtime via configuration update (available in a future release):
304304
//
305-
// result, err := client.Config().Update(ctx, &v1.ConfigUpdate{
305+
// result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
306306
// Name: "secure-sandbox",
307307
// Policy: &v1.SandboxPolicy{
308308
// Version: 2,
@@ -314,7 +314,7 @@
314314
//
315315
// Read a policy back from revision history (available in a future release):
316316
//
317-
// revisions, err := client.Policy().List(ctx, "secure-sandbox")
317+
// revisions, err := client.Policy().List(ctx, "default")
318318
// if err != nil {
319319
// log.Fatal(err)
320320
// }
@@ -328,7 +328,7 @@
328328
//
329329
// Read sandbox and gateway configuration, and update settings:
330330
//
331-
// sbCfg, err := client.Config().GetSandbox(ctx, "my-sandbox")
331+
// sbCfg, err := client.Config().GetSandbox(ctx, "default", "my-sandbox")
332332
// if err != nil {
333333
// log.Fatal(err)
334334
// }
@@ -343,7 +343,7 @@
343343
// }
344344
// fmt.Printf("Gateway settings revision: %d\n", gwCfg.SettingsRevision)
345345
//
346-
// result, err := client.Config().Update(ctx, &v1.ConfigUpdate{
346+
// result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
347347
// Name: "my-sandbox",
348348
// SettingKey: "max_tokens",
349349
// SettingValue: &v1.SettingValue{

‎sdk/go/openshell/v1/errors.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
ErrorInternal = types.ErrorInternal
2323
ErrorUnimplemented = types.ErrorUnimplemented
2424
ErrorConflict = types.ErrorConflict
25+
ErrorUnauthenticated = types.ErrorUnauthenticated
2526
)
2627

2728
// StatusError is the typed error returned by all SDK operations.
@@ -54,3 +55,6 @@ func IsUnimplemented(err error) bool { return types.IsUnimplemented(err) }
5455
// IsConflict returns true if the error indicates a conflict, such as
5556
// optimistic concurrency or an invalid state transition.
5657
func IsConflict(err error) bool { return types.IsConflict(err) }
58+
59+
// IsUnauthenticated returns true if the error indicates missing or invalid credentials.
60+
func IsUnauthenticated(err error) bool { return types.IsUnauthenticated(err) }

‎sdk/go/openshell/v1/errors_test.go‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ func TestStatusError_Error(t *testing.T) {
2222
assert.Contains(t, s, "sandbox not found")
2323
}
2424

25-
func TestStatusError_ErrorWithDetails(t *testing.T) {
25+
func TestStatusError_ErrorWithCause(t *testing.T) {
26+
cause := fmt.Errorf("underlying issue")
2627
err := &StatusError{
2728
Code: ErrorInvalidArgument,
2829
Message: "bad name",
29-
Details: map[string]string{"field": "name"},
30+
Cause: cause,
3031
}
3132
s := err.Error()
3233
assert.Contains(t, s, "InvalidArgument")
3334
assert.Contains(t, s, "bad name")
35+
assert.ErrorIs(t, err, cause)
3436
}
3537

3638
func TestIsNotFound(t *testing.T) {

‎sdk/go/openshell/v1/grpc_errors.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ func contextError(err error) error {
1313
}
1414
switch err {
1515
case context.DeadlineExceeded:
16-
return &StatusError{Code: ErrorDeadlineExceeded, Message: err.Error()}
16+
return &StatusError{Code: ErrorDeadlineExceeded, Message: err.Error(), Cause: err}
1717
case context.Canceled:
18-
return &StatusError{Code: ErrorCancelled, Message: err.Error()}
18+
return &StatusError{Code: ErrorCancelled, Message: err.Error(), Cause: err}
1919
default:
20-
return &StatusError{Code: ErrorInternal, Message: err.Error()}
20+
return &StatusError{Code: ErrorInternal, Message: err.Error(), Cause: err}
2121
}
2222
}

‎sdk/go/openshell/v1/internal/converter/errors.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var grpcToSDK = map[codes.Code]types.ErrorCode{
1515
codes.AlreadyExists: types.ErrorAlreadyExists,
1616
codes.Unavailable: types.ErrorUnavailable,
1717
codes.PermissionDenied: types.ErrorPermissionDenied,
18-
codes.Unauthenticated: types.ErrorPermissionDenied,
18+
codes.Unauthenticated: types.ErrorUnauthenticated,
1919
codes.InvalidArgument: types.ErrorInvalidArgument,
2020
codes.DeadlineExceeded: types.ErrorDeadlineExceeded,
2121
codes.Canceled: types.ErrorCancelled,
@@ -49,5 +49,6 @@ func FromGRPCError(err error) error {
4949
return &types.StatusError{
5050
Code: code,
5151
Message: st.Message(),
52+
Cause: err,
5253
}
5354
}

‎sdk/go/openshell/v1/sandbox_client.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (s *sandboxClient) Watch(ctx context.Context, workspace, name string, opts
248248
default:
249249
if recvErr != io.EOF {
250250
select {
251-
case ch <- Event[*Sandbox]{Type: EventError, Object: nil}:
251+
case ch <- Event[*Sandbox]{Type: EventError, Err: recvErr}:
252252
default:
253253
}
254254
}

‎sdk/go/openshell/v1/types/errors.go‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
ErrorInternal
2424
ErrorUnimplemented
2525
ErrorConflict
26+
ErrorUnauthenticated
2627
)
2728

2829
// String returns the human-readable name of the error code.
@@ -48,6 +49,8 @@ func (c ErrorCode) String() string {
4849
return "Unimplemented"
4950
case ErrorConflict:
5051
return "Conflict"
52+
case ErrorUnauthenticated:
53+
return "Unauthenticated"
5154
default:
5255
return fmt.Sprintf("Unknown(%d)", int(c))
5356
}
@@ -57,13 +60,17 @@ func (c ErrorCode) String() string {
5760
type StatusError struct {
5861
Code ErrorCode
5962
Message string
60-
Details map[string]string
63+
Cause error
6164
}
6265

6366
func (e *StatusError) Error() string {
6467
return fmt.Sprintf("%s: %s", e.Code, e.Message)
6568
}
6669

70+
func (e *StatusError) Unwrap() error {
71+
return e.Cause
72+
}
73+
6774
// IsNotFound returns true if the error indicates a resource was not found.
6875
func IsNotFound(err error) bool {
6976
return hasCode(err, ErrorNotFound)
@@ -110,6 +117,11 @@ func IsConflict(err error) bool {
110117
return hasCode(err, ErrorConflict)
111118
}
112119

120+
// IsUnauthenticated returns true if the error indicates missing or invalid credentials.
121+
func IsUnauthenticated(err error) bool {
122+
return hasCode(err, ErrorUnauthenticated)
123+
}
124+
113125
func hasCode(err error, code ErrorCode) bool {
114126
if err == nil {
115127
return false

‎sdk/go/openshell/v1/types/watch.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package types
77
type Event[T any] struct {
88
Type EventType
99
Object T
10+
Err error
1011
}
1112

1213
// WatchInterface delivers a stream of typed events. Modeled after

0 commit comments

Comments
 (0)