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
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ jobs:
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
GH_PAT: ${{ secrets.GH_PAT }}
GCP_BUCKET: ${{ secrets.GCP_BUCKET }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

# Binary signing
DEVELOPER_ID_APP: ${{ secrets.DEVELOPER_ID_APP }}
Expand Down Expand Up @@ -346,6 +347,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
GH_PAT: ${{ secrets.GH_PAT }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
GCP_BUCKET: ${{ secrets.GCP_BUCKET }}
CERT_THUMBPRINT: ${{ secrets.CERT_THUMBPRINT }}
TIMESTAMP_SERVER: ${{ secrets.TIMESTAMP_SERVER }}
Expand Down Expand Up @@ -463,6 +465,7 @@ jobs:
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
GH_PAT: ${{ secrets.GH_PAT }}
GCP_BUCKET: ${{ secrets.GCP_BUCKET }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}

# Copy files to latest/linux folder
# Copies all uploaded files to stable latest/ paths with simplified naming
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ Thumbs.db
.env.local
.env.*.local

.thunder.json
.thunder.json.env
1 change: 1 addition & 0 deletions .goreleaser.macos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ builds:
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildVersion={{ .Version }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildCommit={{ .Commit }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildDate={{ .Date }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.SentryDSN={{ .Env.SENTRY_DSN }}
# Post-build hook: Signs binaries, creates .pkg installer, signs .pkg, and notarizes
hooks:
post:
Expand Down
1 change: 1 addition & 0 deletions .goreleaser.ubuntu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ builds:
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildVersion={{ .Version }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildCommit={{ .Commit }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildDate={{ .Date }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.SentryDSN={{ .Env.SENTRY_DSN }}

# Linux package configuration (.deb, .rpm, .apk)
# These packages are automatically included in GitHub releases - DO NOT add to release.extra_files
Expand Down
1 change: 1 addition & 0 deletions .goreleaser.windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ builds:
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildVersion={{ .Version }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildCommit={{ .Commit }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.BuildDate={{ .Date }}
- -X github.com/Thunder-Compute/thunder-cli/internal/version.SentryDSN={{ .Env.SENTRY_DSN }}
hooks:
post:
- >-
Expand Down
76 changes: 72 additions & 4 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"net/http"
"sort"
"time"

"github.com/Thunder-Compute/thunder-cli/sentry"
sentrygo "github.com/getsentry/sentry-go"
)

type Client struct {
Expand Down Expand Up @@ -41,6 +44,8 @@ func (c *Client) setHeaders(req *http.Request) {
}

func (c *Client) ValidateToken(ctx context.Context) error {
sentry.AddBreadcrumb("api", "validate_token", nil, sentry.LevelInfo)

req, err := http.NewRequest("GET", c.baseURL+"/v1/auth/validate", nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
Expand All @@ -50,24 +55,48 @@ func (c *Client) ValidateToken(ctx context.Context) error {

resp, err := c.do(ctx, req)
if err != nil {
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ValidateToken").
Set("api_url", c.baseURL),
Level: ptr(sentry.LevelError),
})
return fmt.Errorf("failed to validate token: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == 401 {
return fmt.Errorf("authentication failed: invalid token")
err := fmt.Errorf("authentication failed: invalid token")
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ValidateToken").
Set("status_code", "401"),
Level: ptr(sentry.LevelWarning),
})
return err
}

if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("token validation failed with status %d: %s", resp.StatusCode, string(body))
err := fmt.Errorf("token validation failed with status %d: %s", resp.StatusCode, string(body))
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ValidateToken").
Set("status_code", fmt.Sprintf("%d", resp.StatusCode)),
Extra: sentry.NewExtra().
Set("response_body", string(body)),
Level: ptr(getLogLevelForStatus(resp.StatusCode)),
})
return err
}

_, _ = io.ReadAll(resp.Body)
return nil
}

func (c *Client) ListInstancesWithIPUpdateCtx(ctx context.Context) ([]Instance, error) {
sentry.AddBreadcrumb("api", "list_instances", nil, sentry.LevelInfo)

req, err := http.NewRequest("GET", c.baseURL+"/instances/list?update_ips=true", nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
Expand All @@ -77,17 +106,37 @@ func (c *Client) ListInstancesWithIPUpdateCtx(ctx context.Context) ([]Instance,

resp, err := c.do(ctx, req)
if err != nil {
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ListInstances").
Set("api_url", c.baseURL),
Level: ptr(sentry.LevelError),
})
return nil, fmt.Errorf("failed to make request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode == 401 {
return nil, fmt.Errorf("authentication failed: invalid token")
err := fmt.Errorf("authentication failed: invalid token")
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ListInstances").
Set("status_code", "401"),
Level: ptr(sentry.LevelWarning),
})
return nil, err
}

if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
err := fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
sentry.CaptureError(err, &sentry.EventOptions{
Tags: sentry.NewTags().
Set("api_method", "ListInstances").
Set("status_code", fmt.Sprintf("%d", resp.StatusCode)),
Level: ptr(getLogLevelForStatus(resp.StatusCode)),
})
return nil, err
}

body, err := io.ReadAll(resp.Body)
Expand Down Expand Up @@ -532,3 +581,22 @@ func (c *Client) DeleteSnapshot(snapshotID string) error {

return nil
}

// Helper functions for Sentry integration

// ptr is a helper to create a pointer to a value
func ptr[T any](v T) *T {
return &v
}

// getLogLevelForStatus determines the appropriate Sentry level for HTTP status codes
func getLogLevelForStatus(statusCode int) sentrygo.Level {
switch {
case statusCode >= 500:
return sentrygo.LevelError
case statusCode >= 400:
return sentrygo.LevelWarning
default:
return sentrygo.LevelInfo
}
}
Loading
Loading