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
13 changes: 11 additions & 2 deletions internal/cli/delete.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,7 +182,7 @@ func runDelete(ctx context.Context, p *ui.Printer, pr prompter, o deleteOpts) er
// 2. Uninstall the Helm release (best-effort — the credential is already
// revoked; a leftover release is harmless and re-runnable).
if ns != "" {
if uerr := uninstallChart(ctx, ns); uerr != nil {
if uerr := uninstallChart(ctx, ns, o.kubeconfigPath, o.contextOverride); uerr != nil {
p.Warnf("Chart uninstall reported: %v", uerr)
} else {
p.Successf("Uninstalled the Helm release %s.", ns)
Expand DownExpand Up@@ -213,7 +213,16 @@ func runDelete(ctx context.Context, p *ui.Printer, pr prompter, o deleteOpts) er
// 5. Remove ~/.tracebloc (config + on-host datasets on a single-host install)
// unless --keep-data. This is the one irreversible step on that path.
if o.keepData {
p.Infof("Kept local data and config (~/.tracebloc) — --keep-data.")
// Spare ~/.tracebloc, but still clear the now-dangling active-client pointer:
// the credential is revoked, so leaving this host "enrolled" as the client
// would mislead a later sign-in / reinstall (§7.5). The token and on-host
// data stay; re-running `client create` re-adopts by cluster_id.
prof.ActiveClientID, prof.ActiveClientName, prof.ActiveClientNamespace = "", "", ""
if serr := cfg.Save(); serr != nil {
p.Warnf("Kept local data, but couldn't clear the active-client pointer (%v).", serr)
} else {
p.Infof("Kept local data and config (~/.tracebloc); cleared the active-client pointer — --keep-data.")
}
} else {
if derr := removeHostDataDir(); derr != nil {
p.Warnf("Couldn't remove local data (%v) — remove it by hand: rm -rf %s", derr, hostDataDirDisplay())
Expand Down
41 changes: 40 additions & 1 deletion internal/cli/delete_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,14 +41,19 @@ type fakeNodeboot struct {
removeErr map[string]error // path → error to return from osRemoveAll
executable string
executableErr error
// Kubeconfig/context the uninstall seam was handed — so a test can prove the
// `tracebloc delete` --kubeconfig/--context flags actually reach helm.
uninstallKubeconfig string
uninstallContext string
}

func (f *fakeNodeboot) install(t *testing.T) {
t.Helper()
origU, origT, origP := uninstallChart, teardownCluster, pruneImages
origExe, origRm := osExecutable, osRemoveAll
uninstallChart = func(_ context.Context, ns string) error {
uninstallChart = func(_ context.Context, ns, kubeconfig, kubeContext string) error {
f.calls = append(f.calls, "uninstall:"+ns)
f.uninstallKubeconfig, f.uninstallContext = kubeconfig, kubeContext
return f.uninstallErr
}
teardownCluster = func(_ context.Context, name string) error {
Expand DownExpand Up@@ -222,6 +227,40 @@ func TestDelete_KeepData_SparesDataDir(t *testing.T) {
if !strings.Contains(out.String(), "--keep-data") {
t.Errorf("output should note --keep-data:\n%s", out.String())
}
// …but the now-dangling active-client pointer must be cleared even under
// --keep-data (the credential is revoked; a stale pointer would mislead a
// later sign-in / reinstall).
cfg, _ := config.Load()
if got := cfg.Current().ActiveClientID; got != "" {
t.Errorf("--keep-data should clear the active-client pointer, got %q", got)
}
}

// --kubeconfig/--context must reach the helm uninstall — otherwise the release is
// uninstalled against the ambient current-context, which may be the wrong cluster.
func TestDelete_KubeconfigContext_ReachHelm(t *testing.T) {
withClientBackend(t, func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/edge-device/":
_, _ = w.Write([]byte(`[{"id":5,"first_name":"gpu-box-01","namespace":"gpu-box-01","status":0}]`))
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/revoke"):
w.WriteHeader(http.StatusOK)
}
})
setActiveForDelete(t, "5", "gpu-box-01", "gpu-box-01")
fn := &fakeNodeboot{executable: filepath.Join(t.TempDir(), "tracebloc")}
fn.install(t)

var out bytes.Buffer
err := runDelete(context.Background(), ui.New(&out), nil,
deleteOpts{yes: true, kubeconfigPath: "/tmp/kc.yaml", contextOverride: "k3d-tracebloc"})
if err != nil {
t.Fatalf("offboard: %v", err)
}
if fn.uninstallKubeconfig != "/tmp/kc.yaml" || fn.uninstallContext != "k3d-tracebloc" {
t.Errorf("uninstall got kubeconfig=%q context=%q, want /tmp/kc.yaml + k3d-tracebloc",
fn.uninstallKubeconfig, fn.uninstallContext)
}
}

// (d) A running/online client → refuse unless --force.
Expand Down
16 changes: 14 additions & 2 deletions internal/nodeboot/nodeboot.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -78,8 +78,20 @@ func TeardownCluster(ctx context.Context, name string) error {

// UninstallChart removes the client's Helm release (release name = namespace, the
// installer's convention). A missing release is not an error (idempotent teardown).
func UninstallChart(ctx context.Context, namespace string) error {
_, err := run(ctx, "helm", "uninstall", namespace, "--namespace", namespace)
//
// kubeconfig and kubeContext target the release's cluster: helm otherwise acts on
// the ambient $KUBECONFIG + current-context, so an operator whose current context
// isn't the tracebloc cluster could uninstall the wrong release. Both are appended
// only when non-empty, preserving the default-context behavior.
func UninstallChart(ctx context.Context, namespace, kubeconfig, kubeContext string) error {
args := []string{"uninstall", namespace, "--namespace", namespace}
if kubeconfig != "" {
args = append(args, "--kubeconfig", kubeconfig)
}
if kubeContext != "" {
args = append(args, "--kube-context", kubeContext)
}
_, err := run(ctx, "helm", args...)
// Match helm's release-not-found wording specifically ("... release: not
// found"), not a bare "not found" — an unrelated failure whose output happens
// to contain "not found" (e.g. "Kubernetes cluster unreachable: ... not
Expand Down
21 changes: 17 additions & 4 deletions internal/nodeboot/nodeboot_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,7 +137,7 @@ func TestUninstallChart(t *testing.T) {
f := newFakeRunner()
f.install(t)

if err := UninstallChart(context.Background(), "munich-radiology"); err != nil {
if err := UninstallChart(context.Background(), "munich-radiology", "", ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"helm uninstall munich-radiology --namespace munich-radiology"}
Expand All@@ -151,7 +151,7 @@ func TestUninstallChart(t *testing.T) {
f.on("helm uninstall gone --namespace gone", "Error: uninstall: Release not loaded: gone: release: not found", errors.New("exit 1"))
f.install(t)

if err := UninstallChart(context.Background(), "gone"); err != nil {
if err := UninstallChart(context.Background(), "gone", "", ""); err != nil {
t.Fatalf("a missing release must be swallowed, got: %v", err)
}
})
Expand All@@ -161,7 +161,7 @@ func TestUninstallChart(t *testing.T) {
f.on("helm uninstall ns --namespace ns", "Error: connection refused", errors.New("exit 1"))
f.install(t)

if err := UninstallChart(context.Background(), "ns"); err == nil {
if err := UninstallChart(context.Background(), "ns", "", ""); err == nil {
t.Fatal("want error for a non-not-found helm failure, got nil")
}
})
Expand All@@ -175,10 +175,23 @@ func TestUninstallChart(t *testing.T) {
`Error: Kubernetes cluster unreachable: namespace "kube-system" not found`, errors.New("exit 1"))
f.install(t)

if err := UninstallChart(context.Background(), "ns"); err == nil {
if err := UninstallChart(context.Background(), "ns", "", ""); err == nil {
t.Fatal("a cluster-unreachable 'not found' must surface, got nil")
}
})

t.Run("kubeconfig + context are passed to helm", func(t *testing.T) {
f := newFakeRunner()
f.install(t)

if err := UninstallChart(context.Background(), "ns", "/tmp/kc.yaml", "k3d-tracebloc"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
want := []string{"helm uninstall ns --namespace ns --kubeconfig /tmp/kc.yaml --kube-context k3d-tracebloc"}
if !reflect.DeepEqual(f.calls, want) {
t.Fatalf("calls = %v, want %v", f.calls, want)
}
})
}

func TestPruneImages(t *testing.T) {
Expand Down
Loading