From 572d12d5f3da15062220021aece86c8a3a81dc8e Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 12:08:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(apply):=20=E8=8A=82=E7=82=B9=E4=B8=8A?= =?UTF-8?q?=E6=89=BE=E4=B8=8D=E5=88=B0=20kubeconfig=20=E6=97=B6=E5=9B=9E?= =?UTF-8?q?=E9=80=80=20k3s.yaml=20=E5=B9=B6=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/felis/apply.go | 30 +++++++++++------------------- cmd/felis/apply_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/cmd/felis/apply.go b/cmd/felis/apply.go index bf9a682..afdf573 100644 --- a/cmd/felis/apply.go +++ b/cmd/felis/apply.go @@ -8,7 +8,9 @@ import ( "fmt" "io" "os" + "os/signal" "strings" + "syscall" "felis.lolicon.best/internal/apis/felis/v1alpha1" "felis.lolicon.best/internal/naming" @@ -16,10 +18,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -111,20 +109,15 @@ func cmdApply(args []string, stdout, stderr io.Writer) int { } // ------- K8s client (one context, one client) ------- - // SetupSignalHandler must be called exactly once per process — - // controller-runtime panics on a second call. We create ctx and the - // K8s client here and thread both through every downstream call so no - // callee ever needs to call SetupSignalHandler again. - ctx := ctrl.SetupSignalHandler() - - scheme := runtime.NewScheme() - utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(v1alpha1.AddToScheme(scheme)) - - cfg := ctrl.GetConfigOrDie() - cl, err := client.New(cfg, client.Options{Scheme: scheme}) + // The operator runs this on the node, where the kubeconfig is k3s's own file and + // neither $KUBECONFIG nor ~/.kube is set. buildSystemServerClient falls back to that + // file and names what it tried; ctrl.GetConfigOrDie exited 1 there without a word, + // because controller-runtime's logger is never set up in a CLI command. + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + cl, err := buildSystemServerClient() if err != nil { - fmt.Fprintf(stderr, "felis apply: build client: %v\n", err) + fmt.Fprintf(stderr, "felis apply: %v\n", err) return 1 } @@ -264,8 +257,7 @@ func buildMinecraftServerFromApplyRequest(req applyRequest, namespace string) (* // request if any CRD already carries the given spec.subdomain. metadata.name // uniqueness is enforced by K8s on Create, but spec.subdomain must be checked // here because two CRDs with different names could otherwise share a subdomain. -// It reuses the caller's context and K8s client — it never calls -// SetupSignalHandler or builds its own client. +// It reuses the caller's context and K8s client. func checkSubdomainUnique(ctx context.Context, cl client.Client, namespace, subdomain string) error { var list v1alpha1.MinecraftServerList if err := cl.List(ctx, &list, client.InNamespace(namespace)); err != nil { diff --git a/cmd/felis/apply_test.go b/cmd/felis/apply_test.go index a52179b..e1fa506 100644 --- a/cmd/felis/apply_test.go +++ b/cmd/felis/apply_test.go @@ -1,7 +1,10 @@ package main import ( + "bytes" "encoding/json" + "os" + "path/filepath" "strings" "testing" @@ -370,3 +373,27 @@ func resList(specs ...string) corev1.ResourceList { } return rl } + +// TestApplyReportsAMissingKubeconfig pins the node-side failure: with no kubeconfig to +// find, apply says which ones it tried and exits 1. It used to call +// ctrl.GetConfigOrDie, which ended the process with exit 1 and nothing printed. +func TestApplyReportsAMissingKubeconfig(t *testing.T) { + if _, err := os.Stat(hostBootstrapKubeconfigPath); err == nil { + t.Skipf("%s exists on this machine", hostBootstrapKubeconfigPath) + } + dir := t.TempDir() + t.Setenv("KUBECONFIG", filepath.Join(dir, "missing")) + t.Setenv("KUBERNETES_SERVICE_HOST", "") + form := filepath.Join(dir, "server.json") + if err := os.WriteFile(form, []byte(`{"name":"alpha","subdomain":"alpha","image":"registry.felis.svc:5000/felis/paper:demo","memory":"1Gi","storage":"1Gi"}`), 0o600); err != nil { + t.Fatal(err) + } + var out, errw bytes.Buffer + if code := cmdApply([]string{"-f", form}, &out, &errw); code != 1 { + t.Fatalf("exit = %d, want 1; stderr %q", code, errw.String()) + } + want := "felis apply: no reachable kubeconfig (tried in-cluster/$KUBECONFIG/~/.kube and " + hostBootstrapKubeconfigPath + "): stat " + hostBootstrapKubeconfigPath + ": no such file or directory\n" + if errw.String() != want || out.Len() != 0 { + t.Fatalf("stdout %q, stderr %q, want stderr %q", out.String(), errw.String(), want) + } +}