mirror of https://github.com/docker/docs.git
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:
parent
1670b69a18
commit
35b104beee
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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:], " "))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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:], " "))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue