Merge pull request #934 from n4ss/refactor-trust-inspect

Refactor trust view command into a --pretty flag on trust inspect
This commit is contained in:
Victor Vieux 2018-03-09 13:25:01 -08:00 committed by GitHub
commit 2731c71c99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 124 additions and 87 deletions

View File

@ -16,7 +16,6 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
Annotations: map[string]string{"experimentalCLI": ""}, Annotations: map[string]string{"experimentalCLI": ""},
} }
cmd.AddCommand( cmd.AddCommand(
newViewCommand(dockerCli),
newRevokeCommand(dockerCli), newRevokeCommand(dockerCli),
newSignCommand(dockerCli), newSignCommand(dockerCli),
newTrustKeyCommand(dockerCli), newTrustKeyCommand(dockerCli),

View File

@ -2,6 +2,7 @@ package trust
import ( import (
"encoding/json" "encoding/json"
"fmt"
"sort" "sort"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
@ -11,24 +12,55 @@ import (
"github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/data"
) )
type inspectOptions struct {
remotes []string
// FIXME(n4ss): this is consistent with `docker service inspect` but we should provide
// a `--format` flag too. (format and pretty-print should be exclusive)
prettyPrint bool
}
func newInspectCommand(dockerCli command.Cli) *cobra.Command { func newInspectCommand(dockerCli command.Cli) *cobra.Command {
options := inspectOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]", Use: "inspect IMAGE[:TAG] [IMAGE[:TAG]...]",
Short: "Return low-level information about keys and signatures", Short: "Return low-level information about keys and signatures",
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runInspect(dockerCli, args) options.remotes = args
return runInspect(dockerCli, options)
}, },
} }
flags := cmd.Flags()
flags.BoolVar(&options.prettyPrint, "pretty", false, "Print the information in a human friendly format")
return cmd return cmd
} }
func runInspect(dockerCli command.Cli, remotes []string) error { func runInspect(dockerCli command.Cli, opts inspectOptions) error {
if opts.prettyPrint {
var err error
for index, remote := range opts.remotes {
if err = prettyPrintTrustInfo(dockerCli, remote); err != nil {
return err
}
// Additional separator between the inspection output of each image
if index < len(opts.remotes)-1 {
fmt.Fprint(dockerCli.Out(), "\n\n")
}
}
return err
}
getRefFunc := func(ref string) (interface{}, []byte, error) { getRefFunc := func(ref string) (interface{}, []byte, error) {
i, err := getRepoTrustInfo(dockerCli, ref) i, err := getRepoTrustInfo(dockerCli, ref)
return nil, i, err return nil, i, err
} }
return inspect.Inspect(dockerCli.Out(), remotes, "", getRefFunc) return inspect.Inspect(dockerCli.Out(), opts.remotes, "", getRefFunc)
} }
func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) { func getRepoTrustInfo(cli command.Cli, remote string) ([]byte, error) {

View File

@ -4,34 +4,21 @@ import (
"fmt" "fmt"
"io" "io"
"sort" "sort"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/spf13/cobra"
"github.com/theupdateframework/notary/client" "github.com/theupdateframework/notary/client"
) )
func newViewCommand(dockerCli command.Cli) *cobra.Command { func prettyPrintTrustInfo(cli command.Cli, remote string) error {
cmd := &cobra.Command{
Use: "view IMAGE[:TAG]",
Short: "Display detailed information about keys and signatures",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return viewTrustInfo(dockerCli, args[0])
},
}
return cmd
}
func viewTrustInfo(cli command.Cli, remote string) error {
signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote) signatureRows, adminRolesWithSigs, delegationRoles, err := lookupTrustInfo(cli, remote)
if err != nil { if err != nil {
return err return err
} }
if len(signatureRows) > 0 { if len(signatureRows) > 0 {
fmt.Fprintf(cli.Out(), "\nSignatures for %s\n\n", remote)
if err := printSignatures(cli.Out(), signatureRows); err != nil { if err := printSignatures(cli.Out(), signatureRows); err != nil {
return err return err
} }
@ -42,14 +29,14 @@ func viewTrustInfo(cli command.Cli, remote string) error {
// If we do not have additional signers, do not display // If we do not have additional signers, do not display
if len(signerRoleToKeyIDs) > 0 { if len(signerRoleToKeyIDs) > 0 {
fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s:\n\n", strings.Split(remote, ":")[0]) fmt.Fprintf(cli.Out(), "\nList of signers and their keys for %s\n\n", remote)
if err := printSignerInfo(cli.Out(), signerRoleToKeyIDs); err != nil { if err := printSignerInfo(cli.Out(), signerRoleToKeyIDs); err != nil {
return err return err
} }
} }
// This will always have the root and targets information // This will always have the root and targets information
fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s:\n", strings.Split(remote, ":")[0]) fmt.Fprintf(cli.Out(), "\nAdministrative keys for %s\n\n", remote)
printSortedAdminKeys(cli.Out(), adminRolesWithSigs) printSortedAdminKeys(cli.Out(), adminRolesWithSigs)
return nil return nil
} }
@ -57,7 +44,9 @@ func viewTrustInfo(cli command.Cli, remote string) error {
func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) { func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) {
sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name }) sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name })
for _, adminRole := range adminRoles { for _, adminRole := range adminRoles {
fmt.Fprintf(out, "%s", formatAdminRole(adminRole)) if formattedAdminRole := formatAdminRole(adminRole); formattedAdminRole != "" {
fmt.Fprintf(out, " %s", formattedAdminRole)
}
} }
} }

View File

@ -17,11 +17,13 @@ import (
"github.com/theupdateframework/notary/tuf/data" "github.com/theupdateframework/notary/tuf/data"
) )
// TODO(n4ss): remove common tests with the regular inspect command
type fakeClient struct { type fakeClient struct {
dockerClient.Client dockerClient.Client
} }
func TestTrustViewCommandErrors(t *testing.T) { func TestTrustInspectPrettyCommandErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@ -29,12 +31,7 @@ func TestTrustViewCommandErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires at least 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
}, },
{ {
name: "sha-reference", name: "sha-reference",
@ -48,104 +45,115 @@ func TestTrustViewCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
cmd := newViewCommand( cmd := newInspectCommand(
test.NewFakeCli(&fakeClient{})) test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
cmd.Flags().Set("pretty", "true")
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
func TestTrustViewCommandOfflineErrors(t *testing.T) { func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"nonexistent-reg-name.io/image"}) cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
cli = test.NewFakeCli(&fakeClient{}) cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository) cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository)
cmd = newViewCommand(cli) cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"}) cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image") assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
} }
func TestTrustViewCommandUninitializedErrors(t *testing.T) { func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"reg/unsigned-img"}) cmd.SetArgs([]string{"reg/unsigned-img"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img") assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
cli = test.NewFakeCli(&fakeClient{}) cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository) cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository)
cmd = newViewCommand(cli) cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"reg/unsigned-img:tag"}) cmd.SetArgs([]string{"reg/unsigned-img:tag"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag") assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
} }
func TestTrustViewCommandEmptyNotaryRepoErrors(t *testing.T) { func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"reg/img:unsigned-tag"}) cmd.SetArgs([]string{"reg/img:unsigned-tag"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag"))
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img:")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img"))
cli = test.NewFakeCli(&fakeClient{}) cli = test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository) cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository)
cmd = newViewCommand(cli) cmd = newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"reg/img"}) cmd.SetArgs([]string{"reg/img"})
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img"))
assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img:")) assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img"))
} }
func TestTrustViewCommandFullRepoWithoutSigners(t *testing.T) { func TestTrustInspectPrettyCommandFullRepoWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"signed-repo"}) cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-no-signers.golden") golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-no-signers.golden")
} }
func TestTrustViewCommandOneTagWithoutSigners(t *testing.T) { func TestTrustInspectPrettyCommandOneTagWithoutSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository) cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"signed-repo:green"}) cmd.SetArgs([]string{"signed-repo:green"})
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-view-one-tag-no-signers.golden") golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-one-tag-no-signers.golden")
} }
func TestTrustViewCommandFullRepoWithSigners(t *testing.T) { func TestTrustInspectPrettyCommandFullRepoWithSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"signed-repo"}) cmd.SetArgs([]string{"signed-repo"})
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-view-full-repo-with-signers.golden") golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-with-signers.golden")
} }
func TestTrustViewCommandUnsignedTagInSignedRepo(t *testing.T) { func TestTrustInspectPrettyCommandUnsignedTagInSignedRepo(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{}) cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository) cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository)
cmd := newViewCommand(cli) cmd := newInspectCommand(cli)
cmd.Flags().Set("pretty", "true")
cmd.SetArgs([]string{"signed-repo:unsigned"}) cmd.SetArgs([]string{"signed-repo:unsigned"})
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
golden.Assert(t, cli.OutBuffer().String(), "trust-view-unsigned-tag-with-signers.golden") golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-unsigned-tag-with-signers.golden")
} }
func TestNotaryRoleToSigner(t *testing.T) { func TestNotaryRoleToSigner(t *testing.T) {

View File

@ -18,12 +18,7 @@ func TestTrustInspectCommandErrors(t *testing.T) {
}{ }{
{ {
name: "not-enough-args", name: "not-enough-args",
expectedError: "requires exactly 1 argument", expectedError: "requires at least 1 argument",
},
{
name: "too-many-args",
args: []string{"remote1", "remote2"},
expectedError: "requires exactly 1 argument",
}, },
{ {
name: "sha-reference", name: "sha-reference",
@ -37,8 +32,9 @@ func TestTrustInspectCommandErrors(t *testing.T) {
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
cmd := newViewCommand( cmd := newInspectCommand(
test.NewFakeCli(&fakeClient{})) test.NewFakeCli(&fakeClient{}))
cmd.Flags().Set("pretty", "true")
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard) cmd.SetOutput(ioutil.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)

View File

@ -1,6 +1,10 @@
Signatures for signed-repo
SIGNED TAG DIGEST SIGNERS SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin) green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo: Administrative keys for signed-repo
Repository Key: targetsID
Root Key: rootID Repository Key: targetsID
Root Key: rootID

View File

@ -1,14 +1,18 @@
Signatures for signed-repo
SIGNED TAG DIGEST SIGNERS SIGNED TAG DIGEST SIGNERS
blue 626c75652d646967657374 alice blue 626c75652d646967657374 alice
green 677265656e2d646967657374 (Repo Admin) green 677265656e2d646967657374 (Repo Admin)
red 7265642d646967657374 alice, bob red 7265642d646967657374 alice, bob
List of signers and their keys for signed-repo: List of signers and their keys for signed-repo
SIGNER KEYS SIGNER KEYS
alice A alice A
bob B bob B
Administrative keys for signed-repo: Administrative keys for signed-repo
Repository Key: targetsID
Root Key: rootID Repository Key: targetsID
Root Key: rootID

View File

@ -0,0 +1,10 @@
Signatures for signed-repo:green
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:green
Repository Key: targetsID
Root Key: rootID

View File

@ -0,0 +1,14 @@
No signatures for signed-repo:unsigned
List of signers and their keys for signed-repo:unsigned
SIGNER KEYS
alice A
bob B
Administrative keys for signed-repo:unsigned
Repository Key: targetsID
Root Key: rootID

View File

@ -1,6 +0,0 @@
SIGNED TAG DIGEST SIGNERS
green 677265656e2d646967657374 (Repo Admin)
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID

View File

@ -1,13 +0,0 @@
No signatures for signed-repo:unsigned
List of signers and their keys for signed-repo:
SIGNER KEYS
alice A
bob B
Administrative keys for signed-repo:
Repository Key: targetsID
Root Key: rootID