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

Support multiple Kubeconfig files #75

Merged
merged 3 commits into from
May 24, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 15 additions & 6 deletions cmd/devserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,29 @@ import (
)

var (
debug = flag.Bool("debug", false, "Enable debug mode.")
kubeconfig = flag.String("kubeconfig", "", "Optional Kubeconfig file.")
sync = flag.Bool("sync", false, "Sync the changes from kubenav with the used Kubeconfig file.")
debugFlag = flag.Bool("debug", false, "Enable debug mode.")
kubeconfigFlag = flag.String("kubeconfig", "", "Optional Kubeconfig file.")
kubeconfigIncludeFlag = flag.String("kubeconfig-include", "", "Comma separated list of globs to include in the Kubeconfig. When this option is used the '-kubeconfig' and '-sync' flag is ignored.")
kubeconfigExcludeFlag = flag.String("kubeconfig-exclude", "", "Comma separated list of globs to exclude from the Kubeconfig. This flag must be used in combination with the '-kubeconfig-include' flag.")
syncFlag = flag.Bool("sync", false, "Sync the changes from kubenav with the used Kubeconfig file.")
)

func main() {
// Parse command-line flags.
// When the "-kubeconfig-includ" flag is used the sync flag is ignored. Therefor we set the sync value always to
// false. The same applies for the "-kubeconfig" flag, but this is handled by the Kubernetes client.
flag.Parse()

sync := *syncFlag
if *kubeconfigIncludeFlag != "" {
sync = false
}

// Setup the logger and print the version information.
log := logrus.StandardLogger()

logLevel := logrus.InfoLevel
if *debug {
if *debugFlag {
logLevel = logrus.DebugLevel
}

Expand All @@ -34,14 +43,14 @@ func main() {
log.Infof(version.BuildContext())

// Create the client for the interaction with the Kubernetes API.
client, err := kube.NewClient(*kubeconfig)
client, err := kube.NewClient(*kubeconfigFlag, *kubeconfigIncludeFlag, *kubeconfigExcludeFlag)
if err != nil {
log.WithError(err).Fatalf("Could not create Kubernetes client")
}

// Register the API for the Electron app and start the server.
router := http.NewServeMux()
electron.Register(router, *sync, client)
electron.Register(router, sync, client)

if err := http.ListenAndServe(":14122", router); err != nil {
log.WithError(err).Fatalf("kubenav server died")
Expand Down
25 changes: 17 additions & 8 deletions cmd/electron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ var (
)

var (
debug = flag.Bool("debug", false, "Enable debug mode.")
kubeconfig = flag.String("kubeconfig", "", "Optional Kubeconfig file.")
sync = flag.Bool("sync", false, "Sync the changes from kubenav with the used Kubeconfig file.")
debugFlag = flag.Bool("debug", false, "Enable debug mode.")
kubeconfigFlag = flag.String("kubeconfig", "", "Optional Kubeconfig file.")
kubeconfigIncludeFlag = flag.String("kubeconfig-include", "", "Comma separated list of globs to include in the Kubeconfig. When this option is used the '-kubeconfig' and '-sync' flag is ignored.")
kubeconfigExcludeFlag = flag.String("kubeconfig-exclude", "", "Comma separated list of globs to exclude from the Kubeconfig. This flag must be used in combination with the '-kubeconfig-include' flag.")
syncFlag = flag.Bool("sync", false, "Sync the changes from kubenav with the used Kubeconfig file.")
)

// Message is the structure of a Server Sent Event, which contains the Event and Data. Server Sent Events are used to
Expand All @@ -41,13 +43,20 @@ var messageChannel = make(chan Message)

func main() {
// Parse command-line flags.
// When the "-kubeconfig-includ" flag is used the sync flag is ignored. Therefor we set the sync value always to
// false. The same applies for the "-kubeconfig" flag, but this is handled by the Kubernetes client.
flag.Parse()

sync := *syncFlag
if *kubeconfigIncludeFlag != "" {
sync = false
}

// Setup the logger and print the version information.
log := logrus.StandardLogger()

logLevel := logrus.InfoLevel
if *debug {
if *debugFlag {
logLevel = logrus.DebugLevel
}

Expand All @@ -56,7 +65,7 @@ func main() {
log.Infof(version.BuildContext())

// Create the client for the interaction with the Kubernetes API.
client, err := kube.NewClient(*kubeconfig)
client, err := kube.NewClient(*kubeconfigFlag, *kubeconfigIncludeFlag, *kubeconfigExcludeFlag)
if err != nil {
log.WithError(err).Fatalf("Could not create Kubernetes client")
}
Expand All @@ -66,7 +75,7 @@ func main() {
// frontend from the embedded assets.
go func() {
router := http.NewServeMux()
electron.Register(router, *sync, client)
electron.Register(router, sync, client)

// Add route for Server Sent Events. The events are handled via the message channel. Possible events are
// "navigation" and "cluster". These events are handled by the frontend to navigate to another page or to modify
Expand Down Expand Up @@ -128,7 +137,7 @@ func main() {
// selected context back to the Kubeconfig file, the result from the version check and the Kubernetes client and
// logger.
updateAvailable := checkVersion(version.Version, log)
menuOptions, err := getMenuOptions(*sync, updateAvailable, client, log)
menuOptions, err := getMenuOptions(sync, updateAvailable, client, log)
if err != nil {
log.WithError(err).Fatalf("Could not create menu")
}
Expand All @@ -145,7 +154,7 @@ func main() {
VersionAstilectron: VersionAstilectron,
VersionElectron: VersionElectron,
},
Debug: *debug,
Debug: *debugFlag,
Logger: log,
MenuOptions: menuOptions,
RestoreAssets: RestoreAssets,
Expand Down
120 changes: 120 additions & 0 deletions pkg/electron/kube/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package kube

import (
"os"
"path/filepath"
"runtime"
"strings"

"k8s.io/client-go/tools/clientcmd"
)

// homeDir returns the users home directory, where the '.kube' directory is located.
// The '.kube' directory contains the configuration file for a Kubernetes cluster.
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}

// Get the home directory on windows.
return os.Getenv("USERPROFILE")
}

// loadConfigFile loads a single Kubeconfig file. If the "-kubeconfig" flag is set the provided file will be used. If
// the value is empty the "KUBECONFIG" environment variable or the "~/.kube/config" file is used.
func loadConfigFile(kubeconfig string) (clientcmd.ClientConfig, error) {
if kubeconfig == "" {
if os.Getenv("KUBECONFIG") == "" {
if home := homeDir(); home != "" {
kubeconfig = filepath.Join(home, ".kube", "config")
} else {
return nil, ErrConfigNotFound
}
} else {
kubeconfig = os.Getenv("KUBECONFIG")
}
}

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
&clientcmd.ConfigOverrides{},
), nil
}

// loadConfigFiles loads and merged multiple Kubeconfig file by the provided glob. If a file matches the exlude glob the
// file isn't included in the final config object.
func loadConfigFiles(includeKubeconfig, excludeKubeconfig string) (clientcmd.ClientConfig, error) {
includes := getFilesFromString(includeKubeconfig)
excludes := getFilesFromString(excludeKubeconfig)

includeFiles, err := getFilesForGlobs(includes)
if err != nil {
return nil, err
}

excludeFiles, err := getFilesForGlobs(excludes)
if err != nil {
return nil, err
}

var kubeconfigFiles []string
for _, file := range includeFiles {
if !fileExistsInFiles(file, excludeFiles) {
kubeconfigFiles = append(kubeconfigFiles, file)
}
}

return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{Precedence: kubeconfigFiles},
&clientcmd.ConfigOverrides{},
), nil
}

// getFilesFromString returns a slice of files from a single string. The string is splitted by the "," character. If an
// item starts with "~/" the tilde character is replaced with the path of the home directory.
//
// Note: On macOS the home directory must be lowercase.
func getFilesFromString(filesStr string) []string {
home := homeDir()
if runtime.GOOS == "darwin" {
home = strings.ToLower(home)
}

var files []string
for _, file := range strings.Split(filesStr, ",") {
if strings.HasPrefix(file, "~/") {
files = append(files, home+strings.TrimPrefix(file, "~"))
} else {
files = append(files, file)
}
}

return files
}

// getFilesForGlobs returns a list of files for a list of globs using the filepath.Glob function. The accepted pattern
// for a glob can be found here: https://golang.org/pkg/path/filepath/#Match
func getFilesForGlobs(globs []string) ([]string, error) {
var files []string
for _, glob := range globs {
f, err := filepath.Glob(glob)
if err != nil {
return nil, err
}
files = append(files, f...)
}

return files, nil
}

// fileExistsInFiles returns true when a file exists in a slice of files. When the file doesn't exists in the slice the
// function returns false.
func fileExistsInFiles(file string, files []string) bool {
for _, f := range files {
if f == file {
return true
}
}

return false
}
43 changes: 13 additions & 30 deletions pkg/electron/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package kube

import (
"errors"
"os"
"path/filepath"
"time"

"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -37,38 +35,23 @@ type Cluster struct {
Namespace string `json:"namespace"`
}

// homeDir returns the users home directory, where the '.kube' directory is located.
// The '.kube' directory contains the configuration file for a Kubernetes cluster.
func homeDir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}

// Get the home directory on windows.
return os.Getenv("USERPROFILE")
}

// NewClient returns a new API client for a Kubernetes cluster.
// If the cluster parameter is true the client is configured inside the cluster. If the cluster parameter is false the
// client is configures outside the cluster.
func NewClient(kubeconfig string) (*Client, error) {
if kubeconfig == "" {
if os.Getenv("KUBECONFIG") == "" {
if home := homeDir(); home != "" {
kubeconfig = filepath.Join(home, ".kube", "config")
} else {
return nil, ErrConfigNotFound
}
} else {
kubeconfig = os.Getenv("KUBECONFIG")
func NewClient(kubeconfig, kubeconfigInclude, kubeconfigExclude string) (*Client, error) {
var config clientcmd.ClientConfig
var err error

if kubeconfigInclude != "" {
config, err = loadConfigFiles(kubeconfigInclude, kubeconfigExclude)
if err != nil {
return nil, err
}
} else {
config, err = loadConfigFile(kubeconfig)
if err != nil {
return nil, err
}
}

config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig},
&clientcmd.ConfigOverrides{},
)

return &Client{
config: config,
kubeconfig: kubeconfig,
Expand Down