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. diff --git a/internal/client/config.go b/internal/client/config.go index 6814b5a7ff..ad2f5a28ec 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -12,9 +12,10 @@ import ( "sync" "time" + "k8s.io/apimachinery/pkg/runtime" "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 +24,8 @@ const ( // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true + + inClusterConfig = "incluster" ) // Config tracks a kubernetes configuration. @@ -85,6 +88,10 @@ func (c *Config) SwitchContext(name string) error { if err != nil { return fmt.Errorf("context %q does not exist", name) } + if name == inClusterConfig && ct.LocationOfOrigin == inClusterConfig { + return nil + } + // !!BOZO!! Do you need to reset the flags? flags := genericclioptions.NewConfigFlags(UsePersistentConfig) flags.Context, flags.ClusterName = &name, &ct.Cluster @@ -129,6 +136,9 @@ func (c *Config) CurrentClusterName() (string, error) { ct, ok := cfg.Contexts[cfg.CurrentContext] if !ok { + if c.isInCluster(cfg) { + return inClusterConfig, nil + } return "", fmt.Errorf("invalid current context specified: %q", cfg.CurrentContext) } if isSet(c.flags.Context) { @@ -152,6 +162,10 @@ func (c *Config) CurrentContextName() (string, error) { return "", fmt.Errorf("fail to load rawConfig: %w", err) } + if c.isInCluster(cfg) { + return inClusterConfig, nil + } + return cfg.CurrentContext, nil } @@ -160,6 +174,7 @@ func (c *Config) CurrentContextNamespace() (string, error) { if err != nil { return "", err } + context, err := c.GetContext(name) if err != nil { return "", err @@ -183,10 +198,15 @@ 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 == inClusterConfig { + return c.newInclusterContext(), nil + } + return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) } @@ -202,6 +222,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{ + inClusterConfig: c.newInclusterContext(), + }, nil + } + return cfg.Contexts, nil } @@ -314,6 +340,9 @@ func (c *Config) CurrentUserName() (string, error) { if ctx, ok := cfg.Contexts[current]; ok { return ctx.AuthInfo, nil } + if c.isInCluster(cfg) { + return inClusterConfig, nil + } return "", errors.New("unable to locate current user") } @@ -341,6 +370,31 @@ 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 = 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 == inClusterConfig) && + len(cfg.Contexts) == 0 && + isEmptyString(c.flags.KubeConfig) && + isEmptyString(c.flags.ClusterName) && + isEmptyString(c.flags.APIServer) { + return true + } + return false +} + // ---------------------------------------------------------------------------- // Helpers... @@ -351,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 == "" +}