Skip to content

Commit cf4991d

Browse files
committed
add backend endpoints
1 parent dfd8563 commit cf4991d

7 files changed

Lines changed: 437 additions & 2 deletions

File tree

cmd/list.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func listTests(cmd *cobra.Command, args []string) error {
7272
return err
7373
}
7474

75-
all, err := api.FetchAllTraceTests(context.Background(), client, authOptions, cfg.Service.ID, nil)
75+
all, err := api.FetchAllTraceTestsWithCache(context.Background(), client, authOptions, cfg.Service.ID)
7676
if err != nil {
7777
return err
7878
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.25.0
44

55
require (
66
github.com/Use-Tusk/fence v0.1.16
7-
github.com/Use-Tusk/tusk-drift-schemas v0.1.24
7+
github.com/Use-Tusk/tusk-drift-schemas v0.1.25-0.20260127032119-ba5c9c37743f
88
github.com/agnivade/levenshtein v1.0.3
99
github.com/atotto/clipboard v0.1.4
1010
github.com/aymanbagabas/go-osc52/v2 v2.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/Use-Tusk/fence v0.1.16 h1:iTHn7ttCbvUANZ9Bo/3HyZYAw7ZrNAjoZjaX7kDbE0I
44
github.com/Use-Tusk/fence v0.1.16/go.mod h1:topOkP7EULI+78u+Ax775d36GqWfx2BhqkVEQ/QXXAU=
55
github.com/Use-Tusk/tusk-drift-schemas v0.1.24 h1:q5rdVm46qk+6s7THk1QeSYDJc+/nL63ChyEYF/VxaTM=
66
github.com/Use-Tusk/tusk-drift-schemas v0.1.24/go.mod h1:pa3EvTj9kKxl9f904RVFkj9YK1zB75QogboKi70zalM=
7+
github.com/Use-Tusk/tusk-drift-schemas v0.1.25-0.20260127032119-ba5c9c37743f h1:fgrFZDrRqSPfVtBg0FPT5W4KUmXDTL9eO3cG0OnGC0s=
8+
github.com/Use-Tusk/tusk-drift-schemas v0.1.25-0.20260127032119-ba5c9c37743f/go.mod h1:pa3EvTj9kKxl9f904RVFkj9YK1zB75QogboKi70zalM=
79
github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
810
github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
911
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=

internal/api/client.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,35 @@ func (c *TuskClient) GetValidationTraceTests(ctx context.Context, in *backend.Ge
391391
}
392392
return nil, fmt.Errorf("invalid response")
393393
}
394+
395+
// GetAllTraceTestIds fetches all trace test IDs for a service (lightweight, no pagination).
396+
func (c *TuskClient) GetAllTraceTestIds(ctx context.Context, in *backend.GetAllTraceTestIdsRequest, auth AuthOptions) (*backend.GetAllTraceTestIdsResponseSuccess, error) {
397+
var out backend.GetAllTraceTestIdsResponse
398+
if err := c.makeTestRunServiceRequest(ctx, "get_all_trace_test_ids", in, &out, auth, DefaultRetryConfig(3)); err != nil {
399+
return nil, err
400+
}
401+
402+
if s := out.GetSuccess(); s != nil {
403+
return s, nil
404+
}
405+
if e := out.GetError(); e != nil {
406+
return nil, fmt.Errorf("%s: %s", e.Code, e.Message)
407+
}
408+
return nil, fmt.Errorf("invalid response")
409+
}
410+
411+
// GetTraceTestsByIds fetches trace tests by their IDs (batch fetch).
412+
func (c *TuskClient) GetTraceTestsByIds(ctx context.Context, in *backend.GetTraceTestsByIdsRequest, auth AuthOptions) (*backend.GetTraceTestsByIdsResponseSuccess, error) {
413+
var out backend.GetTraceTestsByIdsResponse
414+
if err := c.makeTestRunServiceRequest(ctx, "get_trace_tests_by_ids", in, &out, auth, DefaultRetryConfig(3)); err != nil {
415+
return nil, err
416+
}
417+
418+
if s := out.GetSuccess(); s != nil {
419+
return s, nil
420+
}
421+
if e := out.GetError(); e != nil {
422+
return nil, fmt.Errorf("%s: %s", e.Code, e.Message)
423+
}
424+
return nil, fmt.Errorf("invalid response")
425+
}

internal/api/fetch_tests.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/Use-Tusk/tusk-drift-cli/internal/cache"
78
"github.com/Use-Tusk/tusk-drift-cli/internal/utils"
89
backend "github.com/Use-Tusk/tusk-drift-schemas/generated/go/backend"
910
)
@@ -138,3 +139,81 @@ func FetchDriftRunTraceTests(
138139
tracker.Finish("")
139140
return all, nil
140141
}
142+
143+
// FetchAllTraceTestsWithCache fetches trace tests using ID-based cache diffing.
144+
// It only fetches new traces and removes deleted ones from cache.
145+
// On network error, it falls back to cached data if available.
146+
func FetchAllTraceTestsWithCache(
147+
ctx context.Context,
148+
client *TuskClient,
149+
auth AuthOptions,
150+
serviceID string,
151+
) ([]*backend.TraceTest, error) {
152+
traceCache, err := cache.NewTraceCache(serviceID)
153+
if err != nil {
154+
// Cache init failed, fall back to full fetch
155+
return FetchAllTraceTests(ctx, client, auth, serviceID, nil)
156+
}
157+
158+
// 1. Fetch all IDs from API
159+
tracker := utils.NewProgressTracker("Syncing traces from Tusk Drift Cloud", false, false)
160+
idsResp, err := client.GetAllTraceTestIds(ctx, &backend.GetAllTraceTestIdsRequest{
161+
ObservableServiceId: serviceID,
162+
}, auth)
163+
if err != nil {
164+
tracker.Stop()
165+
// Network error: try loading from cache
166+
cached, cacheErr := traceCache.LoadAllTraces()
167+
if cacheErr != nil || len(cached) == 0 {
168+
return nil, fmt.Errorf("failed to fetch trace test IDs and no cache available: %w", err)
169+
}
170+
fmt.Printf("Warning: Using cached data due to network error: %v\n", err)
171+
return cached, nil
172+
}
173+
remoteIds := idsResp.TraceTestIds
174+
175+
// 2. Get cached IDs
176+
cachedIds, err := traceCache.GetCachedIds()
177+
if err != nil {
178+
tracker.Stop()
179+
// Cache read failed, fall back to full fetch
180+
return FetchAllTraceTests(ctx, client, auth, serviceID, nil)
181+
}
182+
183+
// 3. Compute diff
184+
toFetch, toDelete := cache.DiffIds(remoteIds, cachedIds)
185+
186+
// 4. Delete removed traces
187+
if len(toDelete) > 0 {
188+
if err := traceCache.DeleteTraces(toDelete); err != nil {
189+
// Non-fatal, continue
190+
fmt.Printf("Warning: failed to delete some cached traces: %v\n", err)
191+
}
192+
}
193+
194+
// 5. Batch fetch new traces and save
195+
if len(toFetch) > 0 {
196+
tracker.SetTotal(len(toFetch))
197+
tracker.Update(0)
198+
199+
newTraces, err := client.GetTraceTestsByIds(ctx, &backend.GetTraceTestsByIdsRequest{
200+
ObservableServiceId: serviceID,
201+
TraceTestIds: toFetch,
202+
}, auth)
203+
if err != nil {
204+
tracker.Stop()
205+
return nil, fmt.Errorf("failed to fetch new trace tests: %w", err)
206+
}
207+
208+
if err := traceCache.SaveTraces(newTraces.TraceTests); err != nil {
209+
// Non-fatal, traces are still usable
210+
fmt.Printf("Warning: failed to save some traces to cache: %v\n", err)
211+
}
212+
tracker.Update(len(toFetch))
213+
}
214+
215+
tracker.Finish("")
216+
217+
// 6. Load all from cache for display
218+
return traceCache.LoadAllTraces()
219+
}

internal/cache/cache.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package cache
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
backend "github.com/Use-Tusk/tusk-drift-schemas/generated/go/backend"
10+
"google.golang.org/protobuf/proto"
11+
)
12+
13+
// TraceCache manages local caching of cloud trace tests.
14+
// Cache files are stored as protobuf-serialized .bin files named by trace test ID.
15+
type TraceCache struct {
16+
cacheDir string
17+
}
18+
19+
// NewTraceCache creates a new TraceCache for the given service ID.
20+
// The cache directory is: <os.UserCacheDir()>/tusk/<serviceID>/
21+
func NewTraceCache(serviceID string) (*TraceCache, error) {
22+
userCacheDir, err := os.UserCacheDir()
23+
if err != nil {
24+
return nil, fmt.Errorf("failed to get user cache directory: %w", err)
25+
}
26+
27+
cacheDir := filepath.Join(userCacheDir, "tusk", serviceID)
28+
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
29+
return nil, fmt.Errorf("failed to create cache directory: %w", err)
30+
}
31+
32+
return &TraceCache{cacheDir: cacheDir}, nil
33+
}
34+
35+
// GetCachedIds returns the IDs of all cached trace tests by listing .bin files.
36+
func (c *TraceCache) GetCachedIds() ([]string, error) {
37+
entries, err := os.ReadDir(c.cacheDir)
38+
if err != nil {
39+
if os.IsNotExist(err) {
40+
return nil, nil
41+
}
42+
return nil, fmt.Errorf("failed to read cache directory: %w", err)
43+
}
44+
45+
var ids []string
46+
for _, entry := range entries {
47+
if entry.IsDir() {
48+
continue
49+
}
50+
name := entry.Name()
51+
if strings.HasSuffix(name, ".bin") {
52+
ids = append(ids, strings.TrimSuffix(name, ".bin"))
53+
}
54+
}
55+
return ids, nil
56+
}
57+
58+
// LoadTrace loads a single trace test from cache by ID.
59+
func (c *TraceCache) LoadTrace(id string) (*backend.TraceTest, error) {
60+
filePath := filepath.Join(c.cacheDir, id+".bin")
61+
data, err := os.ReadFile(filePath)
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to read cache file %s: %w", filePath, err)
64+
}
65+
66+
var trace backend.TraceTest
67+
if err := proto.Unmarshal(data, &trace); err != nil {
68+
return nil, fmt.Errorf("failed to unmarshal trace %s: %w", id, err)
69+
}
70+
return &trace, nil
71+
}
72+
73+
// LoadAllTraces loads all cached trace tests.
74+
func (c *TraceCache) LoadAllTraces() ([]*backend.TraceTest, error) {
75+
ids, err := c.GetCachedIds()
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
traces := make([]*backend.TraceTest, 0, len(ids))
81+
for _, id := range ids {
82+
trace, err := c.LoadTrace(id)
83+
if err != nil {
84+
// Skip corrupted cache files
85+
continue
86+
}
87+
traces = append(traces, trace)
88+
}
89+
return traces, nil
90+
}
91+
92+
// SaveTraces saves multiple trace tests to cache.
93+
func (c *TraceCache) SaveTraces(traces []*backend.TraceTest) error {
94+
for _, trace := range traces {
95+
if err := c.saveTrace(trace); err != nil {
96+
return err
97+
}
98+
}
99+
return nil
100+
}
101+
102+
// saveTrace saves a single trace test to cache.
103+
func (c *TraceCache) saveTrace(trace *backend.TraceTest) error {
104+
data, err := proto.Marshal(trace)
105+
if err != nil {
106+
return fmt.Errorf("failed to marshal trace %s: %w", trace.Id, err)
107+
}
108+
109+
filePath := filepath.Join(c.cacheDir, trace.Id+".bin")
110+
if err := os.WriteFile(filePath, data, 0o644); err != nil {
111+
return fmt.Errorf("failed to write cache file %s: %w", filePath, err)
112+
}
113+
return nil
114+
}
115+
116+
// DeleteTraces removes the specified trace test IDs from cache.
117+
func (c *TraceCache) DeleteTraces(ids []string) error {
118+
for _, id := range ids {
119+
filePath := filepath.Join(c.cacheDir, id+".bin")
120+
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
121+
return fmt.Errorf("failed to delete cache file %s: %w", filePath, err)
122+
}
123+
}
124+
return nil
125+
}
126+
127+
// DiffIds computes which IDs need to be fetched and which need to be deleted.
128+
// Returns (toFetch, toDelete).
129+
func DiffIds(remoteIds, cachedIds []string) (toFetch, toDelete []string) {
130+
remoteSet := make(map[string]struct{}, len(remoteIds))
131+
for _, id := range remoteIds {
132+
remoteSet[id] = struct{}{}
133+
}
134+
135+
cachedSet := make(map[string]struct{}, len(cachedIds))
136+
for _, id := range cachedIds {
137+
cachedSet[id] = struct{}{}
138+
}
139+
140+
// toFetch = remoteIds - cachedIds
141+
for _, id := range remoteIds {
142+
if _, exists := cachedSet[id]; !exists {
143+
toFetch = append(toFetch, id)
144+
}
145+
}
146+
147+
// toDelete = cachedIds - remoteIds
148+
for _, id := range cachedIds {
149+
if _, exists := remoteSet[id]; !exists {
150+
toDelete = append(toDelete, id)
151+
}
152+
}
153+
154+
return toFetch, toDelete
155+
}

0 commit comments

Comments
 (0)