diff --git a/cmd/kops/create.go b/cmd/kops/create.go index f25a6d37de..7a86de24ee 100644 --- a/cmd/kops/create.go +++ b/cmd/kops/create.go @@ -7,8 +7,8 @@ import ( // createCmd represents the create command var createCmd = &cobra.Command{ Use: "create", - Short: "create clusters", - Long: `Create clusters`, + Short: "create resources", + Long: `Create resources`, } func init() { diff --git a/cmd/kops/create_secret.go b/cmd/kops/create_secret.go new file mode 100644 index 0000000000..b5d6cd8536 --- /dev/null +++ b/cmd/kops/create_secret.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +type CreateSecretCommand struct { + cobraCommand *cobra.Command +} + +var createSecretCmd = CreateSecretCommand{ + cobraCommand: &cobra.Command{ + Use: "secret", + Short: "Create secrets", + Long: `Create secrets.`, + }, +} + +func init() { + cmd := createSecretCmd.cobraCommand + + createCmd.AddCommand(cmd) +} diff --git a/cmd/kops/create_secret_sshpublickey.go b/cmd/kops/create_secret_sshpublickey.go new file mode 100644 index 0000000000..5c9fd67a2f --- /dev/null +++ b/cmd/kops/create_secret_sshpublickey.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + "io/ioutil" +) + +type CreateSecretPublickeyCommand struct { + cobraCommand *cobra.Command + + Pubkey string +} + +var createSecretPublickeyCommand = CreateSecretPublickeyCommand{ + cobraCommand: &cobra.Command{ + Use: "sshpublickey", + Short: "Create SSH publickey", + Long: `Create SSH publickey.`, + }, +} + +func init() { + cmd := createSecretPublickeyCommand.cobraCommand + + cmd.Run = func(cmd *cobra.Command, args []string) { + err := createSecretPublickeyCommand.Run(args) + if err != nil { + exitWithError(err) + } + } + + cmd.Flags().StringVarP(&createSecretPublickeyCommand.Pubkey, "pubkey", "i", "", "Path to SSH public key") + + createSecretCmd.cobraCommand.AddCommand(cmd) +} + +func (cmd *CreateSecretPublickeyCommand) Run(args []string) error { + if len(args) == 0 { + return fmt.Errorf("syntax: NAME -i ") + } + if len(args) != 1 { + return fmt.Errorf("syntax: NAME -i ") + } + name := args[0] + + if cmd.Pubkey == "" { + return fmt.Errorf("pubkey path is required (use -i)") + } + + caStore, err := rootCommand.KeyStore() + if err != nil { + return err + } + + data, err := ioutil.ReadFile(cmd.Pubkey) + if err != nil { + return fmt.Errorf("error reading SSH public key %v: %v", cmd.Pubkey, err) + } + + err = caStore.AddSSHPublicKey(name, data) + if err != nil { + return fmt.Errorf("error adding SSH public key: %v", err) + } + + return nil +} diff --git a/cmd/kops/create_secret_tls.go b/cmd/kops/create_secret_tls.go new file mode 100644 index 0000000000..f16b780bb5 --- /dev/null +++ b/cmd/kops/create_secret_tls.go @@ -0,0 +1,153 @@ +package main + +// +//import ( +// "fmt" +// +// "crypto/x509" +// "github.com/golang/glog" +// "github.com/spf13/cobra" +// "k8s.io/kops/upup/pkg/fi" +// "net" +// "strings" +//) +// +//type CreateSecretsCommand struct { +// Id string +// Type string +// +// Usage string +// Subject string +// AlternateNames []string +//} +// +//var createSecretsCommand CreateSecretsCommand +// +//func init() { +// cmd := &cobra.Command{ +// Use: "secret", +// Short: "Create secrets", +// Long: `Create secrets.`, +// Run: func(cmd *cobra.Command, args []string) { +// err := createSecretsCommand.Run() +// if err != nil { +// exitWithError(err) +// } +// }, +// } +// +// createCmd.AddCommand(cmd) +// +// cmd.Flags().StringVarP(&createSecretsCommand.Type, "type", "", "", "Type of secret to create") +// cmd.Flags().StringVarP(&createSecretsCommand.Id, "id", "", "", "Id of secret to create") +// cmd.Flags().StringVarP(&createSecretsCommand.Usage, "usage", "", "", "Usage of secret (for SSL certificate)") +// cmd.Flags().StringVarP(&createSecretsCommand.Subject, "subject", "", "", "Subject (for SSL certificate)") +// cmd.Flags().StringSliceVarP(&createSecretsCommand.AlternateNames, "san", "", nil, "Alternate name (for SSL certificate)") +//} +// +//func (cmd *CreateSecretsCommand) Run() error { +// if cmd.Id == "" { +// return fmt.Errorf("id is required") +// } +// +// if cmd.Type == "" { +// return fmt.Errorf("type is required") +// } +// +// // TODO: Prompt before replacing? +// // TODO: Keep history? +// +// if strings.ToLower(cmd.Type) == strings.ToLower(fi.SecretTypeSecret) { +// return fmt.Errorf("creating secrets of type %q not (currently) supported", cmd.Type) +// //{ +// // secretStore, err := rootCommand.SecretStore() +// // if err != nil { +// // return err +// // } +// // secret, err := fi.CreateSecret() +// // if err != nil { +// // return fmt.Errorf("error creating secret: %v", err) +// // } +// // _, created, err := secretStore.GetOrCreateSecret(cmd.Id, secret) +// // if err != nil { +// // return fmt.Errorf("error creating secret: %v", err) +// // } +// // if !created { +// // return fmt.Errorf("secret already exists") +// // } +// // return nil +// //} +// } +// +// if strings.ToLower(cmd.Type) == strings.ToLower(fi.SecretTypeKeypair) { +// return fmt.Errorf("creating secrets of type %q not (currently) supported", cmd.Type) +// // +// //// TODO: Create a rotate command which keeps the same values? +// //// Or just do it here a "replace" action - existing=fail, replace or rotate +// //// TODO: Create a CreateKeypair class, move to fi (this is duplicated code) +// //{ +// // if cmd.Subject == "" { +// // return fmt.Errorf("subject is required") +// // } +// // +// // subject, err := parsePkixName(cmd.Subject) +// // if err != nil { +// // return fmt.Errorf("Error parsing subject: %v", err) +// // } +// // template := &x509.Certificate{ +// // Subject: *subject, +// // BasicConstraintsValid: true, +// // IsCA: false, +// // } +// // +// // if len(template.Subject.ToRDNSequence()) == 0 { +// // return fmt.Errorf("Subject name was empty") +// // } +// // +// // switch cmd.Usage { +// // case "client": +// // template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} +// // template.KeyUsage = x509.KeyUsageDigitalSignature +// // break +// // +// // case "server": +// // template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} +// // template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment +// // break +// // +// // default: +// // return fmt.Errorf("unknown usage: %q", cmd.Usage) +// // } +// // +// // for _, san := range cmd.AlternateNames { +// // san = strings.TrimSpace(san) +// // if san == "" { +// // continue +// // } +// // if ip := net.ParseIP(san); ip != nil { +// // template.IPAddresses = append(template.IPAddresses, ip) +// // } else { +// // template.DNSNames = append(template.DNSNames, san) +// // } +// // } +// // +// // caStore, err := rootCommand.KeyStore() +// // if err != nil { +// // return err +// // } +// // +// // // TODO: Allow resigning of the existing private key? +// // +// // _, _, err = caStore.CreateKeypair(cmd.Id, template) +// // if err != nil { +// // return fmt.Errorf("error creating keypair %v", err) +// // } +// // return nil +// } +// +// if strings.ToLower(cmd.Type) == strings.ToLower(fi.SecretTypeSSHPublicKey) { +// return fmt.Errorf("creating secrets of type %q not (currently) supported", cmd.Type) +// } +// +// return fmt.Errorf("secret type not known: %q", cmd.Type) +//} diff --git a/cmd/kops/delete_secret.go b/cmd/kops/delete_secret.go new file mode 100644 index 0000000000..047b389bdb --- /dev/null +++ b/cmd/kops/delete_secret.go @@ -0,0 +1,79 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/kops/upup/pkg/fi" +) + +type DeleteSecretCmd struct { +} + +var deleteSecretCmd DeleteSecretCmd + +func init() { + cmd := &cobra.Command{ + Use: "secret", + Short: "Delete secret", + Long: `Delete a secret.`, + Run: func(cmd *cobra.Command, args []string) { + err := deleteSecretCmd.Run(args) + if err != nil { + exitWithError(err) + } + }, + } + + deleteCmd.AddCommand(cmd) +} + +func (c *DeleteSecretCmd) Run(args []string) error { + if len(args) != 2 && len(args) != 3 { + return fmt.Errorf("Syntax: []") + } + + secretType := args[0] + secretName := args[1] + + secretID := "" + if len(args) == 3 { + secretID = args[2] + } + + secrets, err := listSecrets(secretType, []string{secretName}) + if err != nil { + return err + } + + if secretID != "" { + var matches []*fi.KeystoreItem + for _, s := range secrets { + if s.Id == secretID { + matches = append(matches, s) + } + } + secrets = matches + } + + if len(secrets) == 0 { + return fmt.Errorf("secret %q not found") + } + + if len(secrets) != 1 { + // TODO: it would be friendly to print the matching keys + return fmt.Errorf("found multiple matching secrets; specify the id of the key") + } + + keyStore, err := rootCommand.KeyStore() + if err != nil { + return err + } + + err = keyStore.DeleteSecret(secrets[0]) + if err != nil { + return fmt.Errorf("error deleting secret: %v", err) + } + + return nil +} diff --git a/cmd/kops/describe.go b/cmd/kops/describe.go new file mode 100644 index 0000000000..0adca4a4f4 --- /dev/null +++ b/cmd/kops/describe.go @@ -0,0 +1,23 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +// DescribeCmd represents the describe command +type DescribeCmd struct { + cobraCommand *cobra.Command +} + +var describeCmd = DescribeCmd{ + cobraCommand: &cobra.Command{ + Use: "describe", + Short: "describe objects", + }, +} + +func init() { + cmd := describeCmd.cobraCommand + + rootCommand.AddCommand(cmd) +} diff --git a/cmd/kops/describe_secrets.go b/cmd/kops/describe_secrets.go new file mode 100644 index 0000000000..ea42af5e0c --- /dev/null +++ b/cmd/kops/describe_secrets.go @@ -0,0 +1,155 @@ +package main + +import ( + "fmt" + + "bytes" + "crypto/rsa" + "github.com/spf13/cobra" + "k8s.io/kops/upup/pkg/fi" + "os" + "sort" + "strings" + "text/tabwriter" +) + +type DescribeSecretsCommand struct { + Type string +} + +var describeSecretsCommand DescribeSecretsCommand + +func init() { + cmd := &cobra.Command{ + Use: "secrets", + Aliases: []string{"secret"}, + Short: "Describe secrets", + Long: `Describe secrets.`, + Run: func(cmd *cobra.Command, args []string) { + err := describeSecretsCommand.Run(args) + if err != nil { + exitWithError(err) + } + }, + } + + describeCmd.cobraCommand.AddCommand(cmd) + + cmd.Flags().StringVarP(&describeSecretsCommand.Type, "type", "", "", "Filter by secret type") +} + +func (c *DescribeSecretsCommand) Run(args []string) error { + items, err := listSecrets(c.Type, args) + if err != nil { + return err + } + + if len(items) == 0 { + fmt.Fprintf(os.Stderr, "No secrets found\n") + + return nil + } + + w := new(tabwriter.Writer) + var b bytes.Buffer + + // Format in tab-separated columns with a tab stop of 8. + w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.StripEscape) + + keyStore, err := rootCommand.KeyStore() + if err != nil { + return err + } + + for _, i := range items { + fmt.Fprintf(w, "Name:\t%s\n", i.Name) + fmt.Fprintf(w, "Type:\t%s\n", i.Type) + fmt.Fprintf(w, "Id:\t%s\n", i.Id) + + switch i.Type { + case fi.SecretTypeKeypair: + err = describeKeypair(keyStore, i, &b) + if err != nil { + return err + } + + case fi.SecretTypeSSHPublicKey: + err = describeSSHPublicKey(i, &b) + if err != nil { + return err + } + + case fi.SecretTypeSecret: + err = describeSecret(i, &b) + if err != nil { + return err + } + } + + b.WriteString("\n") + _, err = w.Write(b.Bytes()) + if err != nil { + return fmt.Errorf("error writing to output: %v", err) + } + + b.Reset() + } + + return w.Flush() +} + +func describeKeypair(keyStore fi.CAStore, item *fi.KeystoreItem, w *bytes.Buffer) error { + name := item.Name + + cert, err := keyStore.FindCert(name) + if err != nil { + return fmt.Errorf("error retrieving cert %q: %v", name, err) + } + + key, err := keyStore.FindPrivateKey(name) + if err != nil { + return fmt.Errorf("error retrieving private key %q: %v", name, err) + } + + var alternateNames []string + if cert != nil { + alternateNames = append(alternateNames, cert.Certificate.DNSNames...) + alternateNames = append(alternateNames, cert.Certificate.EmailAddresses...) + for _, ip := range cert.Certificate.IPAddresses { + alternateNames = append(alternateNames, ip.String()) + } + sort.Strings(alternateNames) + } + + if cert != nil { + fmt.Fprintf(w, "Subject:\t%s\n", pkixNameToString(&cert.Certificate.Subject)) + fmt.Fprintf(w, "Issuer:\t%s\n", pkixNameToString(&cert.Certificate.Issuer)) + fmt.Fprintf(w, "AlternateNames:\t%s\n", strings.Join(alternateNames, ", ")) + fmt.Fprintf(w, "CA:\t%v\n", cert.IsCA) + fmt.Fprintf(w, "NotAfter:\t%s\n", cert.Certificate.NotAfter) + fmt.Fprintf(w, "NotBefore:\t%s\n", cert.Certificate.NotBefore) + + // PublicKeyAlgorithm doesn't have a String() function. Also, is this important information? + //fmt.Fprintf(w, "PublicKeyAlgorithm:\t%v\n", c.Certificate.PublicKeyAlgorithm) + //fmt.Fprintf(w, "SignatureAlgorithm:\t%v\n", c.Certificate.SignatureAlgorithm) + } + + if key != nil { + if rsaPrivateKey, ok := key.Key.(*rsa.PrivateKey); ok { + fmt.Fprintf(w, "PrivateKeyType:\t%v\n", "rsa") + fmt.Fprintf(w, "KeyLength:\t%v\n", rsaPrivateKey.N.BitLen()) + } else { + fmt.Fprintf(w, "PrivateKeyType:\tunknown (%T)\n", key.Key) + } + } + + return nil +} + +func describeSecret(item *fi.KeystoreItem, w *bytes.Buffer) error { + return nil +} + +func describeSSHPublicKey(item *fi.KeystoreItem, w *bytes.Buffer) error { + return nil +} diff --git a/cmd/kops/format.go b/cmd/kops/format.go index 65aa963d90..4f4ee2f7e1 100644 --- a/cmd/kops/format.go +++ b/cmd/kops/format.go @@ -129,7 +129,7 @@ func (t *Table) Render(items interface{}, out io.Writer, columnNames ...string) w := new(tabwriter.Writer) // Format in tab-separated columns with a tab stop of 8. - w.Init(out, 0, 8, 0, '\t', tabwriter.StripEscape) + w.Init(out, 0, 8, 1, '\t', tabwriter.StripEscape) writeHeader := true if writeHeader { diff --git a/cmd/kops/get.go b/cmd/kops/get.go index 41307a26d6..1fa1018c08 100644 --- a/cmd/kops/get.go +++ b/cmd/kops/get.go @@ -4,14 +4,31 @@ import ( "github.com/spf13/cobra" ) -// getCmd represents the get command -var getCmd = &cobra.Command{ - Use: "get", - SuggestFor: []string{"list"}, - Short: "list or get obejcts", - Long: `list or get obejcts`, +// GetCmd represents the get command +type GetCmd struct { + output string + + cobraCommand *cobra.Command } -func init() { - rootCommand.AddCommand(getCmd) +var getCmd = GetCmd{ + cobraCommand: &cobra.Command{ + Use: "get", + SuggestFor: []string{"list"}, + Short: "list or get objects", + Long: `list or get objects`, + }, +} + +const ( + OutputYaml = "yaml" + OutputTable = "table" +) + +func init() { + cmd := getCmd.cobraCommand + + rootCommand.AddCommand(cmd) + + cmd.PersistentFlags().StringVarP(&getCmd.output, "output", "o", OutputTable, "output format. One of: table, yaml") } diff --git a/cmd/kops/get_cluster.go b/cmd/kops/get_cluster.go index 0c38cff3d9..e2e4580081 100644 --- a/cmd/kops/get_cluster.go +++ b/cmd/kops/get_cluster.go @@ -3,6 +3,7 @@ package main import ( "os" + "fmt" "github.com/golang/glog" "github.com/spf13/cobra" "k8s.io/kops/upup/pkg/api" @@ -10,6 +11,7 @@ import ( ) type GetClustersCmd struct { + FullSpec bool } var getClustersCmd GetClustersCmd @@ -21,28 +23,34 @@ func init() { Short: "get clusters", Long: `List or get clusters.`, Run: func(cmd *cobra.Command, args []string) { - err := getClustersCmd.Run() + err := getClustersCmd.Run(args) if err != nil { glog.Exitf("%v", err) } }, } - getCmd.AddCommand(cmd) + getCmd.cobraCommand.AddCommand(cmd) + + cmd.Flags().BoolVar(&getClustersCmd.FullSpec, "full", false, "Show fully populated configuration") } -func (c *GetClustersCmd) Run() error { +func (c *GetClustersCmd) Run(args []string) error { clusterRegistry, err := rootCommand.ClusterRegistry() if err != nil { return err } - clusterNames, err := clusterRegistry.List() - if err != nil { - return err + var clusters []*api.Cluster + + clusterNames := args + if len(args) == 0 { + clusterNames, err = clusterRegistry.List() + if err != nil { + return err + } } - var clusters []*api.Cluster for _, clusterName := range clusterNames { cluster, err := clusterRegistry.Find(clusterName) if err != nil { @@ -50,28 +58,59 @@ func (c *GetClustersCmd) Run() error { } if cluster == nil { - glog.Warningf("cluster was listed, but then not found %q", clusterName) + return fmt.Errorf("cluster not found %q", clusterName) } clusters = append(clusters, cluster) } + if len(clusters) == 0 { + fmt.Fprintf(os.Stderr, "No clusters found\n") return nil } - t := &Table{} - t.AddColumn("NAME", func(c *api.Cluster) string { - return c.Name - }) - t.AddColumn("CLOUD", func(c *api.Cluster) string { - return c.Spec.CloudProvider - }) - t.AddColumn("ZONES", func(c *api.Cluster) string { - var zoneNames []string - for _, z := range c.Spec.Zones { - zoneNames = append(zoneNames, z.Name) + output := getCmd.output + if output == OutputTable { + t := &Table{} + t.AddColumn("NAME", func(c *api.Cluster) string { + return c.Name + }) + t.AddColumn("CLOUD", func(c *api.Cluster) string { + return c.Spec.CloudProvider + }) + t.AddColumn("ZONES", func(c *api.Cluster) string { + var zoneNames []string + for _, z := range c.Spec.Zones { + zoneNames = append(zoneNames, z.Name) + } + return strings.Join(zoneNames, ",") + }) + return t.Render(clusters, os.Stdout, "NAME", "CLOUD", "ZONES") + } else if output == OutputYaml { + if c.FullSpec { + var fullSpecs []*api.Cluster + for _, cluster := range clusters { + spec, err := clusterRegistry.ReadCompletedConfig(cluster.Name) + if err != nil { + return fmt.Errorf("error reading full cluster spec for %q: %v", cluster.Name, err) + } + fullSpecs = append(fullSpecs, spec) + } + clusters = fullSpecs } - return strings.Join(zoneNames, ",") - }) - return t.Render(clusters, os.Stdout, "NAME", "CLOUD", "ZONES") + + for _, cluster := range clusters { + y, err := api.ToYaml(cluster) + if err != nil { + return fmt.Errorf("error marshaling yaml for %q: %v", cluster.Name, err) + } + _, err = os.Stdout.Write(y) + if err != nil { + return fmt.Errorf("error writing to stdout: %v", err) + } + } + return nil + } else { + return fmt.Errorf("Unknown output format: %q", output) + } } diff --git a/cmd/kops/get_instancegroups.go b/cmd/kops/get_instancegroups.go index fc30f09c36..886194edec 100644 --- a/cmd/kops/get_instancegroups.go +++ b/cmd/kops/get_instancegroups.go @@ -3,6 +3,7 @@ package main import ( "os" + "fmt" "github.com/golang/glog" "github.com/spf13/cobra" "k8s.io/kops/upup/pkg/api" @@ -22,17 +23,17 @@ func init() { Short: "get instancegroups", Long: `List or get InstanceGroups.`, Run: func(cmd *cobra.Command, args []string) { - err := getInstanceGroupsCmd.Run() + err := getInstanceGroupsCmd.Run(args) if err != nil { glog.Exitf("%v", err) } }, } - getCmd.AddCommand(cmd) + getCmd.cobraCommand.AddCommand(cmd) } -func (c *GetInstanceGroupsCmd) Run() error { +func (c *GetInstanceGroupsCmd) Run(args []string) error { registry, err := rootCommand.InstanceGroupRegistry() if err != nil { return err @@ -43,30 +44,64 @@ func (c *GetInstanceGroupsCmd) Run() error { return err } + if len(args) != 0 { + m := make(map[string]*api.InstanceGroup) + for _, ig := range instancegroups { + m[ig.Name] = ig + } + instancegroups = make([]*api.InstanceGroup, 0, len(args)) + for _, arg := range args { + ig := m[arg] + if ig == nil { + return fmt.Errorf("instancegroup not found %q", arg) + } + + instancegroups = append(instancegroups, ig) + } + } + if len(instancegroups) == 0 { + fmt.Fprintf(os.Stderr, "No InstanceGroup objects found\n") return nil } - t := &Table{} - t.AddColumn("NAME", func(c *api.InstanceGroup) string { - return c.Name - }) - t.AddColumn("ROLE", func(c *api.InstanceGroup) string { - return string(c.Spec.Role) - }) - t.AddColumn("MACHINETYPE", func(c *api.InstanceGroup) string { - return c.Spec.MachineType - }) - t.AddColumn("ZONES", func(c *api.InstanceGroup) string { - return strings.Join(c.Spec.Zones, ",") - }) - t.AddColumn("MIN", func(c *api.InstanceGroup) string { - return intPointerToString(c.Spec.MinSize) - }) - t.AddColumn("MAX", func(c *api.InstanceGroup) string { - return intPointerToString(c.Spec.MinSize) - }) - return t.Render(instancegroups, os.Stdout, "NAME", "ROLE", "MACHINETYPE", "MIN", "MAX", "ZONES") + output := getCmd.output + if output == OutputTable { + t := &Table{} + t.AddColumn("NAME", func(c *api.InstanceGroup) string { + return c.Name + }) + t.AddColumn("ROLE", func(c *api.InstanceGroup) string { + return string(c.Spec.Role) + }) + t.AddColumn("MACHINETYPE", func(c *api.InstanceGroup) string { + return c.Spec.MachineType + }) + t.AddColumn("ZONES", func(c *api.InstanceGroup) string { + return strings.Join(c.Spec.Zones, ",") + }) + t.AddColumn("MIN", func(c *api.InstanceGroup) string { + return intPointerToString(c.Spec.MinSize) + }) + t.AddColumn("MAX", func(c *api.InstanceGroup) string { + return intPointerToString(c.Spec.MinSize) + }) + return t.Render(instancegroups, os.Stdout, "NAME", "ROLE", "MACHINETYPE", "MIN", "MAX", "ZONES") + } else if output == OutputYaml { + for _, ig := range instancegroups { + y, err := api.ToYaml(ig) + if err != nil { + return fmt.Errorf("error marshaling yaml for %q: %v", ig.Name, err) + } + _, err = os.Stdout.Write(y) + if err != nil { + return fmt.Errorf("error writing to stdout: %v", err) + } + } + return nil + } else { + return fmt.Errorf("Unknown output format: %q", output) + } } func intPointerToString(v *int) string { diff --git a/cmd/kops/get_secrets.go b/cmd/kops/get_secrets.go new file mode 100644 index 0000000000..6a49f5fc3e --- /dev/null +++ b/cmd/kops/get_secrets.go @@ -0,0 +1,175 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "k8s.io/kops/upup/pkg/fi" + "strings" +) + +type GetSecretsCommand struct { + Type string +} + +var getSecretsCommand GetSecretsCommand + +func init() { + cmd := &cobra.Command{ + Use: "secrets", + Aliases: []string{"secret"}, + Short: "get secrets", + Long: `List or get secrets.`, + Run: func(cmd *cobra.Command, args []string) { + err := getSecretsCommand.Run(args) + if err != nil { + exitWithError(err) + } + }, + } + + getCmd.cobraCommand.AddCommand(cmd) + + cmd.Flags().StringVarP(&getSecretsCommand.Type, "type", "", "", "Filter by secret type") +} + +func listSecrets(secretType string, names []string) ([]*fi.KeystoreItem, error) { + var items []*fi.KeystoreItem + + findType := strings.ToLower(secretType) + switch findType { + case "": + // OK + case "sshpublickey", "keypair", "secret": + // OK + default: + return nil, fmt.Errorf("unknown secret type %q", secretType) + } + + { + caStore, err := rootCommand.KeyStore() + if err != nil { + return nil, err + } + l, err := caStore.List() + if err != nil { + return nil, fmt.Errorf("error listing CA store items %v", err) + } + + for _, i := range l { + if findType != "" && findType != strings.ToLower(i.Type) { + continue + } + items = append(items, i) + } + } + + if findType == "" || findType == strings.ToLower(fi.SecretTypeSecret) { + secretStore, err := rootCommand.SecretStore() + if err != nil { + return nil, err + } + + l, err := secretStore.ListSecrets() + if err != nil { + return nil, fmt.Errorf("error listing secrets %v", err) + } + + for _, id := range l { + i := &fi.KeystoreItem{ + Name: id, + Type: fi.SecretTypeSecret, + } + if findType != "" && findType != strings.ToLower(i.Type) { + continue + } + + items = append(items, i) + } + } + + if len(names) != 0 { + var matches []*fi.KeystoreItem + for _, arg := range names { + var found []*fi.KeystoreItem + for _, i := range items { + // There may be multiple secrets with the same name (of different type) + if i.Name == arg { + found = append(found, i) + } + } + + if len(found) == 0 { + return nil, fmt.Errorf("Secret not found: %q", arg) + } + + matches = append(matches, found...) + } + items = matches + } + + return items, nil +} + +func (c *GetSecretsCommand) Run(args []string) error { + items, err := listSecrets(c.Type, args) + if err != nil { + return err + } + + if len(items) == 0 { + fmt.Fprintf(os.Stderr, "No secrets found\n") + + return nil + } + + output := getCmd.output + if output == OutputTable { + t := &Table{} + t.AddColumn("NAME", func(i *fi.KeystoreItem) string { + return i.Name + }) + t.AddColumn("ID", func(i *fi.KeystoreItem) string { + return i.Id + }) + t.AddColumn("TYPE", func(i *fi.KeystoreItem) string { + return i.Type + }) + return t.Render(items, os.Stdout, "TYPE", "NAME", "ID") + } else if output == OutputYaml { + return fmt.Errorf("yaml output format is not (currently) supported for secrets") + } else if output == "plaintext" { + secretStore, err := rootCommand.SecretStore() + if err != nil { + return err + } + + for _, i := range items { + var data string + switch i.Type { + case fi.SecretTypeSecret: + secret, err := secretStore.FindSecret(i.Name) + if err != nil { + return fmt.Errorf("error getting secret %q: %v", i.Name, err) + } + if secret == nil { + return fmt.Errorf("cannot find secret %q", i.Name) + } + data = string(secret.Data) + + default: + return fmt.Errorf("secret type %v cannot (currently) be exported as plaintext", i.Type) + } + + _, err := fmt.Fprintf(os.Stdout, "%s\n", data) + if err != nil { + return fmt.Errorf("error writing output: %v", err) + } + } + return nil + } else { + return fmt.Errorf("Unknown output format: %q", output) + } + +} diff --git a/cmd/kops/main.go b/cmd/kops/main.go index 736ef31021..e93cb9bf5a 100644 --- a/cmd/kops/main.go +++ b/cmd/kops/main.go @@ -1,5 +1,17 @@ package main +import ( + "fmt" + "os" +) + func main() { Execute() } + +// exitWithError will terminate execution with an error result +// It prints the error to stderr and exits with a non-zero exit code +func exitWithError(err error) { + fmt.Fprintf(os.Stderr, "\n%v\n", err) + os.Exit(1) +} diff --git a/cmd/kops/secrets_create.go b/cmd/kops/secrets_create.go index 1d53cd48d5..bd1429a17e 100644 --- a/cmd/kops/secrets_create.go +++ b/cmd/kops/secrets_create.go @@ -2,141 +2,18 @@ package main import ( "fmt" - - "crypto/x509" - "github.com/golang/glog" "github.com/spf13/cobra" - "net" - "strings" ) -type CreateSecretsCommand struct { - Id string - Type string - - Usage string - Subject string - AlternateNames []string -} - -var createSecretsCommand CreateSecretsCommand - func init() { cmd := &cobra.Command{ Use: "create", Short: "Create secrets", Long: `Create secrets.`, Run: func(cmd *cobra.Command, args []string) { - err := createSecretsCommand.Run() - if err != nil { - glog.Exitf("%v", err) - } + exitWithError(fmt.Errorf("The 'secrets create' command has been replaced by 'create secrets'")) }, } secretsCmd.AddCommand(cmd) - - cmd.Flags().StringVarP(&createSecretsCommand.Type, "type", "", "", "Type of secret to create") - cmd.Flags().StringVarP(&createSecretsCommand.Id, "id", "", "", "Id of secret to create") - cmd.Flags().StringVarP(&createSecretsCommand.Usage, "usage", "", "", "Usage of secret (for SSL certificate)") - cmd.Flags().StringVarP(&createSecretsCommand.Subject, "subject", "", "", "Subject (for SSL certificate)") - cmd.Flags().StringSliceVarP(&createSecretsCommand.AlternateNames, "san", "", nil, "Alternate name (for SSL certificate)") -} - -func (cmd *CreateSecretsCommand) Run() error { - if cmd.Id == "" { - return fmt.Errorf("id is required") - } - - if cmd.Type == "" { - return fmt.Errorf("type is required") - } - - // TODO: Prompt before replacing? - // TODO: Keep history? - - switch cmd.Type { - case "secret": - { - secretStore, err := rootCommand.SecretStore() - if err != nil { - return err - } - _, created, err := secretStore.GetOrCreateSecret(cmd.Id) - if err != nil { - return fmt.Errorf("error creating secrets %v", err) - } - if !created { - return fmt.Errorf("secret already exists") - } - return nil - } - - case "keypair": - // TODO: Create a rotate command which keeps the same values? - // Or just do it here a "replace" action - existing=fail, replace or rotate - // TODO: Create a CreateKeypair class, move to fi (this is duplicated code) - { - if cmd.Subject == "" { - return fmt.Errorf("subject is required") - } - - subject, err := parsePkixName(cmd.Subject) - if err != nil { - return fmt.Errorf("Error parsing subject: %v", err) - } - template := &x509.Certificate{ - Subject: *subject, - BasicConstraintsValid: true, - IsCA: false, - } - - if len(template.Subject.ToRDNSequence()) == 0 { - return fmt.Errorf("Subject name was empty") - } - - switch cmd.Usage { - case "client": - template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} - template.KeyUsage = x509.KeyUsageDigitalSignature - break - - case "server": - template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} - template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment - break - - default: - return fmt.Errorf("unknown usage: %q", cmd.Usage) - } - - for _, san := range cmd.AlternateNames { - san = strings.TrimSpace(san) - if san == "" { - continue - } - if ip := net.ParseIP(san); ip != nil { - template.IPAddresses = append(template.IPAddresses, ip) - } else { - template.DNSNames = append(template.DNSNames, san) - } - } - - caStore, err := rootCommand.KeyStore() - if err != nil { - return err - } - - // TODO: Allow resigning of the existing private key? - - _, _, err = caStore.CreateKeypair(cmd.Id, template) - if err != nil { - return fmt.Errorf("error creating keypair %v", err) - } - return nil - } - - default: - return fmt.Errorf("secret type not known: %q", cmd.Type) - } } diff --git a/cmd/kops/secrets_describe.go b/cmd/kops/secrets_describe.go index 9f2a85677d..d6024aa693 100644 --- a/cmd/kops/secrets_describe.go +++ b/cmd/kops/secrets_describe.go @@ -3,178 +3,18 @@ package main import ( "fmt" - "bytes" - "crypto/rsa" - "github.com/golang/glog" "github.com/spf13/cobra" - "k8s.io/kops/upup/pkg/fi" - "os" - "sort" - "strings" - "text/tabwriter" ) -type DescribeSecretsCommand struct { -} - -var describeSecretsCommand DescribeSecretsCommand - func init() { cmd := &cobra.Command{ Use: "describe", Short: "Describe secrets", Long: `Describe secrets.`, Run: func(cmd *cobra.Command, args []string) { - err := describeSecretsCommand.Run() - if err != nil { - glog.Exitf("%v", err) - } + exitWithError(fmt.Errorf("The 'secrets describe' command has been replaced by 'describe secrets'")) }, } secretsCmd.AddCommand(cmd) } - -func (c *DescribeSecretsCommand) Run() error { - - w := new(tabwriter.Writer) - var b bytes.Buffer - - // Format in tab-separated columns with a tab stop of 8. - w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.StripEscape) - - { - caStore, err := rootCommand.KeyStore() - if err != nil { - return err - } - ids, err := caStore.List() - if err != nil { - return fmt.Errorf("error listing CA store items %v", err) - } - - for _, id := range ids { - cert, err := caStore.FindCert(id) - if err != nil { - return fmt.Errorf("error retrieving cert %q: %v", id, err) - } - - key, err := caStore.FindPrivateKey(id) - if err != nil { - return fmt.Errorf("error retrieving private key %q: %v", id, err) - } - - if key == nil && cert == nil { - continue - } - - err = describeKeypair(id, cert, key, &b) - if err != nil { - return err - } - - b.WriteString("\n") - - _, err = w.Write(b.Bytes()) - if err != nil { - return fmt.Errorf("error writing to output: %v", err) - } - - b.Reset() - } - - } - - { - secretStore, err := rootCommand.SecretStore() - if err != nil { - return err - } - ids, err := secretStore.ListSecrets() - if err != nil { - return fmt.Errorf("error listing secrets %v", err) - } - - for _, id := range ids { - secret, err := secretStore.FindSecret(id) - if err != nil { - return fmt.Errorf("error retrieving secret %q: %v", id, err) - } - - if secret == nil { - continue - } - - err = describeSecret(id, secret, &b) - if err != nil { - return err - } - - b.WriteString("\n") - - _, err = w.Write(b.Bytes()) - if err != nil { - return fmt.Errorf("error writing to output: %v", err) - } - - b.Reset() - } - } - - return w.Flush() -} - -func describeKeypair(id string, c *fi.Certificate, k *fi.PrivateKey, w *bytes.Buffer) error { - var alternateNames []string - if c != nil { - alternateNames = append(alternateNames, c.Certificate.DNSNames...) - alternateNames = append(alternateNames, c.Certificate.EmailAddresses...) - for _, ip := range c.Certificate.IPAddresses { - alternateNames = append(alternateNames, ip.String()) - } - sort.Strings(alternateNames) - } - - fmt.Fprintf(w, "Id:\t%s\n", id) - if c != nil && k != nil { - fmt.Fprintf(w, "Type:\t%s\n", "keypair") - } else if c != nil && k == nil { - fmt.Fprintf(w, "Type:\t%s\n", "certificate") - } else if k != nil && c == nil { - // Unexpected! - fmt.Fprintf(w, "Type:\t%s\n", "privatekey") - } else { - return fmt.Errorf("expected either certificate or key to be set") - } - - if c != nil { - fmt.Fprintf(w, "Subject:\t%s\n", pkixNameToString(&c.Certificate.Subject)) - fmt.Fprintf(w, "Issuer:\t%s\n", pkixNameToString(&c.Certificate.Issuer)) - fmt.Fprintf(w, "AlternateNames:\t%s\n", strings.Join(alternateNames, ", ")) - fmt.Fprintf(w, "CA:\t%v\n", c.IsCA) - fmt.Fprintf(w, "NotAfter:\t%s\n", c.Certificate.NotAfter) - fmt.Fprintf(w, "NotBefore:\t%s\n", c.Certificate.NotBefore) - - // PublicKeyAlgorithm doesn't have a String() function. Also, is this important information? - //fmt.Fprintf(w, "PublicKeyAlgorithm:\t%v\n", c.Certificate.PublicKeyAlgorithm) - //fmt.Fprintf(w, "SignatureAlgorithm:\t%v\n", c.Certificate.SignatureAlgorithm) - } - - if k != nil { - if rsaPrivateKey, ok := k.Key.(*rsa.PrivateKey); ok { - fmt.Fprintf(w, "PrivateKeyType:\t%v\n", "rsa") - fmt.Fprintf(w, "KeyLength:\t%v\n", rsaPrivateKey.N.BitLen()) - } else { - fmt.Fprintf(w, "PrivateKeyType:\tunknown (%T)\n", k.Key) - } - } - - return nil -} - -func describeSecret(id string, s *fi.Secret, w *bytes.Buffer) error { - fmt.Fprintf(w, "Id:\t%s\n", id) - fmt.Fprintf(w, "Type:\t%s\n", "secret") - - return nil -} diff --git a/cmd/kops/secrets_expose.go b/cmd/kops/secrets_expose.go index a6fdae44c7..5c73042ddf 100644 --- a/cmd/kops/secrets_expose.go +++ b/cmd/kops/secrets_expose.go @@ -3,108 +3,18 @@ package main import ( "fmt" - "github.com/golang/glog" "github.com/spf13/cobra" - "os" ) -type ExposeSecretsCommand struct { - ID string - Type string -} - -var exposeSecretsCommand ExposeSecretsCommand - func init() { cmd := &cobra.Command{ Use: "expose", Short: "Expose secrets", Long: `Expose secrets.`, Run: func(cmd *cobra.Command, args []string) { - err := exposeSecretsCommand.Run() - if err != nil { - glog.Exitf("%v", err) - } + exitWithError(fmt.Errorf("The 'secrets export' command has been replaced by 'get secrets -oplaintext'")) }, } secretsCmd.AddCommand(cmd) - - cmd.Flags().StringVarP(&exposeSecretsCommand.Type, "type", "", "", "Type of secret to create") - cmd.Flags().StringVarP(&exposeSecretsCommand.ID, "id", "", "", "Id of secret to create") -} - -func (cmd *ExposeSecretsCommand) Run() error { - id := cmd.ID - if id == "" { - return fmt.Errorf("id is required") - } - - if cmd.Type == "" { - return fmt.Errorf("type is required") - } - - var value string - switch cmd.Type { - case "secret": - { - secretStore, err := rootCommand.SecretStore() - if err != nil { - return err - } - secret, err := secretStore.FindSecret(id) - if err != nil { - return fmt.Errorf("error finding secret %q: %v", id, err) - } - if secret == nil { - return fmt.Errorf("secret not found: %q", id) - } - value = string(secret.Data) - } - - case "certificate", "privatekey": - { - caStore, err := rootCommand.KeyStore() - if err != nil { - return fmt.Errorf("error building CA store: %v", err) - } - - if cmd.Type == "privatekey" { - k, err := caStore.FindPrivateKey(id) - if err != nil { - return fmt.Errorf("error finding privatekey: %v", err) - } - if k == nil { - return fmt.Errorf("privatekey not found: %q", id) - } - value, err = k.AsString() - if err != nil { - return fmt.Errorf("error encoding privatekey: %v", err) - } - } else { - c, err := caStore.FindCert(id) - if err != nil { - return fmt.Errorf("error finding certificate: %v", err) - } - if c == nil { - return fmt.Errorf("certificate not found: %q", id) - } - value, err = c.AsString() - if err != nil { - return fmt.Errorf("error encoding certiifcate: %v", err) - } - } - } - - default: - return fmt.Errorf("secret type not known: %q", cmd.Type) - } - - _, err := fmt.Fprint(os.Stdout, value+"\n") - if err != nil { - return fmt.Errorf("error writing to output: %v", err) - } - - return nil - } diff --git a/cmd/kops/secrets_get.go b/cmd/kops/secrets_get.go index 9767794d8a..c6d5d92a77 100644 --- a/cmd/kops/secrets_get.go +++ b/cmd/kops/secrets_get.go @@ -2,101 +2,18 @@ package main import ( "fmt" - - "bytes" - "github.com/golang/glog" "github.com/spf13/cobra" - "os" - "text/tabwriter" ) -type GetSecretsCommand struct { -} - -var getSecretsCommand GetSecretsCommand - func init() { cmd := &cobra.Command{ Use: "get", Short: "Get secrets", Long: `Get secrets.`, Run: func(cmd *cobra.Command, args []string) { - err := getSecretsCommand.Run() - if err != nil { - glog.Exitf("%v", err) - } + exitWithError(fmt.Errorf("The 'secrets get' command has been replaced by 'get secret'")) }, } secretsCmd.AddCommand(cmd) } - -type SecretInfo struct { - Id string - Type string -} - -func (c *GetSecretsCommand) Run() error { - var infos []*SecretInfo - { - caStore, err := rootCommand.KeyStore() - if err != nil { - return err - } - ids, err := caStore.List() - if err != nil { - return fmt.Errorf("error listing CA store items %v", err) - } - - for _, id := range ids { - info := &SecretInfo{ - Id: id, - Type: "keypair", - } - infos = append(infos, info) - } - } - - { - secretStore, err := rootCommand.SecretStore() - if err != nil { - return err - } - ids, err := secretStore.ListSecrets() - if err != nil { - return fmt.Errorf("error listing secrets %v", err) - } - - for _, id := range ids { - info := &SecretInfo{ - Id: id, - Type: "secret", - } - infos = append(infos, info) - } - } - - var b bytes.Buffer - w := new(tabwriter.Writer) - - // Format in tab-separated columns with a tab stop of 8. - w.Init(os.Stdout, 0, 8, 0, '\t', tabwriter.StripEscape) - for _, info := range infos { - b.WriteByte(tabwriter.Escape) - b.WriteString(info.Type) - b.WriteByte(tabwriter.Escape) - b.WriteByte('\t') - b.WriteByte(tabwriter.Escape) - b.WriteString(info.Id) - b.WriteByte(tabwriter.Escape) - b.WriteByte('\n') - - _, err := w.Write(b.Bytes()) - if err != nil { - return fmt.Errorf("error writing to output: %v", err) - } - b.Reset() - } - w.Flush() - return nil -} diff --git a/docs/addons.md b/docs/addons.md index e2248fcae8..bdc550b76e 100644 --- a/docs/addons.md +++ b/docs/addons.md @@ -24,7 +24,7 @@ And then navigate to `https:///ui` The login credentials are: * Username: `admin` -* Password: get by running `kops secrets expose --id kube --type secret` +* Password: get by running `kops get secrets kube --type secret -oplaintext` ### Monitoring - Standalone diff --git a/docs/secrets.md b/docs/secrets.md new file mode 100644 index 0000000000..0049679e33 --- /dev/null +++ b/docs/secrets.md @@ -0,0 +1,26 @@ +## Managing secrets + +### get secrets + +### get secret -oplaintext + +-oplaintext exposes the raw secret value. + +### describe secret + +`kops describe secret` + +### create secret + +`kops create secret publickey admin -i ~/.ssh/id_rsa.pub` + +### delete secret + +Syntax: `kops delete secret ` +or `kops delete secret ` + +The ID form can be used when there are multiple matching keys. + +example: +`kops delete secret sshpublickey admin` + diff --git a/upup/pkg/fi/ca.go b/upup/pkg/fi/ca.go index 68c4435c4c..aeb4a7e8d8 100644 --- a/upup/pkg/fi/ca.go +++ b/upup/pkg/fi/ca.go @@ -28,6 +28,19 @@ type Certificate struct { PublicKey crypto.PublicKey } +const ( + SecretTypeSSHPublicKey = "SSHPublicKey" + SecretTypeKeypair = "Keypair" + SecretTypeSecret = "Secret" +) + +type KeystoreItem struct { + Type string + Name string + Id string + Data []byte +} + func (c *Certificate) UnmarshalJSON(b []byte) error { s := "" if err := json.Unmarshal(b, &s); err == nil { @@ -67,26 +80,36 @@ func (c *Certificate) MarshalJSON() ([]byte, error) { type CAStore interface { // Cert returns the primary specified certificate - Cert(id string) (*Certificate, error) + Cert(name string) (*Certificate, error) // CertificatePool returns all active certificates with the specified id - CertificatePool(id string) (*CertificatePool, error) - PrivateKey(id string) (*PrivateKey, error) + CertificatePool(name string) (*CertificatePool, error) + PrivateKey(name string) (*PrivateKey, error) - FindCert(id string) (*Certificate, error) - FindPrivateKey(id string) (*PrivateKey, error) + FindCert(name string) (*Certificate, error) + FindPrivateKey(name string) (*PrivateKey, error) //IssueCert(id string, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error) //CreatePrivateKey(id string) (*PrivateKey, error) - CreateKeypair(id string, template *x509.Certificate) (*Certificate, *PrivateKey, error) + CreateKeypair(name string, template *x509.Certificate) (*Certificate, *PrivateKey, error) - List() ([]string, error) + // List will list all the items, but will not fetch the data + List() ([]*KeystoreItem, error) // VFSPath returns the path where the CAStore is stored VFSPath() vfs.Path // AddCert adds an alternative certificate to the pool (primarily useful for CAs) - AddCert(id string, cert *Certificate) error + AddCert(name string, cert *Certificate) error + + // AddSSHPublicKey adds an SSH public key + AddSSHPublicKey(name string, data []byte) error + + // FindSSHPublicKeys retrieves the SSH public keys with the specific name + FindSSHPublicKeys(name string) ([]*KeystoreItem, error) + + // DeleteSecret will delete the specified item + DeleteSecret(item *KeystoreItem) error } func (c *Certificate) AsString() (string, error) { diff --git a/upup/pkg/fi/cloudup/populate_cluster_spec.go b/upup/pkg/fi/cloudup/populate_cluster_spec.go index 6b858842aa..2ddc694688 100644 --- a/upup/pkg/fi/cloudup/populate_cluster_spec.go +++ b/upup/pkg/fi/cloudup/populate_cluster_spec.go @@ -290,7 +290,7 @@ func (c *populateClusterSpec) run() error { if cluster.Spec.DNSZone == "" { dnsZone, err := cloud.FindDNSHostedZone(cluster.Name) if err != nil { - return fmt.Errorf("Error determining default DNS zone; please specify --zone-name: %v", err) + return fmt.Errorf("Error determining default DNS zone; please specify --dns-zone: %v", err) } glog.Infof("Defaulting DNS zone to: %s", dnsZone) cluster.Spec.DNSZone = dnsZone diff --git a/upup/pkg/fi/fitasks/secret.go b/upup/pkg/fi/fitasks/secret.go index a327bb5cbe..695b46a613 100644 --- a/upup/pkg/fi/fitasks/secret.go +++ b/upup/pkg/fi/fitasks/secret.go @@ -61,7 +61,12 @@ func (_ *Secret) Render(c *fi.Context, a, e, changes *Secret) error { secrets := c.SecretStore - _, _, err := secrets.GetOrCreateSecret(name) + secret, err := fi.CreateSecret() + if err != nil { + return fmt.Errorf("error creating secret %q: %v", name, err) + } + + _, _, err = secrets.GetOrCreateSecret(name, secret) if err != nil { return fmt.Errorf("error creating secret %q: %v", name, err) } diff --git a/upup/pkg/fi/secrets.go b/upup/pkg/fi/secrets.go index 8ac4cedba0..b5f94a844d 100644 --- a/upup/pkg/fi/secrets.go +++ b/upup/pkg/fi/secrets.go @@ -14,7 +14,7 @@ type SecretStore interface { // Find a secret, if exists. Returns nil,nil if not found FindSecret(id string) (*Secret, error) // Create or replace a secret - GetOrCreateSecret(id string) (secret *Secret, created bool, err error) + GetOrCreateSecret(id string, secret *Secret) (current *Secret, created bool, err error) // Lists the ids of all known secrets ListSecrets() ([]string, error) diff --git a/upup/pkg/fi/vfs_castore.go b/upup/pkg/fi/vfs_castore.go index fe4a5042c2..9f9dd6fb4a 100644 --- a/upup/pkg/fi/vfs_castore.go +++ b/upup/pkg/fi/vfs_castore.go @@ -2,12 +2,14 @@ package fi import ( "bytes" + "crypto/md5" crypto_rand "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "fmt" "github.com/golang/glog" + "golang.org/x/crypto/ssh" "k8s.io/kops/upup/pkg/fi/vfs" "math/big" "os" @@ -316,20 +318,66 @@ func (c *VFSCAStore) FindCertificatePool(id string) (*CertificatePool, error) { return pool, nil } -func (c *VFSCAStore) List() ([]string, error) { - var ids []string +func (c *VFSCAStore) List() ([]*KeystoreItem, error) { + var items []*KeystoreItem - issuedDir := c.basedir.Join("issued") - files, err := issuedDir.ReadDir() - if err != nil { - return nil, fmt.Errorf("error reading directory %q: %v", issuedDir, err) + { + baseDir := c.basedir.Join("issued") + files, err := baseDir.ReadTree() + if err != nil { + return nil, fmt.Errorf("error reading directory %q: %v", baseDir, err) + } + + for _, f := range files { + relativePath, err := vfs.RelativePath(baseDir, f) + if err != nil { + return nil, err + } + + tokens := strings.Split(relativePath, "/") + if len(tokens) != 2 { + glog.V(2).Infof("ignoring unexpected file in keystore: %q", f) + continue + } + + item := &KeystoreItem{ + Name: tokens[0], + Id: strings.TrimSuffix(tokens[1], ".crt"), + Type: SecretTypeKeypair, + } + items = append(items, item) + } } - for _, f := range files { - name := f.Base() - ids = append(ids, name) + { + baseDir := c.basedir.Join("ssh", "public") + files, err := baseDir.ReadTree() + if err != nil { + return nil, fmt.Errorf("error reading directory %q: %v", baseDir, err) + } + + for _, f := range files { + relativePath, err := vfs.RelativePath(baseDir, f) + if err != nil { + return nil, err + } + + tokens := strings.Split(relativePath, "/") + if len(tokens) != 2 { + glog.V(2).Infof("ignoring unexpected file in keystore: %q", f) + continue + } + + item := &KeystoreItem{ + Name: tokens[0], + Id: insertFingerprintColons(tokens[1]), + Type: SecretTypeSSHPublicKey, + } + items = append(items, item) + } } - return ids, nil + + return items, nil } func (c *VFSCAStore) IssueCert(id string, serial *big.Int, privateKey *PrivateKey, template *x509.Certificate) (*Certificate, error) { @@ -572,3 +620,143 @@ func buildSerial(timestamp int64) *big.Int { return serial } + +func formatFingerprint(data []byte) string { + var buf bytes.Buffer + + for i, b := range data { + s := fmt.Sprintf("%0.2x", b) + if i != 0 { + buf.WriteString(":") + } + buf.WriteString(s) + } + return buf.String() +} + +func insertFingerprintColons(id string) string { + var buf bytes.Buffer + + for { + if id == "" { + break + } + if buf.Len() != 0 { + buf.WriteString(":") + } + if len(id) < 2 { + buf.WriteString(id) + } else { + buf.WriteString(id[0:2]) + id = id[2:] + } + } + return buf.String() +} + +// AddSSHPublicKey stores an SSH public key +func (c *VFSCAStore) AddSSHPublicKey(name string, pubkey []byte) error { + var id string + { + sshPublicKey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey) + if err != nil { + return fmt.Errorf("error parsing public key: %v", err) + } + + // compute fingerprint to serve as id + h := md5.New() + _, err = h.Write(sshPublicKey.Marshal()) + if err != nil { + return err + } + id = formatFingerprint(h.Sum(nil)) + } + + p := c.buildSSHPublicKeyPath(name, id) + return c.storeData(pubkey, p) +} + +func (c *VFSCAStore) buildSSHPublicKeyPath(name string, id string) vfs.Path { + // id is fingerprint with colons, but we store without colons + id = strings.Replace(id, ":", "", -1) + return c.basedir.Join("ssh", "public", name, id) +} + +func (c *VFSCAStore) storeData(data []byte, p vfs.Path) error { + return p.WriteFile(data) +} + +func (c *VFSCAStore) FindSSHPublicKeys(name string) ([]*KeystoreItem, error) { + p := c.basedir.Join("ssh", "public", name) + + items, err := c.loadPath(p) + if err != nil { + return nil, err + } + for _, item := range items { + // Fill in the missing fields + item.Type = SecretTypeSSHPublicKey + item.Name = name + + item.Id = insertFingerprintColons(item.Id) + } + return items, nil +} + +func (c *VFSCAStore) loadPath(p vfs.Path) ([]*KeystoreItem, error) { + files, err := p.ReadDir() + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var keystoreItems []*KeystoreItem + + for _, f := range files { + data, err := f.ReadFile() + if err != nil { + if os.IsNotExist(err) { + glog.V(2).Infof("Ignoring not-found issue reading %q", f) + continue + } + return nil, fmt.Errorf("error loading keystore item %q: %v", f, err) + } + name := f.Base() + keystoreItem := &KeystoreItem{ + Id: name, + Data: data, + } + keystoreItems = append(keystoreItems, keystoreItem) + } + + return keystoreItems, nil +} + +func (c *VFSCAStore) loadData(p vfs.Path) (*PrivateKey, error) { + data, err := p.ReadFile() + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + k, err := ParsePEMPrivateKey(data) + if err != nil { + return nil, fmt.Errorf("error parsing private key from %q: %v", p, err) + } + return k, err +} + +func (c *VFSCAStore) DeleteSecret(item *KeystoreItem) error { + switch item.Type { + case SecretTypeSSHPublicKey: + p := c.buildSSHPublicKeyPath(item.Name, item.Id) + return p.Remove() + + default: + // Primarily because we need to make sure users can recreate them! + return fmt.Errorf("deletion of keystore items of type %v not (yet) supported", item.Type) + } +} diff --git a/upup/pkg/fi/vfs_secretstore.go b/upup/pkg/fi/vfs_secretstore.go index 8f2f94290d..82762c67cc 100644 --- a/upup/pkg/fi/vfs_secretstore.go +++ b/upup/pkg/fi/vfs_secretstore.go @@ -62,7 +62,7 @@ func (c *VFSSecretStore) Secret(id string) (*Secret, error) { return s, nil } -func (c *VFSSecretStore) GetOrCreateSecret(id string) (*Secret, bool, error) { +func (c *VFSSecretStore) GetOrCreateSecret(id string, secret *Secret) (*Secret, bool, error) { p := c.buildSecretPath(id) for i := 0; i < 2; i++ { @@ -75,12 +75,7 @@ func (c *VFSSecretStore) GetOrCreateSecret(id string) (*Secret, bool, error) { return s, false, nil } - s, err = CreateSecret() - if err != nil { - return nil, false, err - } - - err = c.createSecret(s, p) + err = c.createSecret(secret, p) if err != nil { if os.IsExist(err) && i == 0 { glog.Infof("Got already-exists error when writing secret; likely due to concurrent creation. Will retry")