diff --git a/cmd/kind/create/cluster/createcluster.go b/cmd/kind/create/cluster/createcluster.go index 8431d830ea..141e2e0000 100644 --- a/cmd/kind/create/cluster/createcluster.go +++ b/cmd/kind/create/cluster/createcluster.go @@ -36,6 +36,7 @@ type flagpole struct { Config string ImageName string Retain bool + Use bool Wait time.Duration } @@ -55,6 +56,7 @@ func NewCommand() *cobra.Command { cmd.Flags().StringVar(&flags.Config, "config", "", "path to a kind config file") cmd.Flags().StringVar(&flags.ImageName, "image", "", "node docker image to use for booting the cluster") cmd.Flags().BoolVar(&flags.Retain, "retain", false, "retain nodes for debugging when cluster creation fails") + cmd.Flags().BoolVar(&flags.Use, "use", false, "set KUBECONFIG to the configuration of this cluster, once it's created") cmd.Flags().DurationVar(&flags.Wait, "wait", time.Duration(0), "Wait for control plane node to be ready (default 0s)") return cmd } @@ -106,6 +108,7 @@ func runE(flags *flagpole, cmd *cobra.Command, args []string) error { if err = ctx.Create(cfg, create.Retain(flags.Retain), create.WaitForReady(flags.Wait), + create.Use(flags.Use), ); err != nil { return errors.Wrap(err, "failed to create cluster") } diff --git a/pkg/cluster/create/cluster.go b/pkg/cluster/create/cluster.go index 37449a1d44..7733efb1d3 100644 --- a/pkg/cluster/create/cluster.go +++ b/pkg/cluster/create/cluster.go @@ -41,6 +41,14 @@ func WaitForReady(interval time.Duration) ClusterOption { } } +// Use configures the cluster created to be active cluster after its creation +func Use(use bool) ClusterOption { + return func(o *internalcreate.Options) *internalcreate.Options { + o.Use = use + return o + } +} + // SetupKubernetes configures create command to setup kubernetes after creating nodes containers // TODO: Refactor this. It is a temporary solution for a phased breakdown of different // operations, specifically create. see https://github.com/kubernetes-sigs/kind/issues/324 diff --git a/pkg/cluster/internal/create/create.go b/pkg/cluster/internal/create/create.go index 855b3d47e5..3969bed1c4 100644 --- a/pkg/cluster/internal/create/create.go +++ b/pkg/cluster/internal/create/create.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/kind/pkg/cluster/internal/delete" logutil "sigs.k8s.io/kind/pkg/log" + pkgutil "k8s.io/minikube/pkg/util" configaction "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/config" "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installcni" "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/installstorage" @@ -39,6 +40,7 @@ import ( "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/kubeadmjoin" "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/loadbalancer" "sigs.k8s.io/kind/pkg/cluster/internal/create/actions/waitforready" + "sigs.k8s.io/kind/pkg/util" ) // Options holds cluster creation options @@ -46,6 +48,7 @@ import ( type Options struct { Retain bool WaitForReady time.Duration + Use bool //TODO: Refactor this. It is a temporary solution for a phased breakdown of different // operations, specifically create. see https://github.com/kubernetes-sigs/kind/issues/324 SetupKubernetes bool // if kind should setup kubernetes after creating nodes @@ -111,8 +114,51 @@ func Cluster(ctx *context.Context, cfg *config.Cluster, opts *Options) error { return nil } - // print how to set KUBECONFIG to point to the cluster etc. - printUsage(ctx.Name()) + if opts.Use { + if err := setupKubeconfig(ctx, cfg); err != nil { + return err + } + printFootnote() + } else { + // print how to set KUBECONFIG to point to the cluster etc. + printUsage(ctx.Name()) + } + + return nil +} + +func setupKubeconfig(ctx *context.Context, cfg *config.Cluster) error { + + pki := util.NewKubeConfig() + certData, err := pki.CertData(ctx.KubeConfigPath()) + if err != nil { + return err + } + + pkiPaths, err := pki.CertDataPath(certData) + if err != nil { + return err + } + + var server string + for _, v := range certData.Clusters { + server = v.Server + } + + kcs := &pkgutil.KubeConfigSetup{ + ClusterName: ctx.Name(), + ClusterServerAddress: server, + ClientCertificate: pkiPaths.ClientCertPath, + ClientKey: pkiPaths.ClientKeyPath, + CertificateAuthority: pkiPaths.CertificateAuthorityPath, + KeepContext: false, + EmbedCerts: true, + } + + kcs.SetKubeConfigFile(util.DefaultKubeconfigFile) + if err := pkgutil.SetupKubeConfig(kcs); err != nil { + return err + } return nil } @@ -154,3 +200,7 @@ func printSetupInstruction(name string) { name, ) } + +func printFootnote() { + fmt.Print("You can now use the cluster:\nkubectl cluster-info\n") +} diff --git a/pkg/cluster/nodes/node.go b/pkg/cluster/nodes/node.go index 0b44d0610f..2603afe50b 100644 --- a/pkg/cluster/nodes/node.go +++ b/pkg/cluster/nodes/node.go @@ -222,6 +222,45 @@ func (n *Node) Role() (role string, err error) { return role, nil } +// matches kubeconfig server entry like: +// server: https://172.17.0.2:6443 +// which we rewrite to: +// server: https://localhost:$PORT +var serverAddressRE = regexp.MustCompile(`^(\s+server:) https://.*:\d+$`) + +// WriteKubeConfig writes a fixed KUBECONFIG to dest +// this should only be called on a control plane node +// While copyng to the host machine the control plane address +// is replaced with local host and the control plane port with +// a randomly generated port reserved during node creation. +func (n *Node) WriteKubeConfig(dest string, hostPort int32) error { + cmd := n.Command("cat", "/etc/kubernetes/admin.conf") + lines, err := exec.CombinedOutputLines(cmd) + if err != nil { + return errors.Wrap(err, "failed to get kubeconfig from node") + } + + // fix the config file, swapping out the server for the forwarded localhost:port + var buff bytes.Buffer + for _, line := range lines { + match := serverAddressRE.FindStringSubmatch(line) + if len(match) > 1 { + line = fmt.Sprintf("%s https://localhost:%d", match[1], hostPort) + } + buff.WriteString(line) + buff.WriteString("\n") + } + + // create the directory to contain the KUBECONFIG file. + // 0755 is taken from client-go's config handling logic: https://github.com/kubernetes/client-go/blob/5d107d4ebc00ee0ea606ad7e39fd6ce4b0d9bf9e/tools/clientcmd/loader.go#L412 + err = os.MkdirAll(filepath.Dir(dest), 0755) + if err != nil { + return errors.Wrap(err, "failed to create kubeconfig output directory") + } + + return ioutil.WriteFile(dest, buff.Bytes(), 0600) +} + // WriteFile writes content to dest on the node func (n *Node) WriteFile(dest, content string) error { // create destination directory diff --git a/pkg/util/kubeconfig.go b/pkg/util/kubeconfig.go new file mode 100644 index 0000000000..82890a755d --- /dev/null +++ b/pkg/util/kubeconfig.go @@ -0,0 +1,164 @@ +package util + +import ( + "encoding/base64" + "io/ioutil" + "os" + "path" + "path/filepath" + + "gopkg.in/yaml.v2" + "k8s.io/client-go/util/homedir" +) + +// DefaultKubeconfigFile is the absolute default location of the Kubernetes Config File +var DefaultKubeconfigFile = filepath.Join(homedir.HomeDir(), ".kube", "config") + +// CertificateAuthorityPath stores the absolute path of Certificate Authority Cert and +// user's Certificate and Key +type CertificateAuthorityPath struct { + CertificateAuthorityPath, ClientKeyPath, ClientCertPath string +} + +// KubeConfig stores the Cluster and User information read from +// Kubernetes configuration file +type KubeConfig struct { + Clusters []Clusters `yaml:"clusters"` + Users []Users `yaml:"users"` +} + +// Clusters stores an item of a slice of clusters stored in a +// Kubernetes configuration file +type Clusters struct { + Name string `yaml:"name"` + Cluster struct { + CertAuthData string `yaml:"certificate-authority-data"` + Server string `yaml:"server"` + } +} + +// Users is an item of a slice of users stored in a +// Kubernetes configuration file +type Users struct { + Name string + User struct { + ClientCertData string `yaml:"client-certificate-data"` + ClientKeyData string `yaml:"client-key-data"` + } +} + +// CertificateAuthorityData is used to return both clusters and users information +type CertificateAuthorityData struct { + Clusters map[string]Cluster + Users map[string]User +} + +// Cluster is an entry containing one entry with cluster information +// CertAuthorityData holds a base64 representation of the CA Certificate +type Cluster struct { + CertAuthorityData string + Server string +} + +// User is an entry containing the user of the cluster. It contains the base64 representation +// of client certificate and key +type User struct { + ClientCertificateData string + ClientKeyData string +} + +// NewKubeConfig provides is used to store the CA information read from +// the kubeconfig file read created +func NewKubeConfig() *KubeConfig { + return &KubeConfig{} +} + +// CertDataPath reads the base64 data, stores PKI files into $HOME/.kube/ +// and returns the absolute path of where these files are +func (k *KubeConfig) CertDataPath(cad *CertificateAuthorityData) (*CertificateAuthorityPath, error) { + cdp := CertificateAuthorityPath{} + + pkiDir := filepath.Join(os.Getenv("HOME"), ".kube", k.Clusters[0].Name) + + if err := os.MkdirAll(pkiDir, 0750); err != nil { + return nil, err + } + + for _, value := range k.Clusters { + + decoded, err := base64.StdEncoding.DecodeString(value.Cluster.CertAuthData) + if err != nil { + return nil, err + } + + if err = ioutil.WriteFile(path.Join(pkiDir, "ca.crt"), decoded, 0640); err != nil { + return nil, err + } + + cdp.CertificateAuthorityPath = path.Join(pkiDir, "ca.crt") + + } + + for _, value := range k.Users { + + cdp.ClientCertPath = path.Join(pkiDir, "client-cert.crt") + decoded, err := base64.StdEncoding.DecodeString(value.User.ClientCertData) + if err != nil { + return nil, err + } + + if err = ioutil.WriteFile(cdp.ClientCertPath, decoded, 0640); err != nil { + return nil, err + } + + cdp.ClientKeyPath = path.Join(pkiDir, "client.key") + decoded, err = base64.StdEncoding.DecodeString(value.User.ClientKeyData) + if err != nil { + return nil, err + } + + if err = ioutil.WriteFile(cdp.ClientKeyPath, decoded, 0400); err != nil { + return nil, err + } + + } + + return &cdp, nil +} + +// CertData reads the base 64 certificate info from kubeconfig and return a +// a CertificateAuthorityData +func (k *KubeConfig) CertData(kubeconfigPath string) (*CertificateAuthorityData, error) { + + kconfigContent, err := ioutil.ReadFile(kubeconfigPath) + + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(kconfigContent, &k) + if err != nil { + return nil, err + } + + caData := CertificateAuthorityData{} + + for _, c := range k.Clusters { + caData.Clusters = map[string]Cluster{ + c.Name: Cluster{ + CertAuthorityData: c.Cluster.CertAuthData, + Server: c.Cluster.Server, + }, + } + } + + for _, u := range k.Users { + caData.Users = map[string]User{ + u.Name: User{ + ClientCertificateData: u.User.ClientCertData, + ClientKeyData: u.User.ClientKeyData, + }, + } + } + return &caData, nil +}