-
Notifications
You must be signed in to change notification settings - Fork 116
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
Kubeconfig command features update #255
base: master
Are you sure you want to change the base?
Changes from all commits
d1b3fd0
6b547c3
a6fb7b7
6511784
6e52b64
3cd9d68
3aab92e
fa32d08
f728fe2
417fa3b
f8e2eb0
80d342a
7de1328
2130c95
87fb548
b61dea5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,18 +1,25 @@ | ||||||
package cmd | ||||||
|
||||||
import ( | ||||||
"bufio" | ||||||
"errors" | ||||||
"fmt" | ||||||
"io" | ||||||
"io/ioutil" | ||||||
"log" | ||||||
"os" | ||||||
"os/user" | ||||||
"path" | ||||||
"strings" | ||||||
"time" | ||||||
|
||||||
"github.com/spf13/cobra" | ||||||
"github.com/xetys/hetzner-kube/pkg/clustermanager" | ||||||
"github.com/xetys/hetzner-kube/pkg/hetzner" | ||||||
"k8s.io/client-go/tools/clientcmd" | ||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" | ||||||
) | ||||||
|
||||||
const ( | ||||||
defaultContext = "kubernetes-admin@kubernetes" | ||||||
) | ||||||
|
||||||
// clusterKubeconfigCmd represents the clusterKubeconfig command | ||||||
|
@@ -21,14 +28,15 @@ var clusterKubeconfigCmd = &cobra.Command{ | |||||
Short: "setups the kubeconfig for the local machine", | ||||||
Long: `fetches the kubeconfig (e.g. for usage with kubectl) and saves it to ~/.kube/config, or prints it. | ||||||
|
||||||
Example 1: hetzner-kube cluster kubeconfig -n my-cluster # installs the kubeconfig of the cluster "my-cluster" | ||||||
Example 2: hetzner-kube cluster kubeconfig -n my-cluster -b # saves the existing before installing | ||||||
Example 3: hetzner-kube cluster kubeconfig -n my-cluster -p # prints the contents of kubeconfig to console | ||||||
Example 4: hetzner-kube cluster kubeconfig -n my-cluster -p > my-conf.yaml # prints the contents of kubeconfig into a custom file | ||||||
`, | ||||||
Example 1: hetzner-kube cluster kubeconfig my-cluster # prints the kubeconfig of the cluster "my-cluster" | ||||||
Example 2: hetzner-kube cluster kubeconfig my-cluster > my-conf.yaml # prints the contents of kubeconfig into a custom file | ||||||
Example 3: hetzner-kube cluster kubeconfig my-cluster -s -t ./my-conf.yaml # saves the contents of kubeconfig into a custom file | ||||||
Example 4: hetzner-kube cluster kubeconfig my-cluster -m # merges the existing with current cluster (creates backup before merge) | ||||||
`, | ||||||
Args: cobra.ExactArgs(1), | ||||||
PreRunE: validateKubeconfigCmd, | ||||||
Run: func(cmd *cobra.Command, args []string) { | ||||||
|
||||||
name := args[0] | ||||||
_, cluster := AppConf.Config.FindClusterByName(name) | ||||||
|
||||||
|
@@ -45,41 +53,153 @@ Example 4: hetzner-kube cluster kubeconfig -n my-cluster -p > my-conf.yaml # pri | |||||
|
||||||
FatalOnError(err) | ||||||
|
||||||
printContent, _ := cmd.Flags().GetBool("print") | ||||||
force, _ := cmd.Flags().GetBool("force") | ||||||
|
||||||
if printContent { | ||||||
fmt.Println(kubeConfigContent) | ||||||
// get sanitized kubeconfig | ||||||
// we need isSanitized flag to ensure we do not want do a merge if we fail to sanitize config | ||||||
// if it fails we simply print out config as it is | ||||||
isSanitized := false | ||||||
newKubeConfig, err := sanitizeKubeConfig(kubeConfigContent, provider.GetCluster().Name, "hetzner") | ||||||
if err != nil { | ||||||
log.Printf("KubeConfig sanitise process failed, default config will be used instead. Error: %s", err.Error()) | ||||||
} else { | ||||||
fmt.Println("create file") | ||||||
kubeConfigContent = newKubeConfig | ||||||
isSanitized = true | ||||||
} | ||||||
|
||||||
usr, _ := user.Current() | ||||||
dir := usr.HomeDir | ||||||
path := fmt.Sprintf("%s/.kube", dir) | ||||||
if merge, _ := cmd.Flags().GetBool("merge"); merge && isSanitized { | ||||||
xetys marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) { | ||||||
os.MkdirAll(path, 0755) | ||||||
if err = mergeKubeConfig(kubeConfigContent); err != nil { | ||||||
log.Fatalf("During merge we encountered the problem: %s", err.Error()) | ||||||
} | ||||||
log.Printf("KubeConfig successfully merged!") | ||||||
return | ||||||
} | ||||||
|
||||||
// check if there already is an existing config | ||||||
kubeconfigPath := fmt.Sprintf("%s/config", path) | ||||||
if _, err := os.Stat(kubeconfigPath); !force && err == nil { | ||||||
fmt.Println("There already exists a kubeconfig. Overwrite? (use -f to suppress this question) [yN]:") | ||||||
r := bufio.NewReader(os.Stdin) | ||||||
answer, err := r.ReadString('\n') | ||||||
FatalOnError(err) | ||||||
if !strings.ContainsAny(answer, "yY") { | ||||||
log.Fatalln("aborted") | ||||||
} | ||||||
} | ||||||
if save, _ := cmd.Flags().GetBool("save"); save { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignored error There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, if they are defined, no error should occur |
||||||
|
||||||
ioutil.WriteFile(kubeconfigPath, []byte(kubeConfigContent), 0755) | ||||||
targetPath := fmt.Sprintf("%s/.kube/%s.yaml", GetHome(), provider.GetCluster().Name) | ||||||
if target, _ := cmd.Flags().GetString("target"); target != "" { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignored error |
||||||
targetPath = target | ||||||
} | ||||||
log.Printf("Saving current config to '%s'", targetPath) | ||||||
doConfigWrite(targetPath, kubeConfigContent) | ||||||
|
||||||
fmt.Println("kubeconfig configured") | ||||||
return | ||||||
} | ||||||
|
||||||
fmt.Println(kubeConfigContent) | ||||||
}, | ||||||
} | ||||||
|
||||||
// Write kubeConfig to destination | ||||||
func doConfigWrite(dst string, kubeConfig string) (err error) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
if _, err := os.Stat(path.Dir(dst)); os.IsNotExist(err) { | ||||||
os.MkdirAll(path.Dir(dst), 0755) | ||||||
} | ||||||
return ioutil.WriteFile(dst, []byte(kubeConfig), 0755) | ||||||
} | ||||||
|
||||||
// Create backup of current kubeCongig | ||||||
func doConfigCopyBackUp(src string) (err error) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this done to not assign error directly with
While both variants completely good, first variant probably looks bit nicer. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JM2C: the first variant is a bit concise, but make code more hard to understand. We can declare var source, destination *os.File
var err error
if source, err = os.Open(src); err != nil {
return fmt.Errorf("unable to get source file: %v", err)
} or use the second proposal: source, err := os.Open(src)
if err != nil {
return fmt.Errorf("unable to get source file: %v", err)
} IMHO the second variant is more comprehensible when someone look the code for the first time (new contributors, ppl that try to investigate an issue, the user that write the code after few weeks that write the code..). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the second more, as I'm not a fan of C style declaring variables in the header and than doing the code later. The second one is far better to read and understand, and an actual goal of go to make it looking like this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it depends, as i said, all variants is valid in go. but i can agree , i'm always judging from my 5+ experience in go, and for me personally read some-one code in this scope absolutely transparent. but if majority vote for
not a problem :D There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
var source, destination *os.File | ||||||
if source, err = os.Open(src); err != nil { | ||||||
return | ||||||
} | ||||||
defer source.Close() | ||||||
|
||||||
dst := fmt.Sprintf("%s/config.%s", path.Dir(src), time.Now().Format("20060102150405")) | ||||||
if destination, err = os.Create(dst); err != nil { | ||||||
return | ||||||
} | ||||||
defer destination.Close() | ||||||
|
||||||
_, err = io.Copy(destination, source) | ||||||
log.Printf("KubeConfig backup save as '%s'", dst) | ||||||
return | ||||||
} | ||||||
|
||||||
func mergeKubeConfig(kubeConfig string) error { | ||||||
|
||||||
// check if main kubeConfig exists and backup it | ||||||
kubeConfigPath := fmt.Sprintf("%s/.kube/config", GetHome()) | ||||||
if _, err := os.Stat(kubeConfigPath); err == nil { | ||||||
doConfigCopyBackUp(kubeConfigPath) | ||||||
} | ||||||
|
||||||
// Read kubeconfig to k8s config structure | ||||||
apiCfg, err := clientcmd.Load([]byte(kubeConfig)) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
// Create Temporary file we going to use for configs merge and write config to it | ||||||
clusterKubeConfigTmp, err := ioutil.TempFile("", "") | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
defer os.Remove(clusterKubeConfigTmp.Name()) | ||||||
clientcmd.WriteToFile(*apiCfg, clusterKubeConfigTmp.Name()) | ||||||
|
||||||
// initialize Loading rules and add Real config and our tmp config as target for merge | ||||||
// Even if `kubeConfigPath` we do not care, it will be ignored during ClientConfigLoadingRules.Load() | ||||||
loadingRules := clientcmd.ClientConfigLoadingRules{ | ||||||
Precedence: []string{ | ||||||
kubeConfigPath, | ||||||
clusterKubeConfigTmp.Name(), | ||||||
}, | ||||||
} | ||||||
mergedConfig, err := loadingRules.Load() | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
return clientcmd.WriteToFile(*mergedConfig, kubeConfigPath) | ||||||
} | ||||||
|
||||||
func sanitizeKubeConfig(kubeConfig string, clusterName string, prefix string) (string, error) { | ||||||
|
||||||
// Read kubeconfig to k8s config structure | ||||||
apiCfg, err := clientcmd.Load([]byte(kubeConfig)) | ||||||
if err != nil { | ||||||
return "", err | ||||||
} | ||||||
|
||||||
// get our default Context from configuration (check `const` section) | ||||||
var ctx *clientcmdapi.Context | ||||||
if ctx = apiCfg.Contexts[defaultContext]; ctx == nil { | ||||||
return "", fmt.Errorf("default context '%s' does not found in current configuration", defaultContext) | ||||||
} | ||||||
|
||||||
// Apply prefix if it set | ||||||
if prefix != "" { | ||||||
clusterName = fmt.Sprintf("%s-%s", prefix, clusterName) | ||||||
} | ||||||
|
||||||
// save current cluster name and authInfo Names | ||||||
currentCluster := ctx.Cluster | ||||||
currentAuthInfo := ctx.AuthInfo | ||||||
|
||||||
// define new Cluster and AuthInfo Names as Project Name | ||||||
ctx.Cluster = clusterName | ||||||
ctx.AuthInfo = clusterName | ||||||
|
||||||
// Copy current data about Context,Cluster,authInfo with new Names | ||||||
apiCfg.Contexts[clusterName] = ctx | ||||||
apiCfg.Clusters[clusterName] = apiCfg.Clusters[currentCluster] | ||||||
apiCfg.AuthInfos[clusterName] = apiCfg.AuthInfos[currentAuthInfo] | ||||||
apiCfg.CurrentContext = clusterName | ||||||
|
||||||
// Remove outdaited details | ||||||
delete(apiCfg.Clusters, currentCluster) | ||||||
delete(apiCfg.AuthInfos, currentAuthInfo) | ||||||
delete(apiCfg.Contexts, defaultContext) | ||||||
|
||||||
configByte, err := clientcmd.Write(*apiCfg) | ||||||
if err != nil { | ||||||
return "", err | ||||||
} | ||||||
return string(configByte), nil | ||||||
} | ||||||
|
||||||
func validateKubeconfigCmd(cmd *cobra.Command, args []string) error { | ||||||
|
||||||
name := args[0] | ||||||
|
@@ -99,8 +219,7 @@ func validateKubeconfigCmd(cmd *cobra.Command, args []string) error { | |||||
func init() { | ||||||
clusterCmd.AddCommand(clusterKubeconfigCmd) | ||||||
|
||||||
clusterKubeconfigCmd.Flags().StringP("name", "n", "", "name of the cluster") | ||||||
clusterKubeconfigCmd.Flags().BoolP("print", "p", false, "prints output to stdout") | ||||||
clusterKubeconfigCmd.Flags().BoolP("backup", "b", false, "saves existing config") | ||||||
clusterKubeconfigCmd.Flags().BoolP("force", "f", false, "don't ask to overwrite") | ||||||
clusterKubeconfigCmd.Flags().BoolP("merge", "m", false, "merges .kube/config with my-cluster config") | ||||||
clusterKubeconfigCmd.Flags().BoolP("save", "s", false, "saves current config to target location, requires set `--target| -t`") | ||||||
clusterKubeconfigCmd.Flags().StringP("target", "t", "", "saves current config to target location (if not set, default to ~/.kube/my-cluster-config)") | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,10 @@ | ||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"log" | ||
"os/user" | ||
|
||
"github.com/Pallinder/go-randomdata" | ||
) | ||
|
@@ -17,3 +19,29 @@ func FatalOnError(err error) { | |
log.Fatal(err) | ||
} | ||
} | ||
|
||
// GetHome is an helper function to get current home directory | ||
func GetHome() string { | ||
usr, _ := user.Current() | ||
return usr.HomeDir | ||
} | ||
|
||
// Dump is handy dumps of structure as json (almost any structures) | ||
func Dump(cls interface{}) { | ||
data, err := json.MarshalIndent(cls, "", " ") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can replace it with
Since I suppose this is a debug utils, I'm not sure we should keep it in our codebase :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this is debug tools, so can be removed before merge. :) |
||
if err != nil { | ||
log.Println("[ERROR] Oh no! There was an error on Dump command: ", err) | ||
return | ||
} | ||
fmt.Println(string(data)) | ||
} | ||
|
||
// Sdump is same as Dump, only output to a string | ||
func Sdump(cls interface{}) string { | ||
data, err := json.MarshalIndent(cls, "", " ") | ||
if err != nil { | ||
log.Println("[ERROR] Oh no! There was an error on Dump command: ", err) | ||
return "" | ||
} | ||
return fmt.Sprintln(string(data)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure on why we are defining this
defaultContext
, maybe context should be "cluster specific", likekubernetes-admin@{CLUSTER_NAME}
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not what we going to set, this is what we looking to rename. regardless cluster, this context set by default during kubebootstrap, so all our configs with this context. maybe i should change name to
default_pattern
then.