Skip to content

Commit ff09ef8

Browse files
committed
JGC-479 - Add --format flag support to create-evidence
1 parent bb6297e commit ff09ef8

5 files changed

Lines changed: 261 additions & 37 deletions

File tree

evidence/cli/command/command_cli.go

Lines changed: 134 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
package command
22

33
import (
4+
"encoding/json"
45
"errors"
56
"fmt"
7+
"io"
68
"os"
79
"slices"
810
"strings"
911

12+
"github.com/jedib0t/go-pretty/v6/table"
13+
"github.com/jedib0t/go-pretty/v6/text"
1014
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/application"
1115
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/artifacts"
1216
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/build"
1317
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
1418
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/github"
15-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
16-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
19+
_interface "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/interface"
20+
_package "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/package"
1721
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/releasebundle"
1822
commandUtils "github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
1923
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
24+
"github.com/jfrog/jfrog-cli-evidence/evidence/model"
2025

2126
commonCliUtils "github.com/jfrog/jfrog-cli-core/v2/common/cliutils"
2227
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
28+
"github.com/jfrog/jfrog-cli-core/v2/common/format"
2329
pluginsCommon "github.com/jfrog/jfrog-cli-core/v2/plugins/common"
2430
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
2531
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
@@ -37,15 +43,23 @@ import (
3743
generateCmd "github.com/jfrog/jfrog-cli-evidence/evidence/generate"
3844
)
3945

46+
// responseCollector is implemented by evidence create commands that accumulate
47+
// CreateResponse values during Run(). The interface is checked via type assertion
48+
// after command execution so that printCreateEvidenceResponse can format output.
49+
type responseCollector interface {
50+
CollectedResponses() []*model.CreateResponse
51+
}
52+
4053
func GetCommands() []components.Command {
4154
return []components.Command{
4255
{
43-
Name: "create-evidence",
44-
Aliases: []string{"create"},
45-
Flags: flags.GetCommandFlags(flags.CreateEvidence),
46-
Description: create.GetDescription(),
47-
Arguments: create.GetArguments(),
48-
Action: createEvidence,
56+
Name: "create-evidence",
57+
Aliases: []string{"create"},
58+
Flags: flags.GetCommandFlags(flags.CreateEvidence),
59+
Description: create.GetDescription(),
60+
Arguments: create.GetArguments(),
61+
Action: createEvidence,
62+
SupportedFormats: []format.OutputFormat{format.Json, format.Table},
4963
},
5064
{
5165
Name: "get-evidence",
@@ -74,8 +88,10 @@ func GetCommands() []components.Command {
7488
}
7589
}
7690

77-
var execFunc = commands.Exec
78-
var ErrUnsupportedSubject = errors.New("unsupported subject")
91+
var (
92+
execFunc = commands.Exec
93+
ErrUnsupportedSubject = errors.New("unsupported subject")
94+
)
7995

8096
func createEvidence(ctx *components.Context) error {
8197
if err := validateCreateEvidenceCommonContext(ctx); err != nil {
@@ -90,23 +106,50 @@ func createEvidence(ctx *components.Context) error {
90106
return err
91107
}
92108

93-
if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
94-
return github.NewEvidenceGitHubCommand(ctx, execFunc).CreateEvidence(ctx, serverDetails)
109+
outputFormat, err := ctx.GetOutputFormat()
110+
if err != nil {
111+
return err
95112
}
96113

97-
evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
98-
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
99-
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
100-
flags.BuildName: build.NewEvidenceBuildCommand,
101-
flags.PackageName: _package.NewEvidencePackageCommand,
102-
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
114+
// Wrap execFunc to capture collected responses after each command runs.
115+
var collectedResponses []*model.CreateResponse
116+
collectingExec := func(cmd commands.Command) error {
117+
if runErr := execFunc(cmd); runErr != nil {
118+
return runErr
119+
}
120+
if rc, ok := cmd.(responseCollector); ok {
121+
collectedResponses = append(collectedResponses, rc.CollectedResponses()...)
122+
}
123+
return nil
103124
}
104125

105-
if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
106-
return commandFunc(ctx, execFunc).CreateEvidence(ctx, serverDetails)
126+
var createErr error
127+
if slices.Contains(evidenceType, flags.TypeFlag) || (slices.Contains(evidenceType, flags.BuildName) && slices.Contains(evidenceType, flags.TypeFlag)) {
128+
createErr = github.NewEvidenceGitHubCommand(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
129+
} else {
130+
evidenceCommands := map[string]func(*components.Context, commandUtils.ExecCommandFunc) _interface.EvidenceCommands{
131+
flags.SubjectRepoPath: artifacts.NewEvidenceCustomCommand,
132+
flags.ReleaseBundle: releasebundle.NewEvidenceReleaseBundleCommand,
133+
flags.BuildName: build.NewEvidenceBuildCommand,
134+
flags.PackageName: _package.NewEvidencePackageCommand,
135+
flags.ApplicationKey: application.NewEvidenceApplicationCommand,
136+
}
137+
138+
if commandFunc, exists := evidenceCommands[evidenceType[0]]; exists {
139+
createErr = commandFunc(ctx, collectingExec).CreateEvidence(ctx, serverDetails)
140+
} else {
141+
return ErrUnsupportedSubject
142+
}
107143
}
108144

109-
return ErrUnsupportedSubject
145+
if createErr != nil {
146+
return createErr
147+
}
148+
149+
if outputFormat != "" {
150+
return printCreateEvidenceResponse(os.Stdout, outputFormat, collectedResponses)
151+
}
152+
return nil
110153
}
111154

112155
func getEvidence(ctx *components.Context) error {
@@ -487,3 +530,73 @@ func generateKeyPair(ctx *components.Context) error {
487530
cmd := generateCmd.NewGenerateKeyPairCommand(serverDetails, uploadKey, alias, keyFilePath, fileName)
488531
return cmd.Run()
489532
}
533+
534+
// tableFieldOrder defines the display order for create-evidence table output.
535+
var tableFieldOrder = []struct {
536+
key string
537+
value func(*model.CreateResponse) string
538+
}{
539+
{"repository", func(r *model.CreateResponse) string { return r.Repository }},
540+
{"path", func(r *model.CreateResponse) string { return r.Path }},
541+
{"name", func(r *model.CreateResponse) string { return r.Name }},
542+
{"uri", func(r *model.CreateResponse) string { return r.Uri }},
543+
{"sha256", func(r *model.CreateResponse) string { return r.Sha256 }},
544+
{"predicate_type", func(r *model.CreateResponse) string { return r.PredicateType }},
545+
{"predicate_category", func(r *model.CreateResponse) string { return r.PredicateCategory }},
546+
{"predicate_slug", func(r *model.CreateResponse) string { return r.PredicateSlug }},
547+
{"created_at", func(r *model.CreateResponse) string { return r.CreatedAt }},
548+
{"created_by", func(r *model.CreateResponse) string { return r.CreatedBy }},
549+
{"verified", func(r *model.CreateResponse) string {
550+
if r.Verified {
551+
return "true"
552+
}
553+
return "false"
554+
}},
555+
{"provider_id", func(r *model.CreateResponse) string { return r.ProviderId }},
556+
}
557+
558+
// printCreateEvidenceResponse writes formatted output for the given responses.
559+
func printCreateEvidenceResponse(w io.Writer, outputFormat format.OutputFormat, responses []*model.CreateResponse) error {
560+
switch outputFormat {
561+
case format.Json:
562+
return printCreateEvidenceJSON(w, responses)
563+
case format.Table:
564+
return printCreateEvidenceTable(w, responses)
565+
default:
566+
return fmt.Errorf("unsupported format %q: accepted values are json, table", outputFormat)
567+
}
568+
}
569+
570+
func printCreateEvidenceJSON(w io.Writer, responses []*model.CreateResponse) error {
571+
enc := json.NewEncoder(w)
572+
enc.SetIndent("", " ")
573+
if len(responses) == 1 {
574+
return enc.Encode(responses[0])
575+
}
576+
return enc.Encode(responses)
577+
}
578+
579+
func printCreateEvidenceTable(w io.Writer, responses []*model.CreateResponse) error {
580+
for i, r := range responses {
581+
if i > 0 {
582+
fmt.Fprintln(w)
583+
}
584+
t := table.NewWriter()
585+
t.SetOutputMirror(w)
586+
t.SetStyle(table.StyleLight)
587+
t.Style().Options.SeparateRows = false
588+
t.AppendHeader(table.Row{"FIELD", "VALUE"})
589+
t.SetColumnConfigs([]table.ColumnConfig{
590+
{Number: 1, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
591+
{Number: 2, Align: text.AlignLeft, AlignHeader: text.AlignLeft},
592+
})
593+
for _, field := range tableFieldOrder {
594+
val := field.value(r)
595+
if val != "" {
596+
t.AppendRow(table.Row{field.key, val})
597+
}
598+
}
599+
t.Render()
600+
}
601+
return nil
602+
}

evidence/cli/command/command_cli_test.go

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
package command
22

33
import (
4+
"bytes"
45
"flag"
5-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
6-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
7-
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
8-
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
96
"os"
7+
"strings"
108
"testing"
119

1210
"github.com/jfrog/jfrog-cli-core/v2/common/commands"
11+
"github.com/jfrog/jfrog-cli-core/v2/common/format"
1312
"github.com/jfrog/jfrog-cli-core/v2/plugins/components"
1413
coreUtils "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
14+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/flags"
15+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/command/utils"
16+
"github.com/jfrog/jfrog-cli-evidence/evidence/cli/test"
17+
evdConfig "github.com/jfrog/jfrog-cli-evidence/evidence/config"
18+
"github.com/jfrog/jfrog-cli-evidence/evidence/model"
1519
"github.com/stretchr/testify/assert"
1620
"github.com/urfave/cli"
1721
"go.uber.org/mock/gomock"
@@ -878,3 +882,105 @@ func TestValidateCreateEvidenceCommonContext_Attachments(t *testing.T) {
878882
assert.Error(t, validateCreateEvidenceCommonContext(c))
879883
})
880884
}
885+
886+
// ── printCreateEvidenceResponse tests ──────────────────────────────────────────
887+
888+
func sampleResponse() *model.CreateResponse {
889+
return &model.CreateResponse{
890+
Repository: "my-repo",
891+
Path: "com/example",
892+
Name: "artifact-1.0.jar",
893+
Uri: "https://example.jfrog.io/my-repo/com/example/artifact-1.0.jar",
894+
Sha256: "abc123def456",
895+
PredicateType: "https://in-toto.io/attestation/vulns",
896+
PredicateCategory: "SPDX",
897+
PredicateSlug: "vulns",
898+
CreatedAt: "2024-01-15T10:30:00Z",
899+
CreatedBy: "ci-user",
900+
Verified: true,
901+
ProviderId: "jfrog",
902+
}
903+
}
904+
905+
func TestPrintCreateEvidenceResponse_JSON(t *testing.T) {
906+
var buf bytes.Buffer
907+
r := sampleResponse()
908+
909+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r})
910+
assert.NoError(t, err)
911+
912+
out := buf.String()
913+
// Must be valid JSON.
914+
assert.True(t, strings.HasPrefix(strings.TrimSpace(out), "{"), "expected JSON object, got: %s", out)
915+
// Must contain key fields.
916+
assert.Contains(t, out, `"repository"`)
917+
assert.Contains(t, out, `"my-repo"`)
918+
assert.Contains(t, out, `"sha256"`)
919+
assert.Contains(t, out, `"abc123def456"`)
920+
assert.Contains(t, out, `"verified"`)
921+
assert.Contains(t, out, `true`)
922+
}
923+
924+
func TestPrintCreateEvidenceResponse_JSON_MultipleResponses(t *testing.T) {
925+
var buf bytes.Buffer
926+
r1 := sampleResponse()
927+
r2 := sampleResponse()
928+
r2.Name = "artifact-2.0.jar"
929+
930+
err := printCreateEvidenceResponse(&buf, format.Json, []*model.CreateResponse{r1, r2})
931+
assert.NoError(t, err)
932+
933+
out := buf.String()
934+
// Multiple responses should be encoded as a JSON array.
935+
assert.True(t, strings.HasPrefix(strings.TrimSpace(out), "["), "expected JSON array, got: %s", out)
936+
assert.Contains(t, out, `"artifact-1.0.jar"`)
937+
assert.Contains(t, out, `"artifact-2.0.jar"`)
938+
}
939+
940+
func TestPrintCreateEvidenceResponse_Table(t *testing.T) {
941+
var buf bytes.Buffer
942+
r := sampleResponse()
943+
944+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
945+
assert.NoError(t, err)
946+
947+
out := buf.String()
948+
// Table must have a FIELD / VALUE header.
949+
assert.Contains(t, out, "FIELD")
950+
assert.Contains(t, out, "VALUE")
951+
// Known field names and values should appear.
952+
assert.Contains(t, out, "repository")
953+
assert.Contains(t, out, "my-repo")
954+
assert.Contains(t, out, "sha256")
955+
assert.Contains(t, out, "abc123def456")
956+
assert.Contains(t, out, "verified")
957+
assert.Contains(t, out, "true")
958+
}
959+
960+
func TestPrintCreateEvidenceResponse_Table_AbsentFieldsOmitted(t *testing.T) {
961+
var buf bytes.Buffer
962+
// Sparse response – only repository is set.
963+
r := &model.CreateResponse{
964+
Repository: "sparse-repo",
965+
}
966+
967+
err := printCreateEvidenceResponse(&buf, format.Table, []*model.CreateResponse{r})
968+
assert.NoError(t, err)
969+
970+
out := buf.String()
971+
assert.Contains(t, out, "repository")
972+
assert.Contains(t, out, "sparse-repo")
973+
// Fields that are empty must not appear in the table.
974+
assert.NotContains(t, out, "sha256")
975+
assert.NotContains(t, out, "predicate_type")
976+
assert.NotContains(t, out, "provider_id")
977+
}
978+
979+
func TestPrintCreateEvidenceResponse_UnsupportedFormat(t *testing.T) {
980+
var buf bytes.Buffer
981+
r := sampleResponse()
982+
983+
err := printCreateEvidenceResponse(&buf, format.OutputFormat("xml"), []*model.CreateResponse{r})
984+
assert.Error(t, err)
985+
assert.Contains(t, err.Error(), "unsupported format")
986+
}

evidence/create/create_base.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ type createEvidenceBase struct {
5353
artifactoryClient artifactory.ArtifactoryServicesManager
5454
uploader evidenceUploader
5555
stmtResolver sonar.StatementResolver
56+
collectedResponses []*model.CreateResponse
57+
}
58+
59+
// CollectedResponses returns the list of CreateResponse objects gathered during evidence upload.
60+
func (c *createEvidenceBase) CollectedResponses() []*model.CreateResponse {
61+
return c.collectedResponses
5662
}
5763

5864
const EvdDefaultUser = "JFrog CLI"
@@ -275,6 +281,7 @@ func (c *createEvidenceBase) uploadEvidence(evidencePayload []byte, repoPath str
275281
} else {
276282
log.Info("Evidence successfully created but not verified due to missing/invalid public key")
277283
}
284+
c.collectedResponses = append(c.collectedResponses, createResponse)
278285
return createResponse, nil
279286
}
280287

0 commit comments

Comments
 (0)