Loading cmd/felis/apply.go +11 −19 Changes for cmd/felis/apply.go: 11 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -8,7 +8,9 @@ import ( "fmt" "io" "os" "os/signal" "strings" "syscall" "felis.lolicon.best/internal/apis/felis/v1alpha1" "felis.lolicon.best/internal/naming" Loading @@ -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" ) Loading Loading @@ -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 } Loading Loading @@ -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 { Loading cmd/felis/apply_test.go +27 −0 Changes for cmd/felis/apply_test.go: 27 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" Loading Loading @@ -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) } } Loading
cmd/felis/apply.go +11 −19 Changes for cmd/felis/apply.go: 11 added lines, 19 removed lines. Original line number Diff line number Diff line Loading @@ -8,7 +8,9 @@ import ( "fmt" "io" "os" "os/signal" "strings" "syscall" "felis.lolicon.best/internal/apis/felis/v1alpha1" "felis.lolicon.best/internal/naming" Loading @@ -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" ) Loading Loading @@ -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 } Loading Loading @@ -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 { Loading
cmd/felis/apply_test.go +27 −0 Changes for cmd/felis/apply_test.go: 27 added lines, 0 removed lines. Original line number Diff line number Diff line package main import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "testing" Loading Loading @@ -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) } }