diff --git a/cmd/root.go b/cmd/root.go index 5a858107..610c7c7e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -128,11 +128,6 @@ func loadConfiguration() (*config.Config, error) { k8sCfg := client.NewConfig(k8sFlags) k9sCfg := config.NewConfig(k8sCfg) var errs error - conn, err := client.InitConnection(k8sCfg) - k9sCfg.SetConnection(conn) - if err != nil { - errs = errors.Join(errs, err) - } if err := k9sCfg.Load(config.AppConfigFile, false); err != nil { errs = errors.Join(errs, err) @@ -142,14 +137,24 @@ func loadConfiguration() (*config.Config, error) { log.Error().Err(err).Msgf("config refine failed") errs = errors.Join(errs, err) } + + conn, err := client.InitConnection(k8sCfg) + + if err != nil { + errs = errors.Join(errs, err) + } + // Try to access server version if that fail. Connectivity issue? if !conn.CheckConnectivity() { errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName())) } + if !conn.ConnectionOK() { errs = errors.Join(errs, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())) } + k9sCfg.SetConnection(conn) + log.Info().Msg("✅ Kubernetes connectivity") if err := k9sCfg.Save(false); err != nil { log.Error().Err(err).Msg("Config save") diff --git a/internal/client/client.go b/internal/client/client.go index e43a561a..626e69e2 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -286,8 +286,8 @@ func (a *APIClient) CheckConnectivity() bool { } }() - // Need reload to pick up any kubeconfig changes. - cfg, err := NewConfig(a.config.flags).RESTConfig() + cfg, err := a.config.RESTConfig() + if err != nil { log.Error().Err(err).Msgf("restConfig load failed") a.connOK = false @@ -551,9 +551,8 @@ func (a *APIClient) SwitchContext(name string) error { a.reset() ResetMetrics() - if !a.CheckConnectivity() { - return fmt.Errorf("unable to connect to context %q", name) - } + // Need reload to pick up any kubeconfig changes. + a.config = NewConfig(a.config.flags) return nil } diff --git a/internal/client/config.go b/internal/client/config.go index 1617ac6f..e9f01000 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -6,6 +6,8 @@ package client import ( "errors" "fmt" + "net/http" + "net/url" "strings" "sync" "time" @@ -27,6 +29,7 @@ const ( type Config struct { flags *genericclioptions.ConfigFlags mx sync.RWMutex + proxy func(*http.Request) (*url.URL, error) } // NewConfig returns a new k8s config or an error if the flags are invalid. @@ -50,7 +53,17 @@ func (c *Config) CallTimeout() time.Duration { } func (c *Config) RESTConfig() (*restclient.Config, error) { - return c.clientConfig().ClientConfig() + cfg, err := c.clientConfig().ClientConfig() + + if err != nil { + return nil, err + } + + if c.proxy != nil { + cfg.Proxy = c.proxy + } + + return cfg, nil } // Flags returns configuration flags. @@ -179,6 +192,15 @@ func (c *Config) GetContext(n string) (*api.Context, error) { return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) } +func (c *Config) SetProxy(proxy func(*http.Request) (*url.URL, error)) { + c.proxy = proxy +} + +func (c *Config) WithProxy(proxy func(*http.Request) (*url.URL, error)) *Config { + c.SetProxy(proxy) + return c +} + // Contexts fetch all available contexts. func (c *Config) Contexts() (map[string]*api.Context, error) { cfg, err := c.RawConfig() diff --git a/internal/config/data/context.go b/internal/config/data/context.go index 8feb0a9b..358a1aae 100644 --- a/internal/config/data/context.go +++ b/internal/config/data/context.go @@ -20,6 +20,7 @@ type Context struct { View *View `yaml:"view"` FeatureGates FeatureGates `yaml:"featureGates"` PortForwardAddress string `yaml:"portForwardAddress"` + Proxy *Proxy `yaml:"proxy"` mx sync.RWMutex } diff --git a/internal/config/data/proxy.go b/internal/config/data/proxy.go new file mode 100644 index 00000000..6bce7543 --- /dev/null +++ b/internal/config/data/proxy.go @@ -0,0 +1,6 @@ +package data + +// Proxy tracks a context's proxy configuration. +type Proxy struct { + Address string `yaml:"address"` +} diff --git a/internal/config/data/types.go b/internal/config/data/types.go index 9203e352..31e23416 100644 --- a/internal/config/data/types.go +++ b/internal/config/data/types.go @@ -4,6 +4,8 @@ package data import ( + "net/http" + "net/url" "os" "github.com/derailed/k9s/internal/config/json" @@ -43,4 +45,7 @@ type KubeSettings interface { // GetContext returns a given context configuration or err if not found. GetContext(string) (*api.Context, error) + + // SetProxy sets the proxy for the active context, if present + SetProxy(proxy func(*http.Request) (*url.URL, error)) } diff --git a/internal/config/json/schemas/context.json b/internal/config/json/schemas/context.json index e392dd7e..b9545b1a 100644 --- a/internal/config/json/schemas/context.json +++ b/internal/config/json/schemas/context.json @@ -11,6 +11,19 @@ "readOnly": {"type": "boolean"}, "skin": { "type": "string" }, "portForwardAddress": { "type": "string" }, + "proxy": { + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "additionalProperties": false, + "properties": + { + "address": {"type": "string"} + } + } + ] + }, "namespace": { "type": "object", "additionalProperties": false, @@ -41,4 +54,4 @@ } }, "required": ["k9s"] -} \ No newline at end of file +} diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 953fb7ca..df2e231a 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "io/fs" + "net/http" + "net/url" "os" "path/filepath" "sync" @@ -216,6 +218,27 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) { } k.setActiveConfig(cfg) + if cfg.Context.Proxy != nil { + k.ks.SetProxy(func(*http.Request) (*url.URL, error) { + log.Debug().Msgf("[Proxy]: %s", cfg.Context.Proxy.Address) + return url.Parse(cfg.Context.Proxy.Address) + }) + + if k.conn != nil && k.conn.Config() != nil { + // We get on this branch when the user switches the context and k9s + // already has an API connection object so we just set the proxy to + // avoid recreation using client.InitConnection + k.conn.Config().SetProxy(func(*http.Request) (*url.URL, error) { + log.Debug().Msgf("[Proxy]: %s", cfg.Context.Proxy.Address) + return url.Parse(cfg.Context.Proxy.Address) + }) + + if !k.conn.CheckConnectivity() { + return nil, fmt.Errorf("unable to connect to context %q", n) + } + } + } + k.Validate(k.conn, k.ks) // If the context specifies a namespace, use it! if ns := ct.Namespace; ns != client.BlankNamespace { diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go index eae5709d..c8f954fb 100644 --- a/internal/config/mock/test_helpers.go +++ b/internal/config/mock/test_helpers.go @@ -7,6 +7,8 @@ import ( "errors" "fmt" "io/fs" + "net/http" + "net/url" "os" "strings" @@ -106,6 +108,8 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) { return mm, nil } +func (m mockKubeSettings) SetProxy(proxy func(*http.Request) (*url.URL, error)) {} + type mockConnection struct { ct string }