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 <ying.li@docker.com>
This commit is contained in:
Ying Li 2015-11-23 14:47:56 -05:00 committed by David Lawrence
parent 1670b69a18
commit 35b104beee
7 changed files with 479 additions and 428 deletions

View File

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

View File

@ -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:], " "))
}
}

View File

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

View File

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

210
cmd/notary/prettyprint.go Normal file
View File

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

View File

@ -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:], " "))
}
}

View File

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