From ac652c590433e1ffe63d46d6bf0965a93e3335fb Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Tue, 28 Jan 2025 16:52:57 +0100 Subject: [PATCH 1/4] Add possibility to execute k9s inside a Kubernetes POD with working incluster config --- internal/client/config.go | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/internal/client/config.go b/internal/client/config.go index e9f01000bb..52f55805d5 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -14,7 +14,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" - clientcmd "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) @@ -23,6 +23,8 @@ const ( // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true + + incluster = "incluster" ) // Config tracks a kubernetes configuration. @@ -87,6 +89,10 @@ func (c *Config) SwitchContext(name string) error { if err != nil { return fmt.Errorf("context %q does not exist", name) } + if name == incluster && ct.LocationOfOrigin == incluster { + return nil + } + // !!BOZO!! Do you need to reset the flags? flags := genericclioptions.NewConfigFlags(UsePersistentConfig) flags.Context, flags.ClusterName = &name, &ct.Cluster @@ -131,6 +137,9 @@ func (c *Config) CurrentClusterName() (string, error) { ct, ok := cfg.Contexts[cfg.CurrentContext] if !ok { + if c.isIncluster(cfg) { + return incluster, nil + } return "", fmt.Errorf("invalid current context specified: %q", cfg.CurrentContext) } if isSet(c.flags.Context) { @@ -154,6 +163,10 @@ func (c *Config) CurrentContextName() (string, error) { return "", fmt.Errorf("fail to load rawConfig: %w", err) } + if c.isIncluster(cfg) { + return incluster, nil + } + return cfg.CurrentContext, nil } @@ -162,6 +175,7 @@ func (c *Config) CurrentContextNamespace() (string, error) { if err != nil { return "", err } + context, err := c.GetContext(name) if err != nil { return "", err @@ -185,10 +199,16 @@ func (c *Config) GetContext(n string) (*api.Context, error) { if err != nil { return nil, err } + if c, ok := cfg.Contexts[n]; ok { return c, nil } + if n == incluster { + nc := c.newInclusterContext() + return nc, nil + } + return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) } @@ -208,6 +228,12 @@ func (c *Config) Contexts() (map[string]*api.Context, error) { return nil, err } + if len(cfg.Contexts) == 0 && c.isIncluster(cfg) { + return map[string]*api.Context{ + incluster: c.newInclusterContext(), + }, nil + } + return cfg.Contexts, nil } @@ -320,6 +346,9 @@ func (c *Config) CurrentUserName() (string, error) { if ctx, ok := cfg.Contexts[current]; ok { return ctx.AuthInfo, nil } + if c.isIncluster(cfg) { + return incluster, nil + } return "", errors.New("unable to locate current user") } @@ -347,6 +376,30 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { return c.clientConfig().ConfigAccess(), nil } +func (c *Config) newInclusterContext() *api.Context { + ns, _, _ := c.clientConfig().Namespace() + if ns == "" { + ns = "default" + } + nc := api.NewContext() + nc.LocationOfOrigin = incluster + nc.Cluster = incluster + nc.Namespace = ns + nc.AuthInfo = incluster + return nc +} + +func (c *Config) isIncluster(cfg api.Config) bool { + if (cfg.CurrentContext == "" || cfg.CurrentContext == incluster) && + len(cfg.Contexts) == 0 && + c.flags.KubeConfig != nil && *c.flags.KubeConfig == "" && + c.flags.ClusterName != nil && *c.flags.ClusterName == "" && + c.flags.APIServer != nil && *c.flags.APIServer == "" { + return true + } + return false +} + // ---------------------------------------------------------------------------- // Helpers... From 95e55822817fc3f3215149a615c9f6d1e1626159 Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Tue, 28 Jan 2025 16:52:57 +0100 Subject: [PATCH 2/4] Add possibility to execute k9s inside a Kubernetes POD with working incluster config --- internal/client/config.go | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/internal/client/config.go b/internal/client/config.go index e9f01000bb..52f55805d5 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -14,7 +14,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" - clientcmd "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" ) @@ -23,6 +23,8 @@ const ( // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true + + incluster = "incluster" ) // Config tracks a kubernetes configuration. @@ -87,6 +89,10 @@ func (c *Config) SwitchContext(name string) error { if err != nil { return fmt.Errorf("context %q does not exist", name) } + if name == incluster && ct.LocationOfOrigin == incluster { + return nil + } + // !!BOZO!! Do you need to reset the flags? flags := genericclioptions.NewConfigFlags(UsePersistentConfig) flags.Context, flags.ClusterName = &name, &ct.Cluster @@ -131,6 +137,9 @@ func (c *Config) CurrentClusterName() (string, error) { ct, ok := cfg.Contexts[cfg.CurrentContext] if !ok { + if c.isIncluster(cfg) { + return incluster, nil + } return "", fmt.Errorf("invalid current context specified: %q", cfg.CurrentContext) } if isSet(c.flags.Context) { @@ -154,6 +163,10 @@ func (c *Config) CurrentContextName() (string, error) { return "", fmt.Errorf("fail to load rawConfig: %w", err) } + if c.isIncluster(cfg) { + return incluster, nil + } + return cfg.CurrentContext, nil } @@ -162,6 +175,7 @@ func (c *Config) CurrentContextNamespace() (string, error) { if err != nil { return "", err } + context, err := c.GetContext(name) if err != nil { return "", err @@ -185,10 +199,16 @@ func (c *Config) GetContext(n string) (*api.Context, error) { if err != nil { return nil, err } + if c, ok := cfg.Contexts[n]; ok { return c, nil } + if n == incluster { + nc := c.newInclusterContext() + return nc, nil + } + return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) } @@ -208,6 +228,12 @@ func (c *Config) Contexts() (map[string]*api.Context, error) { return nil, err } + if len(cfg.Contexts) == 0 && c.isIncluster(cfg) { + return map[string]*api.Context{ + incluster: c.newInclusterContext(), + }, nil + } + return cfg.Contexts, nil } @@ -320,6 +346,9 @@ func (c *Config) CurrentUserName() (string, error) { if ctx, ok := cfg.Contexts[current]; ok { return ctx.AuthInfo, nil } + if c.isIncluster(cfg) { + return incluster, nil + } return "", errors.New("unable to locate current user") } @@ -347,6 +376,30 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { return c.clientConfig().ConfigAccess(), nil } +func (c *Config) newInclusterContext() *api.Context { + ns, _, _ := c.clientConfig().Namespace() + if ns == "" { + ns = "default" + } + nc := api.NewContext() + nc.LocationOfOrigin = incluster + nc.Cluster = incluster + nc.Namespace = ns + nc.AuthInfo = incluster + return nc +} + +func (c *Config) isIncluster(cfg api.Config) bool { + if (cfg.CurrentContext == "" || cfg.CurrentContext == incluster) && + len(cfg.Contexts) == 0 && + c.flags.KubeConfig != nil && *c.flags.KubeConfig == "" && + c.flags.ClusterName != nil && *c.flags.ClusterName == "" && + c.flags.APIServer != nil && *c.flags.APIServer == "" { + return true + } + return false +} + // ---------------------------------------------------------------------------- // Helpers... From 6b7e399eef05d8936651a4eba9ffa62526431fb9 Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Sun, 30 Mar 2025 22:35:44 +0200 Subject: [PATCH 3/4] Adjust code according to code review findings. --- internal/client/config.go | 57 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/internal/client/config.go b/internal/client/config.go index 62703cb173..ad2f5a28ec 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -24,7 +25,7 @@ const ( // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true - incluster = "incluster" + inClusterConfig = "incluster" ) // Config tracks a kubernetes configuration. @@ -87,7 +88,7 @@ func (c *Config) SwitchContext(name string) error { if err != nil { return fmt.Errorf("context %q does not exist", name) } - if name == incluster && ct.LocationOfOrigin == incluster { + if name == inClusterConfig && ct.LocationOfOrigin == inClusterConfig { return nil } @@ -135,8 +136,8 @@ func (c *Config) CurrentClusterName() (string, error) { ct, ok := cfg.Contexts[cfg.CurrentContext] if !ok { - if c.isIncluster(cfg) { - return incluster, nil + if c.isInCluster(cfg) { + return inClusterConfig, nil } return "", fmt.Errorf("invalid current context specified: %q", cfg.CurrentContext) } @@ -161,8 +162,8 @@ func (c *Config) CurrentContextName() (string, error) { return "", fmt.Errorf("fail to load rawConfig: %w", err) } - if c.isIncluster(cfg) { - return incluster, nil + if c.isInCluster(cfg) { + return inClusterConfig, nil } return cfg.CurrentContext, nil @@ -202,9 +203,8 @@ func (c *Config) GetContext(n string) (*api.Context, error) { return c, nil } - if n == incluster { - nc := c.newInclusterContext() - return nc, nil + if n == inClusterConfig { + return c.newInclusterContext(), nil } return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) @@ -222,9 +222,9 @@ func (c *Config) Contexts() (map[string]*api.Context, error) { return nil, err } - if len(cfg.Contexts) == 0 && c.isIncluster(cfg) { + if len(cfg.Contexts) == 0 && c.isInCluster(cfg) { return map[string]*api.Context{ - incluster: c.newInclusterContext(), + inClusterConfig: c.newInclusterContext(), }, nil } @@ -340,8 +340,8 @@ func (c *Config) CurrentUserName() (string, error) { if ctx, ok := cfg.Contexts[current]; ok { return ctx.AuthInfo, nil } - if c.isIncluster(cfg) { - return incluster, nil + if c.isInCluster(cfg) { + return inClusterConfig, nil } return "", errors.New("unable to locate current user") @@ -373,22 +373,23 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { func (c *Config) newInclusterContext() *api.Context { ns, _, _ := c.clientConfig().Namespace() if ns == "" { - ns = "default" - } - nc := api.NewContext() - nc.LocationOfOrigin = incluster - nc.Cluster = incluster - nc.Namespace = ns - nc.AuthInfo = incluster - return nc + ns = DefaultNamespace + } + return &api.Context{ + LocationOfOrigin: inClusterConfig, + Cluster: inClusterConfig, + Namespace: ns, + AuthInfo: inClusterConfig, + Extensions: make(map[string]runtime.Object), + } } -func (c *Config) isIncluster(cfg api.Config) bool { - if (cfg.CurrentContext == "" || cfg.CurrentContext == incluster) && +func (c *Config) isInCluster(cfg api.Config) bool { + if (cfg.CurrentContext == "" || cfg.CurrentContext == inClusterConfig) && len(cfg.Contexts) == 0 && - c.flags.KubeConfig != nil && *c.flags.KubeConfig == "" && - c.flags.ClusterName != nil && *c.flags.ClusterName == "" && - c.flags.APIServer != nil && *c.flags.APIServer == "" { + isEmptyString(c.flags.KubeConfig) && + isEmptyString(c.flags.ClusterName) && + isEmptyString(c.flags.APIServer) { return true } return false @@ -404,3 +405,7 @@ func isSet(s *string) bool { func areSet(s *[]string) bool { return s != nil && len(*s) != 0 } + +func isEmptyString(s *string) bool { + return s != nil && *s == "" +} From 2f4b062e130a248e3d8917348a904cc9c2fe2613 Mon Sep 17 00:00:00 2001 From: Gregor Noczinski Date: Sun, 30 Mar 2025 23:11:26 +0200 Subject: [PATCH 4/4] Added documentation --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index db0170e8e2..17c98f752c 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,37 @@ Binaries for Linux, Windows and Mac are available as tarballs in the [release pa --- +## Running directly inside a Kubernetes cluster + +You can run k9s directly inside a Kubernetes cluster by creating a Pod with the official image (quay.io/derailed/k9s). This works great with tools like [Telepresence](https://telepresence.io/) or [Engity's Bifröst](https://bifroest.engity.org/) that let you connect to Kubernetes clusters from your computer as if you were right inside them. + +They usually require you to create a matching Service Account, like the following: + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: k9s + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: k9s +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: k9s + namespace: default +``` + +This allows you to use the Service Account `k9s` within the `default` namespace with full cluster permissions. For more details on how to use and define Service Accounts, please refer to [the official Kubernetes documentation](https://kubernetes.io/docs/concepts/security/service-accounts/#how-to-use). + +--- + ## PreFlight Checks * K9s uses 256 colors terminal mode. On `Nix system make sure TERM is set accordingly.