diff --git a/upup/cmd/upup/kubecfg.go b/upup/cmd/upup/kubecfg.go new file mode 100644 index 0000000000..2877f69390 --- /dev/null +++ b/upup/cmd/upup/kubecfg.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// kubecfgCmd represents the kubecfg command +var kubecfgCmd = &cobra.Command{ + Use: "kubecfg", + Short: "Manage kubecfg files", + Long: `Manage kubecfg files`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Usage: generate") + }, +} + +func init() { + RootCmd.AddCommand(kubecfgCmd) +} diff --git a/upup/cmd/upup/kubecfg_generate.go b/upup/cmd/upup/kubecfg_generate.go new file mode 100644 index 0000000000..b57c561c4b --- /dev/null +++ b/upup/cmd/upup/kubecfg_generate.go @@ -0,0 +1,158 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + "io" + "io/ioutil" + "k8s.io/kube-deploy/upup/pkg/fi" + "k8s.io/kube-deploy/upup/pkg/kubecfg" + "os" + "path" +) + +type KubecfgGenerateCommand struct { + StateDir string + ClusterName string + CloudProvider string + Project string + Master string + + tmpdir string + caStore fi.CAStore +} + +var kubecfgGenerateCommand KubecfgGenerateCommand + +func init() { + cmd := &cobra.Command{ + Use: "generate", + Short: "Generate a kubecfg file for a cluster", + Long: `Creates a kubecfg file for a cluster, based on the state`, + Run: func(cmd *cobra.Command, args []string) { + err := kubecfgGenerateCommand.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + }, + } + + kubecfgCmd.AddCommand(cmd) + + // TODO: We need to store this in the persistent state dir + cmd.Flags().StringVarP(&kubecfgGenerateCommand.ClusterName, "name", "", kubecfgGenerateCommand.ClusterName, "Name for cluster") + cmd.Flags().StringVarP(&kubecfgGenerateCommand.CloudProvider, "cloud", "", kubecfgGenerateCommand.CloudProvider, "Cloud provider to use - gce, aws") + cmd.Flags().StringVarP(&kubecfgGenerateCommand.Project, "project", "", kubecfgGenerateCommand.Project, "Project to use (must be set on GCE)") + + cmd.Flags().StringVarP(&kubecfgGenerateCommand.Master, "master", "", kubecfgGenerateCommand.Master, "IP adddress or host of API server") + + cmd.Flags().StringVarP(&kubecfgGenerateCommand.StateDir, "state", "", "", "State directory") +} + +func (c *KubecfgGenerateCommand) Run() error { + if c.StateDir == "" { + return fmt.Errorf("state must be specified") + } + + if c.Master == "" { + return fmt.Errorf("master must be specified") + } + + if c.ClusterName == "" { + return fmt.Errorf("name must be specified") + } + if c.CloudProvider == "" { + return fmt.Errorf("cloud must be specified") + } + + var err error + c.tmpdir, err = ioutil.TempDir("", "k8s") + if err != nil { + return fmt.Errorf("error creating temporary directory: %v", err) + } + defer os.RemoveAll(c.tmpdir) + + b := &kubecfg.KubeconfigBuilder{} + b.Init() + + switch c.CloudProvider { + case "aws": + b.Context = "aws_" + c.ClusterName + + case "gce": + if c.Project == "" { + return fmt.Errorf("--project must be specified (for GCE)") + } + b.Context = c.Project + "_" + c.ClusterName + + default: + return fmt.Errorf("Unknown cloud provider %q", c.CloudProvider) + } + + c.caStore, err = fi.NewFilesystemCAStore(path.Join(c.StateDir, "pki")) + if err != nil { + return fmt.Errorf("error building CA store: %v", err) + } + + if b.CACert, err = c.copyCertificate(fi.CertificateId_CA); err != nil { + return err + } + + if b.KubecfgCert, err = c.copyCertificate("kubecfg"); err != nil { + return err + } + + if b.KubecfgKey, err = c.copyPrivateKey("kubecfg"); err != nil { + return err + } + + b.KubeMasterIP = c.Master + + err = b.CreateKubeconfig() + if err != nil { + return err + } + + return nil +} + +func (c *KubecfgGenerateCommand) copyCertificate(id string) (string, error) { + p := path.Join(c.tmpdir, id+".crt") + cert, err := c.caStore.Cert(id) + if err != nil { + return "", fmt.Errorf("error fetching certificate %q: %v", id, err) + } + + _, err = writeFile(p, cert) + if err != nil { + return "", fmt.Errorf("error writing certificate %q: %v", id, err) + } + + return p, nil +} + +func (c *KubecfgGenerateCommand) copyPrivateKey(id string) (string, error) { + p := path.Join(c.tmpdir, id+".key") + cert, err := c.caStore.PrivateKey(id) + if err != nil { + return "", fmt.Errorf("error fetching private key %q: %v", id, err) + } + + _, err = writeFile(p, cert) + if err != nil { + return "", fmt.Errorf("error writing private key %q: %v", id, err) + } + + return p, nil +} + +func writeFile(dst string, src io.WriterTo) (int64, error) { + f, err := os.Create(dst) + if err != nil { + return 0, fmt.Errorf("error creating file %q: %v", dst, err) + } + defer fi.SafeClose(f) + return src.WriteTo(f) +} diff --git a/upup/cmd/upup/main.go b/upup/cmd/upup/main.go new file mode 100644 index 0000000000..736ef31021 --- /dev/null +++ b/upup/cmd/upup/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + Execute() +} diff --git a/upup/cmd/upup/root.go b/upup/cmd/upup/root.go new file mode 100644 index 0000000000..c4500c03cc --- /dev/null +++ b/upup/cmd/upup/root.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var cfgFile string + +// This represents the base command when called without any subcommands +var RootCmd = &cobra.Command{ + Use: "upup", + Short: "upup manages kubernetes clusters", + Long: `upup manages kubernetes clusters. +It allows you to create, destroy, upgrade and maintain them.`, +} + +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.upup.yaml)") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { // enable ability to specify config file via flag + viper.SetConfigFile(cfgFile) + } + + viper.SetConfigName(".upup") // name of config file (without extension) + viper.AddConfigPath("$HOME") // adding home directory as first search path + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Println("Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/upup/glide.lock b/upup/glide.lock index a74d80e51f..caadc50f1a 100644 --- a/upup/glide.lock +++ b/upup/glide.lock @@ -1,8 +1,8 @@ -hash: 9864d4fe0b94042cab08d2e80910638776d3d1e655b5a3cb9006731ae0306761 -updated: 2016-05-13T09:50:25.860201305-04:00 +hash: a9f93fedfb45e32b891cdf1f53c475acff2dd80fc02577b7c1d4da4e099bc450 +updated: 2016-05-17T12:58:07.23069873-04:00 imports: - name: github.com/aws/aws-sdk-go - version: d85fa529a99a833067e11c0a838b9db7a5d5ea71 + version: bf2f8fe7f45e68017086d069498638893feddf64 subpackages: - aws - aws/awserr @@ -29,27 +29,60 @@ imports: - private/protocol/rest - aws/credentials/ec2rolecreds - aws/ec2metadata +- name: github.com/BurntSushi/toml + version: f0aeabca5a127c4078abb8c8d64298b147264b55 - name: github.com/cloudfoundry-incubator/candiedyaml version: 99c3df83b51532e3615f851d8c2dbb638f5313bf +- name: github.com/fsnotify/fsnotify + version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8 - name: github.com/ghodss/yaml version: e8e0db9016175449df0e9c4b6e6995a9433a395c - name: github.com/go-ini/ini - version: 12f418cc7edc5a618a51407b7ac1f1f512139df3 + version: 2e44421e256d82ebbf3d4d4fcabe8930b905eff3 - name: github.com/golang/glog version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 - name: github.com/golang/protobuf - version: 7cc19b78d562895b13596ddce7aafb59dd789318 + version: b982704f8bb716bb608144408cff30e15fbde841 subpackages: - proto +- name: github.com/hashicorp/hcl + version: 9a905a34e6280ce905da1a32344b25e81011197a + subpackages: + - hcl/ast + - hcl/parser + - hcl/token + - json/parser + - hcl/scanner + - hcl/strconv + - json/scanner + - json/token +- name: github.com/inconshreveable/mousetrap + version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 - name: github.com/jmespath/go-jmespath - version: 0b12d6b521d83fc7f755e7cfc1b1fbdd35a01a74 + version: 3433f3ea46d9f8019119e7dd41274e112a2359a9 +- name: github.com/magiconair/properties + version: c265cfa48dda6474e208715ca93e987829f572f8 +- name: github.com/mitchellh/mapstructure + version: d2dd0262208475919e1a362f675cfc0e7c10e905 +- name: github.com/spf13/cast + version: 27b586b42e29bec072fe7379259cc719e1289da6 +- name: github.com/spf13/cobra + version: 0f866a6211e33cde2091d9290c08f6afd6c9ebbc +- name: github.com/spf13/jwalterweatherman + version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 +- name: github.com/spf13/pflag + version: cb88ea77998c3f024757528e3305022ab50b43be +- name: github.com/spf13/viper + version: d8a428b8a30606e1d0b355d91edf282609ade1a6 - name: golang.org/x/crypto - version: 47ff8dfbc528fea3003fc0ce2d88ffbbfbc46a43 + version: c84e1f8e3a7e322d497cd16c0e8a13c7e127baf3 subpackages: - ssh - curve25519 + - ed25519 + - ed25519/internal/edwards25519 - name: golang.org/x/net - version: 7e42c0e1329bb108f7376a7618a2871ab90f1c4d + version: ef00b378c73f107bf44d5c9b69875255ce89b79a subpackages: - context - context/ctxhttp @@ -60,8 +93,12 @@ imports: - internal - jws - jwt +- name: golang.org/x/sys + version: 833a04a10549a95dc34458c195cbad61bbb6cb4d + subpackages: + - unix - name: google.golang.org/api - version: f9a4669e07732c84854dce1f5c451c22427228fb + version: b34a26664e9b96e9d4aab8a6e8175ea07af5b8b6 subpackages: - compute/v1 - googleapi @@ -81,14 +118,16 @@ imports: - internal/log - internal/remote_api - name: google.golang.org/cloud - version: 200292f09e3aaa34878d801ab71fe823b1f7d36a + version: eb47ba841d53d93506cfbfbc03927daf9cc48f88 subpackages: - compute/metadata - internal - name: google.golang.org/grpc - version: 9604a2bb7dd81d87c2873a9580258465f3c311c8 + version: e802f420af1fde2c7e456cec047c51fbeff2d8fc +- name: gopkg.in/yaml.v2 + version: a83829b6f1293c91addabc89d0571c246397bbf4 - name: k8s.io/kubernetes - version: bb3f5b1768f3bf6c81914c5bb1d7e846561fdc31 + version: a24f03c3c99bd305ace7745b7a5749790be060e3 subpackages: - pkg/util/exec - pkg/util/mount diff --git a/upup/glide.yaml b/upup/glide.yaml index 80dd214fd2..0f7dc97b53 100644 --- a/upup/glide.yaml +++ b/upup/glide.yaml @@ -22,3 +22,4 @@ import: subpackages: - ssh - package: github.com/cloudfoundry-incubator/candiedyaml +- package: github.com/spf13/cobra diff --git a/upup/pkg/fi/ca.go b/upup/pkg/fi/ca.go index 3926a9d575..c9a77f7a15 100644 --- a/upup/pkg/fi/ca.go +++ b/upup/pkg/fi/ca.go @@ -17,6 +17,8 @@ import ( "time" ) +const CertificateId_CA = "ca" + type Certificate struct { Subject pkix.Name IsCA bool @@ -45,7 +47,7 @@ func (c *Certificate) UnmarshalJSON(b []byte) error { func (c *Certificate) MarshalJSON() ([]byte, error) { var data bytes.Buffer - err := c.WriteCertificate(&data) + _, err := c.WriteTo(&data) if err != nil { return nil, fmt.Errorf("error writing SSL certificate: %v", err) } @@ -70,7 +72,7 @@ func (c *Certificate) AsString() (string, error) { } var data bytes.Buffer - err := c.WriteCertificate(&data) + _, err := c.WriteTo(&data) if err != nil { return "", fmt.Errorf("error writing SSL certificate: %v", err) } @@ -88,7 +90,7 @@ func (c *PrivateKey) AsString() (string, error) { } var data bytes.Buffer - err := WritePrivateKey(c.Key, &data) + _, err := c.WriteTo(&data) if err != nil { return "", fmt.Errorf("error writing SSL private key: %v", err) } @@ -114,13 +116,33 @@ func (k *PrivateKey) UnmarshalJSON(b []byte) (err error) { func (k *PrivateKey) MarshalJSON() ([]byte, error) { var data bytes.Buffer - err := WritePrivateKey(k.Key, &data) + _, err := k.WriteTo(&data) if err != nil { return nil, fmt.Errorf("error writing SSL private key: %v", err) } return json.Marshal(data.String()) } +var _ io.WriterTo = &PrivateKey{} + +func (k *PrivateKey) WriteTo(w io.Writer) (int64, error) { + var data bytes.Buffer + var err error + + switch pk := k.Key.(type) { + case *rsa.PrivateKey: + err = pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)}) + default: + return 0, fmt.Errorf("unknown private key type: %T", k.Key) + } + + if err != nil { + return 0, fmt.Errorf("error writing SSL private key: %v", err) + } + + return data.WriteTo(w) +} + func LoadPEMCertificate(pemData []byte) (*Certificate, error) { cert, err := parsePEMCertificate(pemData) if err != nil { @@ -199,8 +221,15 @@ func SignNewCertificate(privateKey *PrivateKey, template *x509.Certificate, sign return c, nil } -func (c *Certificate) WriteCertificate(w io.Writer) error { - return pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: c.Certificate.Raw}) +var _ io.WriterTo = &Certificate{} + +func (c *Certificate) WriteTo(w io.Writer) (int64, error) { + var b bytes.Buffer + err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: c.Certificate.Raw}) + if err != nil { + return 0, err + } + return b.WriteTo(w) } func parsePEMCertificate(pemData []byte) (*x509.Certificate, error) { @@ -221,15 +250,6 @@ func parsePEMCertificate(pemData []byte) (*x509.Certificate, error) { } } -func WritePrivateKey(privateKey crypto.PrivateKey, w io.Writer) error { - rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey) - if ok { - return pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivateKey)}) - } - - return fmt.Errorf("unknown private key type: %T", privateKey) -} - func parsePEMPrivateKey(pemData []byte) (crypto.PrivateKey, error) { for { block, rest := pem.Decode(pemData) diff --git a/upup/pkg/fi/fs_castore.go b/upup/pkg/fi/fs_castore.go index 202d385711..62cdff1b74 100644 --- a/upup/pkg/fi/fs_castore.go +++ b/upup/pkg/fi/fs_castore.go @@ -2,7 +2,6 @@ package fi import ( "bytes" - "crypto" crypto_rand "crypto/rand" "crypto/rsa" "crypto/x509" @@ -72,12 +71,14 @@ func (c *FilesystemCAStore) generateCACertificate() error { IsCA: true, } - caPrivateKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) + caRsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) if err != nil { return fmt.Errorf("error generating RSA private key: %v", err) } - caCertificate, err := SignNewCertificate(&PrivateKey{Key: caPrivateKey}, template, nil, nil) + caPrivateKey := &PrivateKey{Key: caRsaKey} + + caCertificate, err := SignNewCertificate(caPrivateKey, template, nil, nil) if err != nil { return err } @@ -100,7 +101,7 @@ func (c *FilesystemCAStore) generateCACertificate() error { return err } - c.caPrivateKey = &PrivateKey{Key: caPrivateKey} + c.caPrivateKey = caPrivateKey c.caCertificate = caCertificate return nil } @@ -176,7 +177,7 @@ func (c *FilesystemCAStore) Cert(id string) (*Certificate, error) { func (c *FilesystemCAStore) FindCert(id string) (*Certificate, error) { var cert *Certificate - if id == "ca" { + if id == CertificateId_CA { cert = c.caCertificate } else { var err error @@ -228,7 +229,7 @@ func (c *FilesystemCAStore) loadPrivateKey(p string) (*PrivateKey, error) { func (c *FilesystemCAStore) FindPrivateKey(id string) (*PrivateKey, error) { var key *PrivateKey - if id == "ca" { + if id == CertificateId_CA { key = c.caPrivateKey } else { var err error @@ -253,22 +254,23 @@ func (c *FilesystemCAStore) PrivateKey(id string) (*PrivateKey, error) { func (c *FilesystemCAStore) CreatePrivateKey(id string) (*PrivateKey, error) { p := c.buildPrivateKeyPath(id) - privateKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) + rsaKey, err := rsa.GenerateKey(crypto_rand.Reader, 2048) if err != nil { return nil, fmt.Errorf("error generating RSA private key: %v", err) } + privateKey := &PrivateKey{Key: rsaKey} err = c.storePrivateKey(privateKey, p) if err != nil { return nil, err } - return &PrivateKey{Key: privateKey}, nil + return privateKey, nil } -func (c *FilesystemCAStore) storePrivateKey(privateKey crypto.PrivateKey, p string) error { +func (c *FilesystemCAStore) storePrivateKey(privateKey *PrivateKey, p string) error { var data bytes.Buffer - err := WritePrivateKey(privateKey, &data) + _, err := privateKey.WriteTo(&data) if err != nil { return err } @@ -277,8 +279,9 @@ func (c *FilesystemCAStore) storePrivateKey(privateKey crypto.PrivateKey, p stri } func (c *FilesystemCAStore) storeCertificate(cert *Certificate, p string) error { + // TODO: replace storePrivateKey & storeCertificate with writeFile(io.WriterTo)? var data bytes.Buffer - err := cert.WriteCertificate(&data) + _, err := cert.WriteTo(&data) if err != nil { return err } diff --git a/upup/pkg/kubecfg/kubecfg_builder.go b/upup/pkg/kubecfg/kubecfg_builder.go new file mode 100644 index 0000000000..9d2eb6b84f --- /dev/null +++ b/upup/pkg/kubecfg/kubecfg_builder.go @@ -0,0 +1,136 @@ +package kubecfg + +import ( + "fmt" + "github.com/golang/glog" + "os" + "os/exec" + "path" + "strings" +) + +// KubeconfigBuilder builds a kubecfg file +// This logic previously lives in the bash scripts (create-kubeconfig in cluster/common.sh) +type KubeconfigBuilder struct { + KubectlPath string + KubeconfigPath string + + KubeMasterIP string + + Context string + + KubeBearerToken string + KubeUser string + KubePassword string + + CACert string + KubecfgCert string + KubecfgKey string +} + +func (c *KubeconfigBuilder) Init() { + c.KubectlPath = "kubectl" // default to in-path + + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + homedir := os.Getenv("HOME") + kubeconfig = path.Join(homedir, ".kube", "config") + } + c.KubeconfigPath = kubeconfig +} + +func (c *KubeconfigBuilder) CreateKubeconfig() error { + if _, err := os.Stat(c.KubeconfigPath); os.IsNotExist(err) { + err := os.MkdirAll(path.Dir(c.KubeconfigPath), 0700) + if err != nil { + return fmt.Errorf("error creating directories for %q: %v", c.KubeconfigPath, err) + } + f, err := os.OpenFile(c.KubeconfigPath, os.O_RDWR|os.O_CREATE, 0600) + if err != nil { + return fmt.Errorf("error creating config file %q: %v", c.KubeconfigPath, err) + } + f.Close() + } + + var clusterArgs []string + + clusterArgs = append(clusterArgs, "--server=https://"+c.KubeMasterIP) + + if c.CACert == "" { + clusterArgs = append(clusterArgs, "--insecure-skip-tls-verify=true") + } else { + clusterArgs = append(clusterArgs, "--certificate-authority="+c.CACert) + clusterArgs = append(clusterArgs, "--embed-certs=true") + } + + var userArgs []string + + if c.KubeBearerToken != "" { + userArgs = append(userArgs, "--token="+c.KubeBearerToken) + } else if c.KubeUser != "" && c.KubePassword != "" { + userArgs = append(userArgs, "--username="+c.KubeUser) + userArgs = append(userArgs, "--password="+c.KubePassword) + } + + if c.KubecfgCert != "" && c.KubecfgKey != "" { + userArgs = append(userArgs, "--client-certificate="+c.KubecfgCert) + userArgs = append(userArgs, "--client-key="+c.KubecfgKey) + userArgs = append(userArgs, "--embed-certs=true") + } + + setClusterArgs := []string{"config", "set-cluster", c.Context} + setClusterArgs = append(setClusterArgs, clusterArgs...) + err := c.execKubectl(setClusterArgs...) + if err != nil { + return err + } + + if len(userArgs) != 0 { + setCredentialsArgs := []string{"config", "set-credentials", c.Context} + setCredentialsArgs = append(setCredentialsArgs, userArgs...) + err := c.execKubectl(setCredentialsArgs...) + if err != nil { + return err + } + } + + err = c.execKubectl("config", "set-context", c.Context, "--cluster="+c.Context, "--user="+c.Context) + if err != nil { + return err + } + err = c.execKubectl("config", "use-context", c.Context, "--cluster="+c.Context, "--user="+c.Context) + if err != nil { + return err + } + + // If we have a bearer token, also create a credential entry with basic auth + // so that it is easy to discover the basic auth password for your cluster + // to use in a web browser. + if c.KubeBearerToken != "" && c.KubeUser != "" && c.KubePassword != "" { + err := c.execKubectl("config", "set-credentials", c.Context+"-basic-auth", "--username="+c.KubeUser, "--password="+c.KubePassword) + if err != nil { + return err + } + } + + fmt.Printf("Wrote config for %s to %q\n", c.Context, c.KubeconfigPath) + return nil +} + +func (c *KubeconfigBuilder) execKubectl(args ...string) error { + cmd := exec.Command(c.KubectlPath, args...) + env := os.Environ() + env = append(env, fmt.Sprintf("KUBECONFIG=%s", c.KubeconfigPath)) + cmd.Env = env + + glog.V(2).Infof("Running command: %s %s", cmd.Path, strings.Join(cmd.Args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + glog.Info("error running kubectl:") + glog.Info(string(output)) + return fmt.Errorf("error running kubectl") + } + + glog.V(2).Info(string(output)) + return nil +}