Pretty-print the key list in a deterministic sorted order.

Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
Ying Li
2015-11-13 01:01:05 -08:00
parent 45de2828b5
commit 39c682327e
3 changed files with 260 additions and 57 deletions

View File

@@ -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
}

View File

@@ -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) {

113
cmd/notary/keys_test.go Normal file
View File

@@ -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))
}
}
}