Add proxy configuration at the context level (#2675)

mine
Alexandru Placinta 2025-01-12 18:17:19 +02:00 committed by GitHub
parent 388df15dd6
commit b3bd60b67f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 90 additions and 12 deletions

View File

@ -128,11 +128,6 @@ func loadConfiguration() (*config.Config, error) {
k8sCfg := client.NewConfig(k8sFlags) k8sCfg := client.NewConfig(k8sFlags)
k9sCfg := config.NewConfig(k8sCfg) k9sCfg := config.NewConfig(k8sCfg)
var errs error 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 { if err := k9sCfg.Load(config.AppConfigFile, false); err != nil {
errs = errors.Join(errs, err) errs = errors.Join(errs, err)
@ -142,14 +137,24 @@ func loadConfiguration() (*config.Config, error) {
log.Error().Err(err).Msgf("config refine failed") log.Error().Err(err).Msgf("config refine failed")
errs = errors.Join(errs, err) 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? // Try to access server version if that fail. Connectivity issue?
if !conn.CheckConnectivity() { if !conn.CheckConnectivity() {
errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName())) errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName()))
} }
if !conn.ConnectionOK() { if !conn.ConnectionOK() {
errs = errors.Join(errs, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())) errs = errors.Join(errs, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName()))
} }
k9sCfg.SetConnection(conn)
log.Info().Msg("✅ Kubernetes connectivity") log.Info().Msg("✅ Kubernetes connectivity")
if err := k9sCfg.Save(false); err != nil { if err := k9sCfg.Save(false); err != nil {
log.Error().Err(err).Msg("Config save") log.Error().Err(err).Msg("Config save")

View File

@ -286,8 +286,8 @@ func (a *APIClient) CheckConnectivity() bool {
} }
}() }()
// Need reload to pick up any kubeconfig changes. cfg, err := a.config.RESTConfig()
cfg, err := NewConfig(a.config.flags).RESTConfig()
if err != nil { if err != nil {
log.Error().Err(err).Msgf("restConfig load failed") log.Error().Err(err).Msgf("restConfig load failed")
a.connOK = false a.connOK = false
@ -551,9 +551,8 @@ func (a *APIClient) SwitchContext(name string) error {
a.reset() a.reset()
ResetMetrics() ResetMetrics()
if !a.CheckConnectivity() { // Need reload to pick up any kubeconfig changes.
return fmt.Errorf("unable to connect to context %q", name) a.config = NewConfig(a.config.flags)
}
return nil return nil
} }

View File

@ -6,6 +6,8 @@ package client
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"net/url"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -27,6 +29,7 @@ const (
type Config struct { type Config struct {
flags *genericclioptions.ConfigFlags flags *genericclioptions.ConfigFlags
mx sync.RWMutex mx sync.RWMutex
proxy func(*http.Request) (*url.URL, error)
} }
// NewConfig returns a new k8s config or an error if the flags are invalid. // 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) { 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. // 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) 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. // Contexts fetch all available contexts.
func (c *Config) Contexts() (map[string]*api.Context, error) { func (c *Config) Contexts() (map[string]*api.Context, error) {
cfg, err := c.RawConfig() cfg, err := c.RawConfig()

View File

@ -20,6 +20,7 @@ type Context struct {
View *View `yaml:"view"` View *View `yaml:"view"`
FeatureGates FeatureGates `yaml:"featureGates"` FeatureGates FeatureGates `yaml:"featureGates"`
PortForwardAddress string `yaml:"portForwardAddress"` PortForwardAddress string `yaml:"portForwardAddress"`
Proxy *Proxy `yaml:"proxy"`
mx sync.RWMutex mx sync.RWMutex
} }

View File

@ -0,0 +1,6 @@
package data
// Proxy tracks a context's proxy configuration.
type Proxy struct {
Address string `yaml:"address"`
}

View File

@ -4,6 +4,8 @@
package data package data
import ( import (
"net/http"
"net/url"
"os" "os"
"github.com/derailed/k9s/internal/config/json" "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 returns a given context configuration or err if not found.
GetContext(string) (*api.Context, error) GetContext(string) (*api.Context, error)
// SetProxy sets the proxy for the active context, if present
SetProxy(proxy func(*http.Request) (*url.URL, error))
} }

View File

@ -11,6 +11,19 @@
"readOnly": {"type": "boolean"}, "readOnly": {"type": "boolean"},
"skin": { "type": "string" }, "skin": { "type": "string" },
"portForwardAddress": { "type": "string" }, "portForwardAddress": { "type": "string" },
"proxy": {
"oneOf": [
{ "type": "null" },
{
"type": "object",
"additionalProperties": false,
"properties":
{
"address": {"type": "string"}
}
}
]
},
"namespace": { "namespace": {
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,

View File

@ -7,6 +7,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -216,6 +218,27 @@ func (k *K9s) ActivateContext(n string) (*data.Context, error) {
} }
k.setActiveConfig(cfg) 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) k.Validate(k.conn, k.ks)
// If the context specifies a namespace, use it! // If the context specifies a namespace, use it!
if ns := ct.Namespace; ns != client.BlankNamespace { if ns := ct.Namespace; ns != client.BlankNamespace {

View File

@ -7,6 +7,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"net/http"
"net/url"
"os" "os"
"strings" "strings"
@ -106,6 +108,8 @@ func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {
return mm, nil return mm, nil
} }
func (m mockKubeSettings) SetProxy(proxy func(*http.Request) (*url.URL, error)) {}
type mockConnection struct { type mockConnection struct {
ct string ct string
} }