Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for KUBECONTEXT environment variable and --kubecontext option into botkube CLI #1457

Merged
merged 3 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cli/docs/botkube_install.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ botkube install --repo @local
--force Force resource updates through a replacement strategy
-h, --help help for install
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--kubecontext string The name of the kubeconfig context to use.
--namespace string Botkube installation namespace. (default "botkube")
--no-hooks Disable pre/post install/upgrade hooks
--release-name string Botkube Helm chart release name. (default "botkube")
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/docs/botkube_migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ botkube migrate [OPTIONS] [flags]
--image-tag string Botkube image tag, e.g. "latest" or "v1.7.0"
--instance-name string Botkube Cloud Instance name that will be created
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--kubecontext string The name of the kubeconfig context to use.
-l, --label string Label used for identifying the Botkube pod (default "app=botkube")
-n, --namespace string Namespace of Botkube pod (default "botkube")
-q, --skip-connect Skips connecting to Botkube Cloud after migration
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/docs/botkube_uninstall.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ botkube uninstall --release-name botkube-dev
-h, --help help for uninstall
--keep-history remove all associated resources and mark the release as deleted, but retain the release history
--kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster.
--kubecontext string The name of the kubeconfig context to use.
--namespace string Botkube namespace. (default "botkube")
--no-hooks prevent hooks from running during uninstallation
--release-name string Botkube Helm release name. (default "botkube")
Expand Down
13 changes: 7 additions & 6 deletions internal/cli/helmx/action_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,23 @@ import (

"helm.sh/helm/v3/pkg/action"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/rest"

"github.com/kubeshop/botkube/internal/cli"
"github.com/kubeshop/botkube/internal/kubex"
"github.com/kubeshop/botkube/pkg/ptr"
)

const helmDriver = "secrets"

// GetActionConfiguration returns generic configuration for Helm actions.
func GetActionConfiguration(k8sCfg *rest.Config, forNamespace string) (*action.Configuration, error) {
func GetActionConfiguration(k8sCfg *kubex.ConfigWithMeta, forNamespace string) (*action.Configuration, error) {
actionConfig := new(action.Configuration)
helmCfg := &genericclioptions.ConfigFlags{
APIServer: &k8sCfg.Host,
Insecure: &k8sCfg.Insecure,
CAFile: &k8sCfg.CAFile,
BearerToken: &k8sCfg.BearerToken,
APIServer: &k8sCfg.K8s.Host,
Insecure: &k8sCfg.K8s.Insecure,
CAFile: &k8sCfg.K8s.CAFile,
BearerToken: &k8sCfg.K8s.BearerToken,
Context: &k8sCfg.CurrentContext,
Namespace: ptr.FromType(forNamespace),
}

Expand Down
4 changes: 2 additions & 2 deletions internal/cli/install/helm/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import (
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
"k8s.io/client-go/rest"

"github.com/kubeshop/botkube/internal/cli"
"github.com/kubeshop/botkube/internal/cli/helmx"
"github.com/kubeshop/botkube/internal/cli/install/iox"
"github.com/kubeshop/botkube/internal/cli/printer"
"github.com/kubeshop/botkube/internal/kubex"
)

const restartAnnotationFmt = "extraAnnotations.cli\\.botkube\\.io\\/restart\\-timestamp=\"%d\""
Expand All @@ -35,7 +35,7 @@ type Helm struct {
}

// NewHelm returns a new Helm instance.
func NewHelm(k8sCfg *rest.Config, forNamespace string) (*Helm, error) {
func NewHelm(k8sCfg *kubex.ConfigWithMeta, forNamespace string) (*Helm, error) {
configuration, err := helmx.GetActionConfiguration(k8sCfg, forNamespace)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func Install(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, opt
return err
}

helmInstaller, err := helm.NewHelm(k8sCfg.K8s, opts.HelmParams.Namespace)
helmInstaller, err := helm.NewHelm(k8sCfg, opts.HelmParams.Namespace)
if err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/uninstall/helm/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import (

"github.com/avast/retry-go/v4"
"helm.sh/helm/v3/pkg/action"
"k8s.io/client-go/rest"

"github.com/kubeshop/botkube/internal/cli/helmx"
"github.com/kubeshop/botkube/internal/cli/printer"
"github.com/kubeshop/botkube/internal/kubex"
)

// Helm provides option to or delete Helm release.
Expand All @@ -18,7 +18,7 @@ type Helm struct {
}

// NewHelm returns a new Helm instance.
func NewHelm(k8sCfg *rest.Config, forNamespace string) (*Helm, error) {
func NewHelm(k8sCfg *kubex.ConfigWithMeta, forNamespace string) (*Helm, error) {
configuration, err := helmx.GetActionConfiguration(k8sCfg, forNamespace)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/uninstall/uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func Uninstall(ctx context.Context, w io.Writer, k8sCfg *kubex.ConfigWithMeta, o
}
}

uninstaller, err := helm.NewHelm(k8sCfg.K8s, opts.HelmParams.ReleaseNamespace)
uninstaller, err := helm.NewHelm(k8sCfg, opts.HelmParams.ReleaseNamespace)
if err != nil {
return err
}
Expand Down
89 changes: 59 additions & 30 deletions internal/kubex/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import (
)

var kubeconfig string
var kubecontext string

// RegisterKubeconfigFlag registers `--kubeconfig` flag.
func RegisterKubeconfigFlag(flags *pflag.FlagSet) {
flags.StringVar(&kubeconfig, clientcmd.RecommendedConfigPathFlag, "", "Paths to a kubeconfig. Only required if out-of-cluster.")
flags.StringVar(&kubecontext, "kubecontext", "", "The name of the kubeconfig context to use.")
}

type ConfigWithMeta struct {
Expand All @@ -35,53 +37,80 @@ type ConfigWithMeta struct {
//
// code inspired by sigs.k8s.io/controller-runtime@v0.13.1/pkg/client/config/config.go
func LoadRestConfigWithMetaInformation() (*ConfigWithMeta, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
// 1. --kubeconfig flag
if kubeconfig != "" {
c := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, nil)
return transform(c)
}

// 2. KUBECONFIG environment variable pointing at a file
kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
if len(kubeconfigPath) == 0 {
if c, err := rest.InClusterConfig(); err == nil {
return &ConfigWithMeta{
K8s: c,
CurrentContext: "In cluster",
}, nil
}
}

// 3. In-cluster config if running in cluster
// 4. $HOME/.kube/config if exists
// 5. user.HomeDir/.kube/config if exists
//
// NOTE: For default config file locations, upstream only checks
// $HOME for the user's home directory, but we can also try
// os/user.HomeDir when $HOME is unset.
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if _, ok := os.LookupEnv("HOME"); !ok {
u, err := user.Current()
if err != nil {
return nil, fmt.Errorf("could not get current user: %w", err)
loadingRules.ExplicitPath = kubeconfig
} else {
// 2. KUBECONFIG environment variable pointing at a file
kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
if len(kubeconfigPath) == 0 {
// 3. In-cluster config if running in cluster
if c, err := rest.InClusterConfig(); err == nil {
return &ConfigWithMeta{
K8s: c,
CurrentContext: "In cluster",
}, nil
}
} else {
loadingRules.ExplicitPath = kubeconfigPath
// 4. $HOME/.kube/config if exists
// 5. user.HomeDir/.kube/config if exists
//
// NOTE: For default config file locations, upstream only checks
// $HOME for the user's home directory, but we can also try
// os/user.HomeDir when $HOME is unset.
if _, ok := os.LookupEnv("HOME"); !ok {
u, err := user.Current()
if err != nil {
return nil, fmt.Errorf("could not get current user: %w", err)
}
loadingRules.Precedence = append(loadingRules.Precedence, filepath.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName))
}
}
loadingRules.Precedence = append(loadingRules.Precedence, filepath.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName))
}

return transform(clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, nil))
configOverrides := &clientcmd.ConfigOverrides{CurrentContext: loadKubecontext("")}
return transform(clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides))
}

func transform(c clientcmd.ClientConfig) (*ConfigWithMeta, error) {
rawConfig, err := c.RawConfig()
if err != nil {
return nil, fmt.Errorf("while getting raw config: %v", err)
}

clientConfig, err := c.ClientConfig()
if err != nil {
return nil, fmt.Errorf("while getting client config: %v", err)
}

return &ConfigWithMeta{
K8s: clientConfig,
CurrentContext: rawConfig.CurrentContext,
CurrentContext: loadKubecontext(rawConfig.CurrentContext),
}, nil
}

// Loads kubecontext.
//
// Config precedence:
//
// * --kubecontext flag
//
// * KUBECONTEXT environment variable
//
// * fallback value
func loadKubecontext(fallbackValue string) string {
// 1. --kubecontext flag
if kubecontext != "" {
return kubecontext
}

// 2. KUBECONTEXT env
kubeCtx := os.Getenv("KUBECONTEXT")
if kubeCtx != "" {
return kubeCtx
}

return fallbackValue
}
Loading