Skip to content

Commit 8baf212

Browse files
add wrap errors
1 parent 309427f commit 8baf212

File tree

9 files changed

+280
-102
lines changed

9 files changed

+280
-102
lines changed

pkg/connector/apps.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (o *appResourceType) List(
4343
if opts.PageToken.Token == "" {
4444
adminApp, err := sdkResources.NewAppResource("JumpCloud Administration", resourceTypeApp, adminAppID, nil)
4545
if err != nil {
46-
return nil, nil, err
46+
return nil, nil, fmt.Errorf("failed to create JumpCloud Administration app resource: %w", err)
4747
}
4848
rv = append(rv, adminApp)
4949
}
@@ -57,14 +57,14 @@ func (o *appResourceType) List(
5757

5858
apps, resp, err := client.ApplicationsApi.ApplicationsList(ctx).Skip(skip).Execute()
5959
if err != nil {
60-
return nil, nil, err
60+
return nil, nil, wrapSDKError(err, resp, "failed to list applications")
6161
}
6262
defer resp.Body.Close()
6363

6464
for i := range apps.Results {
6565
ur, err := appResource(&apps.Results[i])
6666
if err != nil {
67-
return nil, nil, err
67+
return nil, nil, fmt.Errorf("failed to construct app resource during application list: %w", err)
6868
}
6969
rv = append(rv, ur)
7070
}
@@ -79,7 +79,7 @@ func (o *appResourceType) List(
7979
func appResource(app *jcapi1.Application) (*v2.Resource, error) {
8080
trait, err := appTrait(app)
8181
if err != nil {
82-
return nil, err
82+
return nil, fmt.Errorf("failed to construct app trait during app resource creation: %w", err)
8383
}
8484

8585
var annos annotations.Annotations
@@ -149,7 +149,7 @@ func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource
149149

150150
users, resp, err := o.ext.UserList().Skip(skip).Execute(ctx)
151151
if err != nil {
152-
return nil, nil, err
152+
return nil, nil, fmt.Errorf("failed to list admin users: %w", err)
153153
}
154154
defer resp.Body.Close()
155155

@@ -163,7 +163,7 @@ func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource
163163
// If the user is a system user, we need to fetch the user by email to get the ID
164164
systemUser, err := fetchUserByEmail(ctx, client, adminUser.GetEmail())
165165
if err != nil && !errors.Is(err, errUserNotFoundForEmail) {
166-
return nil, nil, err
166+
return nil, nil, fmt.Errorf("failed to fetch system user by email during admin grants processing: %w", err)
167167
}
168168
if systemUser != nil {
169169
adminPrincipal = systemUser
@@ -216,7 +216,7 @@ func (o *appResourceType) Grants(
216216
} else {
217217
err := b.Unmarshal(opts.PageToken.Token)
218218
if err != nil {
219-
return nil, nil, err
219+
return nil, nil, fmt.Errorf("failed to unmarshal pagination bag during app grants: %w", err)
220220
}
221221
}
222222

@@ -229,7 +229,7 @@ func (o *appResourceType) Grants(
229229
if current.Token != "" {
230230
skip64, err := strconv.ParseInt(current.Token, 10, 32)
231231
if err != nil {
232-
return nil, nil, err
232+
return nil, nil, fmt.Errorf("failed to parse skip value from pagination token during app grants: %w", err)
233233
}
234234
skip = int32(skip64)
235235
}
@@ -247,12 +247,12 @@ func (o *appResourceType) Grants(
247247
}
248248

249249
if req == nil {
250-
return nil, nil, errors.New("unexpected state while listing application grants")
250+
return nil, nil, fmt.Errorf("unexpected pagination state while listing application grants: resourceID=%s, resourceTypeID=%s", current.ResourceID, current.ResourceTypeID)
251251
}
252252

253253
assignments, resp, err := req.Execute()
254254
if err != nil {
255-
return nil, nil, err
255+
return nil, nil, wrapSDKError(err, resp, "failed to list application associations")
256256
}
257257
defer resp.Body.Close()
258258

@@ -263,7 +263,7 @@ func (o *appResourceType) Grants(
263263
// pops if nextToken is empty, going to the next phase
264264
err = b.Next(npt)
265265
if err != nil {
266-
return nil, nil, err
266+
return nil, nil, fmt.Errorf("failed to advance to next page during app grants pagination: %w", err)
267267
}
268268

269269
for i := range assignments {
@@ -284,7 +284,7 @@ func (o *appResourceType) Grants(
284284

285285
pt, err := b.Marshal()
286286
if err != nil {
287-
return nil, nil, err
287+
return nil, nil, fmt.Errorf("failed to marshal page token after listing app grants: %w", err)
288288
}
289289

290290
return rv, &sdkResources.SyncOpResults{NextPageToken: pt}, nil

pkg/connector/connector.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/conductorone/baton-sdk/pkg/uhttp"
1717
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
1818
"go.uber.org/zap"
19+
"google.golang.org/grpc/codes"
1920
)
2021

2122
type Connector struct {
@@ -52,8 +53,14 @@ func WithAPIKey(ctx context.Context, apiKey string, orgId string) Option {
5253

5354
c._client1 = jcapi1.NewAPIClient(cc1)
5455
c._client2 = jcapi2.NewAPIClient(cc2)
56+
57+
baseHttpClient, err := uhttp.NewBaseHttpClientWithContext(ctx, httpClient)
58+
if err != nil {
59+
return err
60+
}
61+
5562
c.ext = &ExtensionClient{
56-
client: httpClient,
63+
client: baseHttpClient,
5764
apiKey: apiKey,
5865
orgId: orgId,
5966
}
@@ -90,12 +97,16 @@ func New(ctx context.Context, opts ...Option) (*Connector, error) {
9097
c := &Connector{}
9198
for _, opt := range opts {
9299
if err := opt(c); err != nil {
93-
return nil, fmt.Errorf("failed to apply option: %w", err)
100+
return nil, fmt.Errorf("failed to apply connector option during initialization: %w", err)
94101
}
95102
}
96103

97104
if c._client1 == nil || c._client2 == nil {
98-
return nil, fmt.Errorf("no client configuration provided")
105+
return nil, uhttp.WrapErrors(
106+
codes.InvalidArgument,
107+
"connector initialization failed: API client not configured",
108+
fmt.Errorf("API clients not properly initialized during connector setup"),
109+
)
99110
}
100111

101112
return c, nil
@@ -147,13 +158,18 @@ func (c *Connector) Validate(ctx context.Context) (annotations.Annotations, erro
147158
_, resp, err := client.DirectoriesApi.DirectoriesList(ctx).Limit(1).Execute()
148159
if err != nil {
149160
l.Error("DirectoriesList for Validate Failed", zap.Error(err))
150-
return nil, fmt.Errorf("jumpcloud-connector: failed to verify api key: %w", err)
161+
return nil, wrapSDKError(err, resp, "failed to verify api key")
151162
}
152163
defer resp.Body.Close()
153164

154165
if resp.StatusCode != http.StatusOK {
155-
err := fmt.Errorf("jumpcloud-connector verify returned non-200: '%d'", resp.StatusCode)
156-
l.Error("Invalid Status Code from Verify", zap.Error(err))
166+
code := httpStatusToGRPCCode(resp.StatusCode)
167+
err := uhttp.WrapErrors(
168+
code,
169+
fmt.Sprintf("validate: unexpected status code %d", resp.StatusCode),
170+
nil,
171+
)
172+
l.Error("jumpcloud-connector: Invalid Status Code from Validate", zap.Error(err))
157173
return nil, err
158174
}
159175

pkg/connector/errors.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package connector
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"net/url"
9+
10+
"github.com/conductorone/baton-sdk/pkg/uhttp"
11+
"google.golang.org/grpc/codes"
12+
)
13+
14+
// httpStatusToGRPCCode maps HTTP status codes to gRPC codes for error classification.
15+
// This helps the Baton SDK determine if errors are retryable and how to handle them.
16+
func httpStatusToGRPCCode(statusCode int) codes.Code {
17+
switch statusCode {
18+
case http.StatusBadRequest:
19+
return codes.InvalidArgument
20+
case http.StatusUnauthorized:
21+
return codes.Unauthenticated
22+
case http.StatusForbidden:
23+
return codes.PermissionDenied
24+
case http.StatusNotFound:
25+
return codes.NotFound
26+
case http.StatusConflict:
27+
return codes.AlreadyExists
28+
case http.StatusTooManyRequests:
29+
return codes.ResourceExhausted
30+
case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable:
31+
return codes.Unavailable
32+
case http.StatusGatewayTimeout:
33+
return codes.DeadlineExceeded
34+
default:
35+
if statusCode >= 500 {
36+
return codes.Unavailable
37+
}
38+
if statusCode >= 400 {
39+
return codes.InvalidArgument
40+
}
41+
return codes.Unknown
42+
}
43+
}
44+
45+
// wrapSDKError wraps errors from the JumpCloud SDK (jcapi1/jcapi2) with appropriate gRPC codes.
46+
// This allows the Baton SDK to properly classify errors for retry logic and alerting.
47+
func wrapSDKError(err error, resp *http.Response, operation string) error {
48+
if err == nil {
49+
return nil
50+
}
51+
52+
// If we don't have a response, inspect the error to classify it correctly
53+
if resp == nil {
54+
// Check for context errors (timeout, cancellation)
55+
if errors.Is(err, context.DeadlineExceeded) {
56+
return uhttp.WrapErrors(
57+
codes.DeadlineExceeded,
58+
fmt.Sprintf("%s: request timeout", operation),
59+
err,
60+
)
61+
}
62+
if errors.Is(err, context.Canceled) {
63+
return uhttp.WrapErrors(
64+
codes.Canceled,
65+
fmt.Sprintf("%s: request canceled", operation),
66+
err,
67+
)
68+
}
69+
70+
// Check for URL errors (configuration issues, network errors, timeouts)
71+
var urlErr *url.Error
72+
if errors.As(err, &urlErr) {
73+
// Timeout on URL operation
74+
if urlErr.Timeout() {
75+
return uhttp.WrapErrors(
76+
codes.DeadlineExceeded,
77+
fmt.Sprintf("%s: request timeout", operation),
78+
urlErr,
79+
)
80+
}
81+
82+
// Temporary network errors (DNS, connection issues)
83+
if urlErr.Temporary() {
84+
return uhttp.WrapErrors(
85+
codes.Unavailable,
86+
fmt.Sprintf("%s: temporary network error", operation),
87+
urlErr,
88+
)
89+
}
90+
91+
// URL parsing errors indicate configuration issues
92+
if urlErr.Op == "parse" {
93+
return uhttp.WrapErrors(
94+
codes.InvalidArgument,
95+
fmt.Sprintf("%s: invalid URL configuration", operation),
96+
urlErr,
97+
)
98+
}
99+
}
100+
101+
// Default: assume it's a network/connection error (service unavailable)
102+
return uhttp.WrapErrors(
103+
codes.Unavailable,
104+
fmt.Sprintf("%s: network or connection error", operation),
105+
err,
106+
)
107+
}
108+
109+
// Map HTTP status codes to gRPC codes
110+
code := httpStatusToGRPCCode(resp.StatusCode)
111+
var message string
112+
113+
switch code {
114+
case codes.InvalidArgument:
115+
if resp.StatusCode == http.StatusBadRequest {
116+
message = fmt.Sprintf("%s: invalid request", operation)
117+
} else {
118+
message = fmt.Sprintf("%s: client error (status %d)", operation, resp.StatusCode)
119+
}
120+
case codes.Unauthenticated:
121+
message = fmt.Sprintf("%s: authentication failed", operation)
122+
case codes.PermissionDenied:
123+
message = fmt.Sprintf("%s: permission denied", operation)
124+
case codes.NotFound:
125+
message = fmt.Sprintf("%s: resource not found", operation)
126+
case codes.AlreadyExists:
127+
message = fmt.Sprintf("%s: resource already exists", operation)
128+
case codes.ResourceExhausted:
129+
message = fmt.Sprintf("%s: rate limit exceeded", operation)
130+
case codes.Unavailable:
131+
if resp.StatusCode >= 500 && resp.StatusCode < 600 {
132+
message = fmt.Sprintf("%s: service unavailable", operation)
133+
} else {
134+
message = fmt.Sprintf("%s: server error (status %d)", operation, resp.StatusCode)
135+
}
136+
case codes.DeadlineExceeded:
137+
message = fmt.Sprintf("%s: request timeout", operation)
138+
default:
139+
message = fmt.Sprintf("%s: unexpected status code %d", operation, resp.StatusCode)
140+
}
141+
142+
return uhttp.WrapErrors(code, message, err)
143+
}

pkg/connector/ext.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package connector
22

3-
import "net/http"
3+
import "github.com/conductorone/baton-sdk/pkg/uhttp"
44

55
type ExtensionClient struct {
6-
client *http.Client
6+
client *uhttp.BaseHttpClient
77
apiKey string
88
orgId string
99
}

0 commit comments

Comments
 (0)