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
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,11 +13,19 @@
#
# Org-wide rollout beyond backend + cli is an open decision — flagged
# to @saadqbal on the PR that added this file.
#
# target-branch is "develop", NOT the default (main): all work in this
# repo flows feature → develop → main (promote PRs). Dependabot defaults
# to the repo default branch (main); left unset, bumps landed straight on
# main, bypassing develop and regressing on the next promote (the develop
# side has the older deps). Point them at develop so they ride the normal
# review + promote path.

version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"
labels:
Expand All@@ -34,6 +42,7 @@ updates:

- package-ecosystem: "github-actions"
directory: "/"
target-branch: "develop"
schedule:
interval: "monthly"
labels:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/add-to-kanban.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ jobs:
add-to-project:
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v1.0.2
- uses: actions/add-to-project@v2.0.0
with:
project-url: https://github.com/orgs/tracebloc/projects/2
github-token: ${{ secrets.PROJECTS_KANBAN_TOKEN }}
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ jobs:
"$BIN" dataset push --help >/dev/null

- name: Upload binary as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: tracebloc-${{ matrix.os }}-${{ matrix.arch }}
path: dist/tracebloc-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.ext }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,7 +58,7 @@ jobs:
# kubeconfig resolution. kind ships a default StorageClass
# (local-path), so the test's PVC binds.
- name: Create kind cluster
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.14.0

# Run the unit suite + the integration suite each under coverage, then merge
# them (go tool covdata). The merge credits the real-cluster I/O seams —
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mutation.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -110,7 +110,7 @@ jobs:

- name: Upload machine-readable report
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: gremlins-report
path: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,7 +153,7 @@ jobs:
cat "$BIN_NAME.sha256"

- name: Upload per-matrix artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: dist-${{ matrix.os }}-${{ matrix.arch }}
path: dist/
Expand All@@ -174,7 +174,7 @@ jobs:
ref: ${{ inputs.ref || github.ref }}

- name: Download all matrix artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: dist/
merge-multiple: true
Expand DownExpand Up@@ -205,7 +205,7 @@ jobs:
echo "tag=$REF" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.tag.outputs.tag }}
name: ${{ steps.tag.outputs.tag }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stale-backlog.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
- uses: actions/stale@v10
with:
days-before-issue-stale: 42 # 6 weeks of no activity → warning
days-before-issue-close: 14 # +2 weeks of silence → close
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,14 +17,14 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/fatih/color v1.19.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/schollz/progressbar/v3 v3.19.0
github.com/schollz/progressbar/v3 v3.19.1
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.45.0
golang.org/x/text v0.40.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.36.1
k8s.io/apimachinery v0.36.1
k8s.io/client-go v0.36.1
k8s.io/api v0.36.2
k8s.io/apimachinery v0.36.2
k8s.io/client-go v0.36.2
)

require (
Expand All@@ -44,7 +44,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/spdystream v0.5.1 // indirect
Expand All@@ -65,7 +65,7 @@ require (
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/streaming v0.36.1 // indirect
k8s.io/streaming v0.36.2 // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
Expand Down
25 changes: 12 additions & 13 deletions go.sum
Original file line numberDiff line numberDiff line change
Expand Up@@ -64,8 +64,8 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
Expand All@@ -92,8 +92,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/schollz/progressbar/v3 v3.19.1 h1:iv8BgwOvdML/S3p84uBpy/IMigv4U9594vPZYa2EdrU=
github.com/schollz/progressbar/v3 v3.19.1/go.mod h1:LFL7jqimKxfhero4K1eCkUr/6R39AgQeiPCJtlTWIW8=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
Expand DownExpand Up@@ -135,7 +135,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand DownExpand Up@@ -166,18 +165,18 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4=
k8s.io/streaming v0.36.1/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
k8s.io/streaming v0.36.2 h1:NSKthPPg9UFSKsRauVJUVGH2Dvn8fhKmY4qrMkw/p98=
k8s.io/streaming v0.36.2/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
Expand Down
27 changes: 27 additions & 0 deletions internal/api/client.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -505,6 +505,33 @@ func (c *Client) RevokeClient(ctx context.Context, id int) error {
// against a misbehaving `next` chain, set well above any real account.
const maxListPages = 100

// GetClient fetches a single client by its dashboard id (GET /edge-device/{id}/).
// The detail route is the same one PatchClientClusterID/RevokeClient address, and
// returns one ProvisionedClient. This is the O(1) way to check ONE client's
// status — unlike ListClients, which pages through the whole account (the
// home-screen heartbeat must not do that under its ~1.2s budget, cli#338).
// A 404 returns (nil, nil) so the caller can distinguish "no such client" from
// a transport/backend error.
func (c *Client) GetClient(ctx context.Context, id int) (*ProvisionedClient, error) {
path := fmt.Sprintf("/edge-device/%d/", id)
url := c.BaseURL + path
status, raw, err := c.get(ctx, path)
if err != nil {
return nil, err
}
if status == http.StatusNotFound {
return nil, nil
}
if status < 200 || status >= 300 {
return nil, &APIError{StatusCode: status, Body: string(raw), URL: url}
}
var out ProvisionedClient
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("decoding get-client response: %w", err)
}
return &out, nil
}

// ListClients returns ALL clients in the caller's account (GET /edge-device/).
// The endpoint is DRF-paginated, so this follows `next` to the end — list,
// `use <id>`, and create-time collision detection must see every client, not
Expand Down
47 changes: 47 additions & 0 deletions internal/api/client_get_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
package api

import (
"context"
"net/http"
"testing"
)

// TestGetClient covers the single-client detail fetch (GET /edge-device/{id}/)
// that backs the home-screen heartbeat (cli#338): a 2xx decodes one client, a
// 404 is (nil, nil) — "no such client", not an error — and any other non-2xx is
// an APIError. It also pins the path so the heartbeat can't regress to a list.
func TestGetClient(t *testing.T) {
t.Run("2xx decodes a single client + hits the detail path", func(t *testing.T) {
c := stubClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/edge-device/1070/" {
t.Errorf("path = %q, want /edge-device/1070/ (detail route, not the list)", r.URL.Path)
}
_, _ = w.Write([]byte(`{"id":1070,"first_name":"asad-macbook","status":1,"namespace":"asad-macbook-3"}`))
})
pc, err := c.GetClient(context.Background(), 1070)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if pc == nil || pc.ID != 1070 || pc.Status != 1 {
t.Fatalf("GetClient = %+v, want id=1070 status=1", pc)
}
})

t.Run("404 -> (nil, nil), not an error", func(t *testing.T) {
c := stubClient(t, func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })
pc, err := c.GetClient(context.Background(), 999)
if pc != nil || err != nil {
t.Fatalf("404 must be (nil, nil); got (%+v, %v)", pc, err)
}
})

t.Run("non-2xx (500) -> APIError", func(t *testing.T) {
c := stubClient(t, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"detail":"boom"}`))
})
if _, err := c.GetClient(context.Background(), 1); err == nil {
t.Error("a 500 must return an error")
}
})
}
39 changes: 20 additions & 19 deletions internal/cli/client.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -864,30 +864,31 @@ func runClientStatus(ctx context.Context, p *ui.Printer, wait bool, timeout time
}
}

// lookupClientStatus finds the account client whose numeric id matches active and
// returns its backend status code. found=false means no such client (deleted, or
// signed into the wrong account). A list error is returned verbatim so --wait can
// treat it as transient and retry.
// lookupClientStatus fetches the active client directly and returns its backend
// status code. found=false means no such client (deleted, or signed into the
// wrong account). A lookup error is returned verbatim so --wait can treat it as
// transient and retry. Fetches the single client by id (GET /edge-device/{id}/)
// rather than listing the whole account — the home-screen heartbeat runs this
// under a ~1.2s budget, and paging every client blew it (cli#338).
func lookupClientStatus(ctx context.Context, client *api.Client, active string) (status int, found bool, err error) {
clients, err := client.ListClients(ctx)
id, err := strconv.Atoi(active)
if err != nil {
return 0, false, err
// A non-numeric active id can never match a backend client, so report it
// as not-found — exactly what the old ListClients+match path did — rather
// than a permanent error. A --wait loop fail-fasts on a missing client but
// treats errors as transient, so returning an error here would make it
// poll a permanent parse failure to the timeout (Bugbot: poll/retry loops
// must fail-fast on non-transient errors).
return 0, false, nil
}
if c := findClientByID(clients, active); c != nil {
return c.Status, true, nil
c, err := client.GetClient(ctx, id)
if err != nil {
return 0, false, err
}
return 0, false, nil
}

// findClientByID returns the account client whose numeric dashboard id equals id
// (the string form stored as the active-client pointer), or nil if none match.
func findClientByID(clients []api.ProvisionedClient, id string) *api.ProvisionedClient {
for i := range clients {
if strconv.Itoa(clients[i].ID) == id {
return &clients[i]
}
if c == nil {
return 0, false, nil // 404 — no such client
}
return nil
return c.Status, true, nil
}

// EdgeDevice.status codes mirrored from the backend (metaApi User.py).
Expand Down
Loading
Loading