From 39c682327ecb5a8a2b30ac9d6951dd3247631d06 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Fri, 13 Nov 2015 01:01:05 -0800 Subject: [PATCH] Pretty-print the key list in a deterministic sorted order. Signed-off-by: Ying Li --- cmd/notary/integration_test.go | 67 ++++++++++------ cmd/notary/keys.go | 137 +++++++++++++++++++++++++-------- cmd/notary/keys_test.go | 113 +++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 57 deletions(-) create mode 100644 cmd/notary/keys_test.go diff --git a/cmd/notary/integration_test.go b/cmd/notary/integration_test.go index ce3166c561..da834e93c7 100644 --- a/cmd/notary/integration_test.go +++ b/cmd/notary/integration_test.go @@ -13,7 +13,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "sort" "strings" "testing" @@ -162,23 +161,50 @@ func splitLines(chunk string) []string { return results } -// List keys, parses the output, and returns the keys as an array of root key -// IDs and an array of signing key IDs -func GetKeys(t *testing.T, tempDir string) ([]string, []string) { +// List keys, parses the output, and returns the unique key IDs as an array +// of root key IDs and an array of signing key IDs. Output expected looks like: +// ROLE GUN KEY ID LOCATION +// ---------------------------------------------------------------- +// root 8bd63a896398b558ac... file (.../private) +// snapshot repo e9e9425cd9a85fc7a5... file (.../private) +// targets repo f5b84e2d92708c5acb... file (.../private) +func getUniqueKeys(t *testing.T, tempDir string) ([]string, []string) { output, err := runCommand(t, tempDir, "key", "list") assert.NoError(t, err) + lines := splitLines(output) - parts := strings.Split(output, "# Signing keys:") - assert.Len(t, parts, 2) - - fixed := make([][]string, 2) - for i, part := range parts { - fixed[i] = splitLines( - strings.TrimPrefix(strings.TrimSpace(part), "# Root keys:")) - sort.Strings(fixed[i]) + var ( + rootMap = make(map[string]bool) + nonrootMap = make(map[string]bool) + root []string + nonroot []string + ) + // first two lines are header + for _, line := range lines[2:] { + parts := strings.Fields(line) + var ( + placeToGo map[string]bool + keyID string + ) + if strings.TrimSpace(parts[0]) == "root" { + // no gun, so there are only 3 fields + placeToGo, keyID = rootMap, parts[1] + } else { + // gun comes between role and key ID + placeToGo, keyID = nonrootMap, parts[2] + } + // keys are 32-chars long (32 byte shasum, hex-encoded) + assert.Len(t, keyID, 64) + placeToGo[keyID] = true + } + for k := range rootMap { + root = append(root, k) + } + for k := range nonrootMap { + nonroot = append(nonroot, k) } - return fixed[0], fixed[1] + return root, nonroot } // List keys, parses the output, and asserts something about the number of root @@ -186,24 +212,19 @@ func GetKeys(t *testing.T, tempDir string) ([]string, []string) { func assertNumKeys(t *testing.T, tempDir string, numRoot, numSigning int, rootOnDisk bool) ([]string, []string) { - uniqueKeys := make(map[string]struct{}) - root, signing := GetKeys(t, tempDir) + root, signing := getUniqueKeys(t, tempDir) + assert.Len(t, root, numRoot) assert.Len(t, signing, numSigning) - for i, rootKeyLine := range root { - keyID := strings.Split(rootKeyLine, "-")[0] - keyID = strings.TrimSpace(keyID) - root[i] = keyID - uniqueKeys[keyID] = struct{}{} + for _, rootKeyID := range root { _, err := os.Stat(filepath.Join( - tempDir, "private", "root_keys", keyID+"_root.key")) + tempDir, "private", "root_keys", rootKeyID+"_root.key")) // os.IsExist checks to see if the error is because a file already // exist, and hence doesn't actually the right funciton to use here assert.Equal(t, rootOnDisk, !os.IsNotExist(err)) // this function is declared is in the build-tagged setup files - verifyRootKeyOnHardware(t, keyID) + verifyRootKeyOnHardware(t, rootKeyID) } - assert.Len(t, uniqueKeys, numRoot) return root, signing } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 0d1d5e07e0..6de84c6929 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -2,6 +2,8 @@ package main import ( "archive/zip" + "fmt" + "io" "os" "path/filepath" "sort" @@ -13,6 +15,7 @@ import ( "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" + "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -88,6 +91,105 @@ var cmdKeyImportRoot = &cobra.Command{ Run: keysImportRoot, } +func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string { + if len(str) <= maxWidth { + return str + } + if leftTruncate { + return fmt.Sprintf("...%s", str[len(str)-(maxWidth-3):]) + } + return fmt.Sprintf("%s...", str[:maxWidth-3]) +} + +const ( + maxGUNWidth = 25 + maxLocWidth = 40 +) + +type keyInfo struct { + gun string // assumption that this is "" if role is root + role string + keyID string + location string +} + +// We want to sort by gun, then by role, then by keyID, then by location +// In the case of a root role, then there is no GUN, and a root role comes +// first. +type keyInfoSorter []keyInfo + +func (k keyInfoSorter) Len() int { return len(k) } +func (k keyInfoSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } +func (k keyInfoSorter) Less(i, j int) bool { + // special-case role + if k[i].role != k[j].role { + if k[i].role == data.CanonicalRootRole { + return true + } + if k[j].role == data.CanonicalRootRole { + return false + } + // otherwise, neither of them are root, they're just different, so + // go with the traditional sort order. + } + + // sort order is GUN, role, keyID, location. + orderedI := []string{k[i].gun, k[i].role, k[i].keyID, k[i].location} + orderedJ := []string{k[j].gun, k[j].role, k[j].keyID, k[j].location} + + for x := 0; x < 4; x++ { + switch { + case orderedI[x] < orderedJ[x]: + return true + case orderedI[x] > orderedJ[x]: + return false + } + // continue on and evalulate the next item + } + // this shouldn't happen - that means two values are exactly equal + return false +} + +// Given a list of KeyStores in order of listing preference, pretty-prints the +// root keys and then the signing keys. +func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) { + var info []keyInfo + + for _, store := range keyStores { + for keyPath, role := range store.ListKeys() { + gun := "" + if role != data.CanonicalRootRole { + gun = filepath.Dir(keyPath) + } + info = append(info, keyInfo{ + role: role, + location: store.Name(), + gun: gun, + keyID: filepath.Base(keyPath), + }) + } + } + sort.Stable(keyInfoSorter(info)) + + table := tablewriter.NewWriter(writer) + table.SetHeader([]string{"ROLE", "GUN", "KEY ID", "LOCATION"}) + table.SetBorder(false) + table.SetColumnSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("-") + table.SetAutoWrapText(false) + + for _, oneKeyInfo := range info { + table.Append([]string{ + oneKeyInfo.role, + truncateWithEllipsis(oneKeyInfo.gun, maxGUNWidth, true), + oneKeyInfo.keyID, + truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true), + }) + } + table.Render() +} + func keysList(cmd *cobra.Command, args []string) { if len(args) > 0 { cmd.Usage() @@ -97,42 +199,9 @@ func keysList(cmd *cobra.Command, args []string) { parseConfig() stores := getKeyStores(cmd, mainViper.GetString("trust_dir"), retriever, true) - - keys := make(map[trustmanager.KeyStore]map[string]string) - for _, store := range stores { - keys[store] = store.ListKeys() - } - cmd.Println("") - cmd.Println("# Root keys: ") - for store, keysMap := range keys { - for k, v := range keysMap { - if v == "root" { - cmd.Println(k, "-", store.Name()) - } - } - } - + prettyPrintKeys(stores, cmd.Out()) cmd.Println("") - cmd.Println("# Signing keys: ") - - // Get a list of all the keys - for store, keysMap := range keys { - var sortedKeys []string - for k := range keysMap { - sortedKeys = append(sortedKeys, k) - } - - // Sort the list of all the keys - sort.Strings(sortedKeys) - - // Print a sorted list of the key/role - for _, k := range sortedKeys { - if keysMap[k] != "root" { - printKey(cmd, k, keysMap[k], store.Name()) - } - } - } } func keysGenerateRootKey(cmd *cobra.Command, args []string) { diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go new file mode 100644 index 0000000000..d3c1a55126 --- /dev/null +++ b/cmd/notary/keys_test.go @@ -0,0 +1,113 @@ +package main + +import ( + "bytes" + "crypto/rand" + "fmt" + "io/ioutil" + "reflect" + "sort" + "strings" + "testing" + + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/stretchr/testify/assert" +) + +func TestTruncateWithEllipsis(t *testing.T) { + digits := "1234567890" + // do not truncate + assert.Equal(t, truncateWithEllipsis(digits, 10, true), digits) + assert.Equal(t, truncateWithEllipsis(digits, 10, false), digits) + assert.Equal(t, truncateWithEllipsis(digits, 11, true), digits) + assert.Equal(t, truncateWithEllipsis(digits, 11, false), digits) + + // left and right truncate + assert.Equal(t, truncateWithEllipsis(digits, 8, true), "...67890") + assert.Equal(t, truncateWithEllipsis(digits, 8, false), "12345...") +} + +func TestKeyInfoSorter(t *testing.T) { + expected := []keyInfo{ + {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "i"}, + {role: data.CanonicalRootRole, gun: "", keyID: "a", location: "j"}, + {role: data.CanonicalRootRole, gun: "", keyID: "z", location: "z"}, + {role: "a", gun: "a", keyID: "a", location: "y"}, + {role: "b", gun: "a", keyID: "a", location: "y"}, + {role: "b", gun: "a", keyID: "b", location: "y"}, + {role: "b", gun: "a", keyID: "b", location: "z"}, + {role: "a", gun: "b", keyID: "a", location: "z"}, + } + jumbled := make([]keyInfo, len(expected)) + // randomish indices + for j, e := range []int{3, 6, 1, 4, 0, 7, 5, 2} { + jumbled[j] = expected[e] + } + + sort.Sort(keyInfoSorter(jumbled)) + assert.True(t, reflect.DeepEqual(expected, jumbled), + fmt.Sprintf("Expected %v, Got %v", expected, jumbled)) +} + +type otherMemoryStore struct { + trustmanager.KeyMemoryStore +} + +func (l *otherMemoryStore) Name() string { + return strings.Repeat("z", 70) +} + +// Given a list of key stores, the keys should be pretty-printed with their +// roles, locations, IDs, and guns first in sorted order in the key store +func TestPrettyPrintKeys(t *testing.T) { + ret := passphrase.ConstantRetriever("pass") + keyStores := []trustmanager.KeyStore{ + trustmanager.NewKeyMemoryStore(ret), + &otherMemoryStore{KeyMemoryStore: *trustmanager.NewKeyMemoryStore(ret)}, + } + + longNameShortened := "..." + strings.Repeat("z", 37) + + // just use the same key for testing + key, err := trustmanager.GenerateED25519Key(rand.Reader) + assert.NoError(t, err) + + root := data.CanonicalRootRole + + // add keys to the key stores + err = keyStores[0].AddKey(key.ID(), root, key) + assert.NoError(t, err) + + err = keyStores[1].AddKey(key.ID(), root, key) + assert.NoError(t, err) + + err = keyStores[0].AddKey(strings.Repeat("a/", 30)+key.ID(), "targets", key) + assert.NoError(t, err) + + err = keyStores[1].AddKey("short/gun/"+key.ID(), "snapshot", key) + assert.NoError(t, err) + + expected := [][]string{ + {root, key.ID(), keyStores[0].Name()}, + {root, key.ID(), longNameShortened}, + {"targets", "..." + strings.Repeat("/a", 11), key.ID(), keyStores[0].Name()}, + {"snapshot", "short/gun", key.ID(), longNameShortened}, + } + + var b bytes.Buffer + prettyPrintKeys(keyStores, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, len(expected)+2) + for i, line := range lines[2:] { + // we are purposely not putting spaces in test data so easier to split + splitted := strings.Fields(line) + for j, v := range splitted { + assert.Equal(t, expected[i][j], strings.TrimSpace(v)) + } + } +}