mirror of
https://github.com/docker/docs.git
synced 2026-04-12 14:25:46 +07:00
Pretty-print the key list in a deterministic sorted order.
Signed-off-by: Ying Li <ying.li@docker.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
113
cmd/notary/keys_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user