Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
fix(home): adopt a LOCAL cluster's release when the client pointer is missing (#401)#407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| package cli | ||
| // Home-screen fallbacks for machines the provisioning pointer never reached | ||
| // (#401). Split from home.go to respect its file budget. | ||
| import ( | ||
| "context" | ||
| "net" | ||
| "net/url" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "github.com/tracebloc/cli/internal/cluster" | ||
| ) | ||
| // localEnvFallback answers "is there a secure environment on THIS machine?" | ||
| // when the active-client pointer is empty — the state every pre-#388 Windows | ||
| // install is in permanently, because only `client create` writes the pointer | ||
| // and the Windows installer never ran it. Field case: `doctor` said "Ready to | ||
| // run training" while home said "No secure environment on this machine yet". | ||
| // | ||
| // The ownership gate in realProbeEnv exists so a status screen never greets a | ||
| // SHARED cluster's unrelated client as yours (§7.5). This fallback keeps that | ||
| // guarantee by adopting a discovered release only when the kubeconfig's server | ||
| // is LOCAL (loopback / host.docker.internal / a k3d wildcard bind) — a cluster | ||
| // that is this machine by definition, so whatever tracebloc release runs there | ||
| // is this machine's environment. Remote/shared clusters return the same honest | ||
| // no-release the gate always produced, and every error degrades to no-release | ||
| // (never "offline" — an unprovisioned machine with no reachable local cluster | ||
| // most likely has no environment at all). | ||
| func localEnvFallback(ctx context.Context) envProbe { | ||
| resolved, err := loadClusterFn(cluster.KubeconfigOptions{}) | ||
| if err != nil { | ||
| return envProbe{local: localNoRelease} | ||
| } | ||
| if !isLocalServerURL(resolved.ServerURL) { | ||
| return envProbe{local: localNoRelease} | ||
| } | ||
| resolved.RestConfig.Timeout = homeProbeTimeout | ||
| cs, err := newClientsetFn(resolved) | ||
| if err != nil { | ||
| return envProbe{local: localNoRelease} | ||
| } | ||
| // Namespace-only discovery — never the cluster-wide scan, mirroring the | ||
| // gate's no-silent-retarget rule even on a local cluster. | ||
| release, nsUsed, err := discoverRelease(ctx, nil, cs, resolved.Namespace, false) | ||
| if err != nil { | ||
| return envProbe{local: localNoRelease} | ||
| } | ||
| ep := envProbe{name: release.ReleaseName} | ||
| if jobsManagerReady(ctx, cs, nsUsed, release) { | ||
| ep.local = localLive | ||
| } else { | ||
| ep.local = localDegraded | ||
| } | ||
| if ep.local == localLive { | ||
| if c, ok := machineCapacity(ctx, cs); ok { | ||
| ep.compute, ep.hasCompute = c, true | ||
| } | ||
| } | ||
| return ep | ||
| } | ||
| // isLocalServerURL reports whether a kubeconfig server URL points at THIS | ||
| // machine. Covers loopback names/addresses, the wildcard binds k3d writes when | ||
| // no host is pinned, and Docker Desktop's host alias (the same signals | ||
| // doctor's reachability remedy keys on). | ||
| func isLocalServerURL(serverURL string) bool { | ||
| u, err := url.Parse(serverURL) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| host := u.Hostname() | ||
| switch strings.ToLower(host) { | ||
| case "localhost", "host.docker.internal", "0.0.0.0", "::": | ||
| return true | ||
| } | ||
| if ip := net.ParseIP(host); ip != nil { | ||
| return ip.IsLoopback() || ip.IsUnspecified() | ||
| } | ||
| return false | ||
| } | ||
| // tbAliasAvailable reports whether a real tracebloc-owned `tb` alias sits next | ||
| // to this binary, so examples/remedies can echo the short name. On unix the | ||
| // installer symlinks `tb` → tracebloc and delete.go's aliasStatus judges | ||
| // ownership exactly as offboarding does. On Windows symlinks need admin, so | ||
| // install.ps1 writes a `tb.cmd` shim instead — a regular file the symlink test | ||
| // can never accept (#401): ours = the shim invokes this binary. | ||
| func tbAliasAvailable() bool { | ||
| exe, err := osExecutable() | ||
| if err != nil { | ||
| return false | ||
| } | ||
| dir := filepath.Dir(exe) | ||
| tb := filepath.Join(dir, binTB) | ||
| if tb != exe { | ||
| if _, ours := aliasStatus(tb, exe); ours { | ||
| return true | ||
| } | ||
| } | ||
| return tbCmdAliasOurs(dir, exe) | ||
| } | ||
| // tbCmdAliasOurs reports whether a tb.cmd shim in dir invokes THIS binary by | ||
| // its full path (install.ps1 writes an absolute target — the same ownership | ||
| // bar aliasStatus applies to symlinks). Matching just the basename would claim | ||
| // any third-party shim that merely mentions "tracebloc", or one invoking a | ||
| // different tracebloc at another path (Bugbot). Case-insensitive: .cmd is a | ||
| // Windows artifact and NTFS paths are case-insensitive. | ||
| func tbCmdAliasOurs(dir, exe string) bool { | ||
| b, err := os.ReadFile(filepath.Join(dir, binTB+".cmd")) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| return strings.Contains(strings.ToLower(string(b)), strings.ToLower(filepath.Clean(exe))) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package cli | ||
| import ( | ||
| "context" | ||
| "errors" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| appsv1 "k8s.io/api/apps/v1" | ||
| corev1 "k8s.io/api/core/v1" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/kubernetes/fake" | ||
| "k8s.io/client-go/rest" | ||
| "github.com/tracebloc/cli/internal/cluster" | ||
| ) | ||
| // fallbackRelease seeds the objects DiscoverParentRelease keys on (mirrors | ||
| // internal/cluster's discovery fixtures) plus a Ready jobs-manager so the | ||
| // probe classifies localLive. | ||
| func fallbackRelease(ns string) []interface{} { | ||
| dep := &appsv1.Deployment{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "tracebloc-jobs-manager", | ||
| Namespace: ns, | ||
| Labels: map[string]string{ | ||
| "app.kubernetes.io/name": "client", | ||
| "app.kubernetes.io/instance": "tracebloc", | ||
| "app.kubernetes.io/managed-by": "Helm", | ||
| "app.kubernetes.io/version": "1.9.5", | ||
| "helm.sh/chart": "client-1.9.5", | ||
| }, | ||
| }, | ||
| Status: appsv1.DeploymentStatus{ReadyReplicas: 1, Replicas: 1}, | ||
| } | ||
| svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: "jobs-manager", Namespace: ns}} | ||
| return []interface{}{dep, svc} | ||
| } | ||
| func stubFallbackSeams(t *testing.T, serverURL string, cs kubernetes.Interface, loadErr error) { | ||
| t.Helper() | ||
| origLoad, origCS := loadClusterFn, newClientsetFn | ||
| t.Cleanup(func() { loadClusterFn, newClientsetFn = origLoad, origCS }) | ||
| loadClusterFn = func(cluster.KubeconfigOptions) (*cluster.ResolvedConfig, error) { | ||
| if loadErr != nil { | ||
| return nil, loadErr | ||
| } | ||
| return &cluster.ResolvedConfig{ | ||
| Namespace: "tracebloc", | ||
| ServerURL: serverURL, | ||
| RestConfig: &rest.Config{Host: serverURL}, | ||
| }, nil | ||
| } | ||
| newClientsetFn = func(*cluster.ResolvedConfig) (kubernetes.Interface, error) { | ||
| if cs == nil { | ||
| return nil, errors.New("no clientset expected on this path") | ||
| } | ||
| return cs, nil | ||
| } | ||
| } | ||
| // #401: an empty active-client pointer must not read as "no environment" when | ||
| // a tracebloc release runs on a LOCAL cluster (the pre-#388 Windows install | ||
| // state: doctor said Ready, home said run-the-installer). | ||
| func TestLocalEnvFallback_AdoptsLocalRelease(t *testing.T) { | ||
| o := fallbackRelease("tracebloc") | ||
| cs := fake.NewClientset(o[0].(*appsv1.Deployment), o[1].(*corev1.Service)) | ||
| stubFallbackSeams(t, "https://127.0.0.1:6550", cs, nil) | ||
| ep := localEnvFallback(context.Background()) | ||
| if ep.local != localLive || ep.name != "tracebloc" { | ||
| t.Fatalf("=> %+v, want localLive named tracebloc", ep) | ||
| } | ||
| } | ||
| // The §7.5 ownership guarantee survives: a REMOTE cluster in the kubeconfig is | ||
| // never adopted without the pointer — same honest no-release as before, and | ||
| // the clientset is never even dialed. | ||
| func TestLocalEnvFallback_RemoteClusterStaysGated(t *testing.T) { | ||
| stubFallbackSeams(t, "https://k8s.corp.example:6443", nil, nil) | ||
| if ep := localEnvFallback(context.Background()); ep.local != localNoRelease { | ||
| t.Fatalf("=> %+v, want localNoRelease (gate holds for remote clusters)", ep) | ||
| } | ||
| } | ||
| func TestLocalEnvFallback_NoKubeconfigIsNoRelease(t *testing.T) { | ||
| stubFallbackSeams(t, "", nil, errors.New("no kubeconfig")) | ||
| if ep := localEnvFallback(context.Background()); ep.local != localNoRelease { | ||
| t.Fatalf("=> %+v, want localNoRelease", ep) | ||
| } | ||
| } | ||
| func TestIsLocalServerURL(t *testing.T) { | ||
| local := []string{ | ||
| "https://127.0.0.1:6550", | ||
| "https://localhost:6443", | ||
| "https://[::1]:6443", | ||
| "https://0.0.0.0:6443", // k3d wildcard bind | ||
| "https://host.docker.internal:6550", | ||
| } | ||
| remote := []string{ | ||
| "https://10.2.3.4:6443", | ||
| "https://k8s.corp.example:443", | ||
| "https://192.168.1.20:6443", | ||
| "", "not a url", | ||
| } | ||
| for _, u := range local { | ||
| if !isLocalServerURL(u) { | ||
| t.Errorf("isLocalServerURL(%q) = false, want true", u) | ||
| } | ||
| } | ||
| for _, u := range remote { | ||
| if isLocalServerURL(u) { | ||
| t.Errorf("isLocalServerURL(%q) = true, want false", u) | ||
| } | ||
| } | ||
| } | ||
| // #401: the Windows installer writes a tb.cmd shim (symlinks need admin); the | ||
| // alias check must accept it so remedies echo `tb` on Windows too. | ||
| func TestTbCmdAliasOurs(t *testing.T) { | ||
| dir := t.TempDir() | ||
| exe := filepath.Join(dir, "tracebloc.exe") | ||
| if tbCmdAliasOurs(dir, exe) { | ||
| t.Fatal("no shim present must be false") | ||
| } | ||
| shim := "@echo off\r\n\"" + exe + "\" %*\r\n" | ||
| if err := os.WriteFile(filepath.Join(dir, "tb.cmd"), []byte(shim), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if !tbCmdAliasOurs(dir, exe) { | ||
| t.Fatal("a shim invoking this binary must be ours") | ||
| } | ||
| if err := os.WriteFile(filepath.Join(dir, "tb.cmd"), []byte("@echo off\r\nsome-other-tool %*\r\n"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if tbCmdAliasOurs(dir, exe) { | ||
| t.Fatal("a shim invoking a different tool is not ours") | ||
| } | ||
| // Bugbot: mentioning the name is not ownership — neither a comment that | ||
| // says "tracebloc" nor an invocation of a DIFFERENT tracebloc install. | ||
| if err := os.WriteFile(filepath.Join(dir, "tb.cmd"), | ||
| []byte("@echo off\r\nrem tracebloc helper\r\nsome-other-tool %*\r\n"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if tbCmdAliasOurs(dir, exe) { | ||
| t.Fatal("a shim merely mentioning tracebloc is not ours") | ||
| } | ||
| other := filepath.Join(dir, "elsewhere", "tracebloc.exe") | ||
| if err := os.WriteFile(filepath.Join(dir, "tb.cmd"), | ||
| []byte("@echo off\r\n\""+other+"\" %*\r\n"), 0o755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if tbCmdAliasOurs(dir, exe) { | ||
| t.Fatal("a shim invoking a different tracebloc install is not ours") | ||
| } | ||
| } |
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.