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
6 changes: 6 additions & 0 deletions internal/cli/clustertarget.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,12 @@ var (
newClientsetFn = cluster.NewClientset
)

// resolveClusterTargetFn is a test seam over resolveClusterTarget so a command
// test can inject a fully-resolved target (fake clientset + release + PVC)
// without seeding the k8s objects discoverRelease / DiscoverSharedPVC look for.
// Same fn-var seam pattern as loadClusterFn / listDatasetsFn.
var resolveClusterTargetFn = resolveClusterTarget

// clusterTarget bundles the cluster handles the data commands resolve from a
// kubeconfig before doing any work: the resolved config, a clientset, the
// parent tracebloc release, and — when asked — the shared data PVC.
Expand Down
6 changes: 6 additions & 0 deletions internal/cli/data.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -1437,6 +1437,12 @@ func writePushErrorJSON(w io.Writer, sp push.SpecArgs, e error, code int) {
// listDatasetsFn is a test seam over push.ListDatasets.
var listDatasetsFn = push.ListDatasets

// teardownFn is a test seam over push.Teardown (the destructive DROP TABLE +
// file removal). Production points at the real Teardown; a test overrides it to
// drive the clean and the partial-failure (table dropped, files remain → exit
// 7) paths without a cluster.
var teardownFn = push.Teardown

// Test seams over the cluster-touching steps of runIngestionRun (#1009).
// Production wires them to the real functions; a table test overrides them to
// drive the classify → exit-code → JSON → reclaim matrix without a cluster
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/data_delete.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,7 +126,7 @@ undone — re-ingesting the data is the only way back.`)
// running teardown against a cluster with no tracebloc install.
opts := cluster.KubeconfigOptions{Path: a.Kubeconfig, Context: a.Context, Namespace: a.Namespace}
binding := bindActiveClientNamespace(&opts)
target, err := resolveClusterTarget(ctx, a.Printer, opts, binding, true)
target, err := resolveClusterTargetFn(ctx, a.Printer, opts, binding, true)
if err != nil {
return binding.explain(err)
}
Expand DownExpand Up@@ -198,7 +198,7 @@ undone — re-ingesting the data is the only way back.`)
// files on any volume type — including hostPath, where fsGroup is
// a no-op (tracebloc/client#259).
p.Infof("Removing in-cluster artifacts…")
res, err := push.Teardown(ctx, cs, &push.SPDYExecutor{Config: resolved.RestConfig, Client: cs}, resolved.Namespace, plan, push.PodSpecOptions{
res, err := teardownFn(ctx, cs, &push.SPDYExecutor{Config: resolved.RestConfig, Client: cs}, resolved.Namespace, plan, push.PodSpecOptions{
Namespace: resolved.Namespace,
PVCClaimName: pvc.ClaimName,
PVCMountPath: pvc.MountPath,
Expand Down
99 changes: 99 additions & 0 deletions internal/cli/data_delete_execute_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
package cli

import (
"bytes"
"context"
"errors"
"strings"
"testing"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"

"github.com/tracebloc/cli/internal/cluster"
"github.com/tracebloc/cli/internal/push"
"github.com/tracebloc/cli/internal/ui"
)

// TestRunDataDelete_Execute covers the destructive teardown path — the P0 the
// coverage audit flagged (runDataDelete was 22%; every existing test stopped at
// cluster discovery). It drives the command past discovery via the
// resolveClusterTargetFn seam (a canned target — no k8s fixture needed) and the
// listDatasetsFn seam (target resolution), then exercises the three teardown
// outcomes through the new teardownFn seam:
// - clean success -> exit 0 + a "Deleted" line
// - table dropped but file removal fails -> exit 7 + the recovery hint
// (the idempotent-DROP re-run guidance, backend#1027's sibling)
// - teardown fails before the drop -> exit 7 "teardown failed"
//
// The mixed-case "Churn" case also pins the case-insensitive resolveDeleteTarget
// match (backend#1027: a mis-cased name used to DROP nothing and still exit 0).
func TestRunDataDelete_Execute(t *testing.T) {
origRCT, origList, origTD := resolveClusterTargetFn, listDatasetsFn, teardownFn
t.Cleanup(func() {
resolveClusterTargetFn, listDatasetsFn, teardownFn = origRCT, origList, origTD
})

resolveClusterTargetFn = func(_ context.Context, _ *ui.Printer, _ cluster.KubeconfigOptions, _ activeClientBinding, _ bool) (*clusterTarget, error) {
return &clusterTarget{
Resolved: &cluster.ResolvedConfig{Context: "ctx", Namespace: "tracebloc"},
Clientset: fake.NewSimpleClientset(),
Release: &cluster.ParentRelease{ReleaseName: "tracebloc", IngestorSAName: "tracebloc-ingestor"},
PVC: &cluster.SharedPVC{ClaimName: "client-pvc", MountPath: "/data/shared"},
}, nil
}
listDatasetsFn = func(_ context.Context, _ kubernetes.Interface, _ *rest.Config, _ string) ([]string, error) {
return []string{"churn"}, nil
}

run := func(table string) (string, error) {
var buf bytes.Buffer
err := runDataDelete(context.Background(), runDataDeleteArgs{
Table: table, Yes: true, Printer: ui.New(&buf),
})
return buf.String(), err
}

t.Run("clean teardown -> success", func(t *testing.T) {
teardownFn = func(_ context.Context, _ kubernetes.Interface, _ push.Executor, _ string, _ push.TeardownPlan, _ push.PodSpecOptions) (push.TeardownResult, error) {
return push.TeardownResult{RemovedPaths: []string{"/data/shared/churn"}}, nil
}
out, err := run("churn")
if err != nil {
t.Fatalf("clean teardown: want nil error, got %v", err)
}
if !strings.Contains(out, "Deleted") {
t.Errorf("want a success line, got:\n%s", out)
}
})

t.Run("table dropped but file removal fails -> exit 7 + recovery hint", func(t *testing.T) {
teardownFn = func(_ context.Context, _ kubernetes.Interface, _ push.Executor, _ string, _ push.TeardownPlan, _ push.PodSpecOptions) (push.TeardownResult, error) {
return push.TeardownResult{DroppedTable: true}, errors.New("pod exec failed")
}
// Mixed case also exercises the case-insensitive resolveDeleteTarget match.
_, err := run("Churn")
var ee *exitError
if !errors.As(err, &ee) || ee.Code() != 7 {
t.Fatalf("partial failure: want exit 7, got %v", err)
}
if !strings.Contains(err.Error(), "was dropped, but removing its files failed") {
t.Errorf("want the dropped-but-files-remain recovery message, got: %v", err)
}
})

t.Run("teardown fails before the drop -> exit 7 teardown failed", func(t *testing.T) {
teardownFn = func(_ context.Context, _ kubernetes.Interface, _ push.Executor, _ string, _ push.TeardownPlan, _ push.PodSpecOptions) (push.TeardownResult, error) {
return push.TeardownResult{}, errors.New("could not reach the mysql pod")
}
_, err := run("churn")
var ee *exitError
if !errors.As(err, &ee) || ee.Code() != 7 {
t.Fatalf("pre-drop failure: want exit 7, got %v", err)
}
if !strings.Contains(err.Error(), "teardown failed") {
t.Errorf("want a plain \"teardown failed\" message, got: %v", err)
}
})
}
Loading