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

Kubeconfig command features update #255

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
189 changes: 154 additions & 35 deletions cmd/cluster_kubeconfig.go
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"
Copy link
Collaborator

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", like kubernetes-admin@{CLUSTER_NAME}?

Copy link
Contributor Author

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.

)

// clusterKubeconfigCmd represents the clusterKubeconfig command
Expand All @@ -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)

Expand All @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignored error

Copy link
Owner

Choose a reason for hiding this comment

The 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 != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func doConfigWrite(dst string, kubeConfig string) (err error) {
func doConfigWrite(dst string, kubeConfig string) error {


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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func doConfigCopyBackUp(src string) (err error) {
func doConfigCopyBackUp(src string) error {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this done to not assign error directly with :=
for example if you going to use:

if source, err := os.Open(src); err != nil {
		return
}

source will available only inside condition, and we want have access to it outside of condition.
if source declared, then use of := will rise error.
if to remove declaration of err from function then i need also use condition differently as:

source, err := os.Open(src)
if err != nil {
   return
}

While both variants completely good, first variant probably looks bit nicer.
But here this is how i do usually. also no need do return with exact parameters :)
If still want me to use your suggestion, let me know

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 err just before the usage (as we do for source) and return the error (eventually decorated with extra info), like:

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..).
Off course is my opinion, maybe a 3rd opinion should be useful ;) cc/ @xetys

Copy link
Owner

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

if err != nil {
    return err
}

not a problem :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mavimo @xetys check my comment later below, maybe we really need change way kubeconfig command work, instead of flag usage to print/save/merge , better to change it to sub-command and do kubeconfig save|print|merge and then each of subcommand will have own options if necessary?

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]
Expand All @@ -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)")
}
28 changes: 28 additions & 0 deletions cmd/util.go
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"
)
Expand All @@ -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, "", " ")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can replace it with

fmt.Println(Sdump(cli))

Since I suppose this is a debug utils, I'm not sure we should keep it in our codebase :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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))
}
16 changes: 15 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,22 @@ require (
github.com/go-kit/kit v0.7.0
github.com/go-logfmt/logfmt v0.3.0 // indirect
github.com/go-stack/stack v1.7.0 // indirect
github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd // indirect
github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce // indirect
github.com/hetznercloud/hcloud-go v1.12.0
github.com/imdario/mergo v0.3.7 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/json-iterator/go v1.1.5 // indirect
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect
github.com/magiconair/properties v1.8.0
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/mitchellh/go-homedir v0.0.0-20180801233206-58046073cbff
github.com/mitchellh/mapstructure v0.0.0-20180715050151-f15292f7a699 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pelletier/go-toml v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.0.0 // indirect
Expand All @@ -30,7 +36,15 @@ require (
github.com/spf13/viper v1.1.0
github.com/stretchr/testify v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20180808211826-de0752318171
golang.org/x/net v0.0.0-20190227160552-c95aed5357e7 // indirect
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 // indirect
golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c // indirect
golang.org/x/text v0.3.0 // indirect
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.2.1 // indirect
k8s.io/api v0.0.0-20190227093513-33f4ffca8693 // indirect
k8s.io/apimachinery v0.0.0-20190223094358-dcb391cde5ca // indirect
k8s.io/client-go v10.0.0+incompatible
k8s.io/klog v0.2.0 // indirect
sigs.k8s.io/yaml v1.1.0 // indirect
)
Loading