From 35b104beee713febba1429caae82425ad0ed8c3c Mon Sep 17 00:00:00 2001 From: Ying Li Date: Mon, 23 Nov 2015 14:47:56 -0500 Subject: [PATCH] Move all the pretty-print functions to their own file in cmd/notary. Also add tests for pretty-printing and sorting targets. Signed-off-by: Ying Li --- cmd/notary/cert.go | 69 --------- cmd/notary/cert_test.go | 85 ----------- cmd/notary/keys.go | 112 -------------- cmd/notary/keys_test.go | 122 --------------- cmd/notary/prettyprint.go | 210 +++++++++++++++++++++++++ cmd/notary/prettyprint_test.go | 269 +++++++++++++++++++++++++++++++++ cmd/notary/tuf.go | 40 ----- 7 files changed, 479 insertions(+), 428 deletions(-) delete mode 100644 cmd/notary/cert_test.go create mode 100644 cmd/notary/prettyprint.go create mode 100644 cmd/notary/prettyprint_test.go diff --git a/cmd/notary/cert.go b/cmd/notary/cert.go index 73f93fe132..09d1ae2821 100644 --- a/cmd/notary/cert.go +++ b/cmd/notary/cert.go @@ -2,16 +2,10 @@ package main import ( "crypto/x509" - "fmt" - "io" - "math" "os" - "sort" - "time" "github.com/docker/notary/certs" "github.com/docker/notary/trustmanager" - "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -134,66 +128,3 @@ func certList(cmd *cobra.Command, args []string) { prettyPrintCerts(trustedCerts, cmd.Out()) cmd.Println("") } - -func printCert(cmd *cobra.Command, cert *x509.Certificate) { - timeDifference := cert.NotAfter.Sub(time.Now()) - certID, err := trustmanager.FingerprintCert(cert) - if err != nil { - fatalf("Could not fingerprint certificate: %v", err) - } - - cmd.Printf("%s %s (expires in: %v days)\n", cert.Subject.CommonName, certID, math.Floor(timeDifference.Hours()/24)) -} - -// cert by repo name then expiry time. Don't bother sorting by fingerprint. -type certSorter []*x509.Certificate - -func (t certSorter) Len() int { return len(t) } -func (t certSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } -func (t certSorter) Less(i, j int) bool { - if t[i].Subject.CommonName < t[j].Subject.CommonName { - return true - } else if t[i].Subject.CommonName > t[j].Subject.CommonName { - return false - } - - return t[i].NotAfter.Before(t[j].NotAfter) -} - -// Given a list of Ceritifcates in order of listing preference, pretty-prints -// the cert common name, fingerprint, and expiry -func prettyPrintCerts(certs []*x509.Certificate, writer io.Writer) { - if len(certs) == 0 { - writer.Write([]byte("\nNo trusted root certificates present.\n\n")) - return - } - - sort.Stable(certSorter(certs)) - - table := tablewriter.NewWriter(writer) - table.SetHeader([]string{ - "GUN", "Fingerprint of Trusted Root Certificate", "Expires In"}) - table.SetBorder(false) - table.SetColumnSeparator(" ") - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("-") - table.SetAutoWrapText(false) - - for _, c := range certs { - days := math.Floor(c.NotAfter.Sub(time.Now()).Hours() / 24) - expiryString := "< 1 day" - if days == 1 { - expiryString = "1 day" - } else if days > 1 { - expiryString = fmt.Sprintf("%d days", int(days)) - } - - certID, err := trustmanager.FingerprintCert(c) - if err != nil { - fatalf("Could not fingerprint certificate: %v", err) - } - - table.Append([]string{c.Subject.CommonName, certID, expiryString}) - } - table.Render() -} diff --git a/cmd/notary/cert_test.go b/cmd/notary/cert_test.go deleted file mode 100644 index 48ea8ce0ed..0000000000 --- a/cmd/notary/cert_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "bytes" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "io/ioutil" - "reflect" - "strings" - "testing" - "time" - - "github.com/docker/notary/trustmanager" - "github.com/stretchr/testify/assert" -) - -func generateCertificate(t *testing.T, gun string, expireInHours int64) *x509.Certificate { - template, err := trustmanager.NewCertificate(gun) - assert.NoError(t, err) - template.NotAfter = template.NotBefore.Add( - time.Hour * time.Duration(expireInHours)) - - ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - assert.NoError(t, err) - - certBytes, err := x509.CreateCertificate(rand.Reader, template, template, - ecdsaPrivKey.Public(), ecdsaPrivKey) - assert.NoError(t, err) - - cert, err := x509.ParseCertificate(certBytes) - assert.NoError(t, err) - return cert -} - -// If there are no certs in the cert store store, a message that there are no -// certs should be displayed. -func TestPrettyPrintZeroCerts(t *testing.T) { - var b bytes.Buffer - prettyPrintCerts([]*x509.Certificate{}, &b) - text, err := ioutil.ReadAll(&b) - assert.NoError(t, err) - - lines := strings.Split(strings.TrimSpace(string(text)), "\n") - assert.Len(t, lines, 1) - assert.Equal(t, "No trusted root certificates present.", lines[0]) -} - -// Certificates are pretty-printed in table form sorted by gun and then expiry -func TestPrettySortedCerts(t *testing.T) { - unsorted := []*x509.Certificate{ - generateCertificate(t, "xylitol", 77), // 3 days 5 hours - generateCertificate(t, "xylitol", 12), // less than 1 day - generateCertificate(t, "cheesecake", 25), // a little more than 1 day - generateCertificate(t, "baklava", 239), // almost 10 days - } - - var b bytes.Buffer - prettyPrintCerts(unsorted, &b) - text, err := ioutil.ReadAll(&b) - assert.NoError(t, err) - - expected := [][]string{ - {"baklava", "9 days"}, - {"cheesecake", "1 day"}, - {"xylitol", "< 1 day"}, - {"xylitol", "3 days"}, - } - - lines := strings.Split(strings.TrimSpace(string(text)), "\n") - assert.Len(t, lines, len(expected)+2) - - // starts with headers - assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( - "GUN FINGERPRINT OF TRUSTED ROOT CERTIFICATE EXPIRES IN"))) - assert.Equal(t, "----", lines[1][:4]) - - for i, line := range lines[2:] { - splitted := strings.Fields(line) - assert.True(t, len(splitted) >= 3) - assert.Equal(t, expected[i][0], splitted[0]) - assert.Equal(t, expected[i][1], strings.Join(splitted[2:], " ")) - } -} diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 782e6ede27..b028a606ca 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "sort" "strconv" "strings" @@ -17,7 +16,6 @@ import ( "github.com/docker/notary/trustmanager" "github.com/docker/notary/tuf/data" - "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -101,110 +99,6 @@ var cmdKeyRemove = &cobra.Command{ Run: keyRemove, } -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), - }) - } - } - if len(info) == 0 { - writer.Write([]byte("No signing keys found.\n")) - return - } - - 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() @@ -398,12 +292,6 @@ func keysImportRoot(cmd *cobra.Command, args []string) { } } -func printKey(cmd *cobra.Command, keyPath, alias, loc string) { - keyID := filepath.Base(keyPath) - gun := filepath.Dir(keyPath) - cmd.Printf("%s - %s - %s - %s\n", gun, alias, keyID, loc) -} - func keysRotate(cmd *cobra.Command, args []string) { if len(args) < 1 { cmd.Usage() diff --git a/cmd/notary/keys_test.go b/cmd/notary/keys_test.go index 836eafb608..1b791aa9a1 100644 --- a/cmd/notary/keys_test.go +++ b/cmd/notary/keys_test.go @@ -3,139 +3,17 @@ 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" ) var ret = passphrase.ConstantRetriever("pass") -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 TestPrettyPrintRootAndSigningKeys(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) - - // starts with headers - assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), - []string{"ROLE", "GUN", "KEY", "ID", "LOCATION"})) - assert.Equal(t, "----", lines[1][:4]) - - 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)) - } - } -} - -// If there are no keys in any of the key stores, a message that there are no -// signing keys should be displayed. -func TestPrettyPrintZeroKeys(t *testing.T) { - ret := passphrase.ConstantRetriever("pass") - emptyKeyStore := trustmanager.NewKeyMemoryStore(ret) - - var b bytes.Buffer - prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &b) - text, err := ioutil.ReadAll(&b) - assert.NoError(t, err) - - lines := strings.Split(strings.TrimSpace(string(text)), "\n") - assert.Len(t, lines, 1) - assert.Equal(t, "No signing keys found.", lines[0]) -} - // If there are no keys, removeKeyInteractively will just return an error about // there not being any key func TestRemoveIfNoKey(t *testing.T) { diff --git a/cmd/notary/prettyprint.go b/cmd/notary/prettyprint.go new file mode 100644 index 0000000000..191220b7f7 --- /dev/null +++ b/cmd/notary/prettyprint.go @@ -0,0 +1,210 @@ +package main + +import ( + "crypto/x509" + "encoding/hex" + "fmt" + "io" + "math" + "path/filepath" + "sort" + "time" + + "github.com/docker/notary/client" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/olekukonko/tablewriter" +) + +// returns a tablewriter +func getTable(headers []string, writer io.Writer) *tablewriter.Table { + table := tablewriter.NewWriter(writer) + table.SetBorder(false) + table.SetColumnSeparator(" ") + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("-") + table.SetAutoWrapText(false) + table.SetHeader(headers) + return table +} + +// --- pretty printing certs --- + +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), + }) + } + } + if len(info) == 0 { + writer.Write([]byte("No signing keys found.\n")) + return + } + + sort.Stable(keyInfoSorter(info)) + + table := getTable([]string{"ROLE", "GUN", "KEY ID", "LOCATION"}, writer) + + for _, oneKeyInfo := range info { + table.Append([]string{ + oneKeyInfo.role, + truncateWithEllipsis(oneKeyInfo.gun, maxGUNWidth, true), + oneKeyInfo.keyID, + truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true), + }) + } + table.Render() +} + +// --- pretty printing targets --- + +type targetsSorter []*client.Target + +func (t targetsSorter) Len() int { return len(t) } +func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t targetsSorter) Less(i, j int) bool { + return t[i].Name < t[j].Name +} + +// Given a list of KeyStores in order of listing preference, pretty-prints the +// root keys and then the signing keys. +func prettyPrintTargets(ts []*client.Target, writer io.Writer) { + if len(ts) == 0 { + writer.Write([]byte("\nNo targets present in this repository.\n\n")) + return + } + + sort.Stable(targetsSorter(ts)) + + table := getTable([]string{"Name", "Digest", "Size (bytes)"}, writer) + + for _, t := range ts { + table.Append([]string{ + t.Name, + hex.EncodeToString(t.Hashes["sha256"]), + fmt.Sprintf("%d", t.Length), + }) + } + table.Render() +} + +// --- pretty printing certs --- + +// cert by repo name then expiry time. Don't bother sorting by fingerprint. +type certSorter []*x509.Certificate + +func (t certSorter) Len() int { return len(t) } +func (t certSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t certSorter) Less(i, j int) bool { + if t[i].Subject.CommonName < t[j].Subject.CommonName { + return true + } else if t[i].Subject.CommonName > t[j].Subject.CommonName { + return false + } + + return t[i].NotAfter.Before(t[j].NotAfter) +} + +// Given a list of Ceritifcates in order of listing preference, pretty-prints +// the cert common name, fingerprint, and expiry +func prettyPrintCerts(certs []*x509.Certificate, writer io.Writer) { + if len(certs) == 0 { + writer.Write([]byte("\nNo trusted root certificates present.\n\n")) + return + } + + sort.Stable(certSorter(certs)) + + table := getTable([]string{ + "GUN", "Fingerprint of Trusted Root Certificate", "Expires In"}, writer) + + for _, c := range certs { + days := math.Floor(c.NotAfter.Sub(time.Now()).Hours() / 24) + expiryString := "< 1 day" + if days == 1 { + expiryString = "1 day" + } else if days > 1 { + expiryString = fmt.Sprintf("%d days", int(days)) + } + + certID, err := trustmanager.FingerprintCert(c) + if err != nil { + fatalf("Could not fingerprint certificate: %v", err) + } + + table.Append([]string{c.Subject.CommonName, certID, expiryString}) + } + table.Render() +} diff --git a/cmd/notary/prettyprint_test.go b/cmd/notary/prettyprint_test.go new file mode 100644 index 0000000000..31d4b5d86a --- /dev/null +++ b/cmd/notary/prettyprint_test.go @@ -0,0 +1,269 @@ +package main + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/hex" + "fmt" + "io/ioutil" + "reflect" + "sort" + "strings" + "testing" + "time" + + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/tuf/data" + "github.com/stretchr/testify/assert" +) + +// --- tests for pretty printing keys --- + +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) +} + +// If there are no keys in any of the key stores, a message that there are no +// signing keys should be displayed. +func TestPrettyPrintZeroKeys(t *testing.T) { + ret := passphrase.ConstantRetriever("pass") + emptyKeyStore := trustmanager.NewKeyMemoryStore(ret) + + var b bytes.Buffer + prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, 1) + assert.Equal(t, "No signing keys found.", lines[0]) +} + +// 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 TestPrettyPrintRootAndSigningKeys(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) + + // starts with headers + assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), + []string{"ROLE", "GUN", "KEY", "ID", "LOCATION"})) + assert.Equal(t, "----", lines[1][:4]) + + 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)) + } + } +} + +// --- tests for pretty printing targets --- + +// If there are no targets, no table is printed, only a line saying that there +// are no targets. +func TestPrettyPrintZeroTargets(t *testing.T) { + var b bytes.Buffer + prettyPrintTargets([]*client.Target{}, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, 1) + assert.Equal(t, "No targets present in this repository.", lines[0]) + +} + +// Targets are sorted by name, and the name, SHA256 digest, and size are +// printed. +func TestPrettyPrintSortedTargets(t *testing.T) { + hashes := make([][]byte, 3) + var err error + for i, letter := range []string{"a012", "b012", "c012"} { + hashes[i], err = hex.DecodeString(letter) + assert.NoError(t, err) + } + unsorted := []*client.Target{ + {Name: "zebra", Hashes: data.Hashes{"sha256": hashes[0]}, Length: 8}, + {Name: "abracadabra", Hashes: data.Hashes{"sha256": hashes[1]}, Length: 1}, + {Name: "bee", Hashes: data.Hashes{"sha256": hashes[2]}, Length: 5}, + } + + var b bytes.Buffer + prettyPrintTargets(unsorted, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + expected := [][]string{ + {"abracadabra", "b012", "1"}, + {"bee", "c012", "5"}, + {"zebra", "a012", "8"}, + } + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, len(expected)+2) + + // starts with headers + assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( + "NAME DIGEST SIZE (BYTES)"))) + assert.Equal(t, "----", lines[1][:4]) + + for i, line := range lines[2:] { + splitted := strings.Fields(line) + assert.Equal(t, expected[i], splitted) + } +} + +// --- tests for pretty printing certs --- + +func generateCertificate(t *testing.T, gun string, expireInHours int64) *x509.Certificate { + template, err := trustmanager.NewCertificate(gun) + assert.NoError(t, err) + template.NotAfter = template.NotBefore.Add( + time.Hour * time.Duration(expireInHours)) + + ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.NoError(t, err) + + certBytes, err := x509.CreateCertificate(rand.Reader, template, template, + ecdsaPrivKey.Public(), ecdsaPrivKey) + assert.NoError(t, err) + + cert, err := x509.ParseCertificate(certBytes) + assert.NoError(t, err) + return cert +} + +// If there are no certs in the cert store store, a message that there are no +// certs should be displayed. +func TestPrettyPrintZeroCerts(t *testing.T) { + var b bytes.Buffer + prettyPrintCerts([]*x509.Certificate{}, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, 1) + assert.Equal(t, "No trusted root certificates present.", lines[0]) +} + +// Certificates are pretty-printed in table form sorted by gun and then expiry +func TestPrettyPrintSortedCerts(t *testing.T) { + unsorted := []*x509.Certificate{ + generateCertificate(t, "xylitol", 77), // 3 days 5 hours + generateCertificate(t, "xylitol", 12), // less than 1 day + generateCertificate(t, "cheesecake", 25), // a little more than 1 day + generateCertificate(t, "baklava", 239), // almost 10 days + } + + var b bytes.Buffer + prettyPrintCerts(unsorted, &b) + text, err := ioutil.ReadAll(&b) + assert.NoError(t, err) + + expected := [][]string{ + {"baklava", "9 days"}, + {"cheesecake", "1 day"}, + {"xylitol", "< 1 day"}, + {"xylitol", "3 days"}, + } + + lines := strings.Split(strings.TrimSpace(string(text)), "\n") + assert.Len(t, lines, len(expected)+2) + + // starts with headers + assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields( + "GUN FINGERPRINT OF TRUSTED ROOT CERTIFICATE EXPIRES IN"))) + assert.Equal(t, "----", lines[1][:4]) + + for i, line := range lines[2:] { + splitted := strings.Fields(line) + assert.True(t, len(splitted) >= 3) + assert.Equal(t, expected[i][0], splitted[0]) + assert.Equal(t, expected[i][1], strings.Join(splitted[2:], " ")) + } +} diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index ba91a2c513..4f59076f00 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -3,16 +3,13 @@ package main import ( "bufio" "crypto/sha256" - "encoding/hex" "fmt" - "io" "io/ioutil" "net" "net/http" "net/url" "os" "path/filepath" - "sort" "strings" "time" @@ -25,7 +22,6 @@ import ( notaryclient "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" "github.com/docker/notary/utils" - "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -448,39 +444,3 @@ func getRemoteTrustServer() string { } return remoteTrustServer } - -type targetsSorter []*notaryclient.Target - -func (t targetsSorter) Len() int { return len(t) } -func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] } -func (t targetsSorter) Less(i, j int) bool { - return t[i].Name < t[j].Name -} - -// Given a list of KeyStores in order of listing preference, pretty-prints the -// root keys and then the signing keys. -func prettyPrintTargets(ts []*notaryclient.Target, writer io.Writer) { - if len(ts) == 0 { - writer.Write([]byte("\nNo targets present in this repository.\n\n")) - return - } - - sort.Stable(targetsSorter(ts)) - - table := tablewriter.NewWriter(writer) - table.SetHeader([]string{"Name", "Digest", "Size (bytes)"}) - table.SetBorder(false) - table.SetColumnSeparator(" ") - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("-") - table.SetAutoWrapText(false) - - for _, t := range ts { - table.Append([]string{ - t.Name, - hex.EncodeToString(t.Hashes["sha256"]), - fmt.Sprintf("%d", t.Length), - }) - } - table.Render() -}