Skip to content

Commit f45e8ee

Browse files
extend the list artifacts command to support filtering by repo (#634)
* add repo filter for listing artifacts * fix filter variable assignment
1 parent f504441 commit f45e8ee

3 files changed

Lines changed: 132 additions & 13 deletions

File tree

cmd/kosli/listArtifacts.go

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/spf13/cobra"
1212
)
1313

14-
const listArtifactsShortDesc = `List artifacts in a flow. `
14+
const listArtifactsShortDesc = `List artifacts in a flow or repo. `
1515

1616
const listArtifactsLongDesc = listArtifactsShortDesc + `The results are paginated and ordered from latest to oldest.
1717
By default, the page limit is 15 artifacts per page.
@@ -23,6 +23,12 @@ kosli list artifacts \
2323
--api-token yourAPIToken \
2424
--org yourOrgName
2525
26+
# list the last 15 artifacts for a repo:
27+
kosli list artifacts \
28+
--repo yourRepoName \
29+
--api-token yourAPIToken \
30+
--org yourOrgName
31+
2632
# list the last 30 artifacts for a flow:
2733
kosli list artifacts \
2834
--flow yourFlowName \
@@ -42,8 +48,11 @@ kosli list artifacts \
4248
type listArtifactsOptions struct {
4349
listOptions
4450
flowName string
51+
repoName string
4552
}
4653

54+
var filter string
55+
4756
func newListArtifactsCmd(out io.Writer) *cobra.Command {
4857
o := new(listArtifactsOptions)
4958
cmd := &cobra.Command{
@@ -57,6 +66,10 @@ func newListArtifactsCmd(out io.Writer) *cobra.Command {
5766
if err != nil {
5867
return ErrorBeforePrintingUsage(cmd, err.Error())
5968
}
69+
err = MuXRequiredFlags(cmd, []string{"flow", "repo"}, true)
70+
if err != nil {
71+
return err
72+
}
6073
return o.validate(cmd)
6174
},
6275
RunE: func(cmd *cobra.Command, args []string) error {
@@ -65,19 +78,23 @@ func newListArtifactsCmd(out io.Writer) *cobra.Command {
6578
}
6679

6780
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
81+
cmd.Flags().StringVar(&o.repoName, "repo", "", repoNameFlag)
6882
addListFlags(cmd, &o.listOptions)
6983

70-
err := RequireFlags(cmd, []string{"flow"})
71-
if err != nil {
72-
logger.Error("failed to configure required flags: %v", err)
73-
}
74-
7584
return cmd
7685
}
7786

7887
func (o *listArtifactsOptions) run(out io.Writer) error {
79-
url := fmt.Sprintf("%s/api/v2/artifacts/%s/%s?page=%d&per_page=%d",
80-
global.Host, global.Org, o.flowName, o.pageNumber, o.pageLimit)
88+
var url string
89+
if o.flowName != "" {
90+
filter = "flow"
91+
url = fmt.Sprintf("%s/api/v2/artifacts/%s/%s?page=%d&per_page=%d",
92+
global.Host, global.Org, o.flowName, o.pageNumber, o.pageLimit)
93+
} else {
94+
filter = "repo"
95+
url = fmt.Sprintf("%s/api/v2/repos/%s/artifacts/%s?page=%d&per_page=%d",
96+
global.Host, global.Org, o.repoName, o.pageNumber, o.pageLimit)
97+
}
8198

8299
reqParams := &requests.RequestParams{
83100
Method: http.MethodGet,
@@ -97,7 +114,15 @@ func (o *listArtifactsOptions) run(out io.Writer) error {
97114
}
98115

99116
func printArtifactsListAsTable(raw string, out io.Writer, page int) error {
100-
var artifacts []map[string]interface{}
117+
if filter == "flow" {
118+
return printArtifactsListForFlow(raw, out, page)
119+
} else {
120+
return printArtifactsListForRepo(raw, out, page)
121+
}
122+
}
123+
124+
func printArtifactsListForFlow(raw string, out io.Writer, page int) error {
125+
var artifacts []map[string]any
101126
err := json.Unmarshal([]byte(raw), &artifacts)
102127
if err != nil {
103128
return err
@@ -115,7 +140,6 @@ func printArtifactsListAsTable(raw string, out io.Writer, page int) error {
115140
header := []string{"COMMIT", "ARTIFACT", "STATE", "CREATED_AT"}
116141
rows := []string{}
117142
for _, artifact := range artifacts {
118-
119143
gitCommit := artifact["git_commit"].(string)[:7]
120144
artifactName := artifact["filename"].(string)
121145

@@ -137,3 +161,69 @@ func printArtifactsListAsTable(raw string, out io.Writer, page int) error {
137161

138162
return nil
139163
}
164+
165+
func printArtifactsListForRepo(raw string, out io.Writer, page int) error {
166+
var response map[string]any
167+
err := json.Unmarshal([]byte(raw), &response)
168+
if err != nil {
169+
return err
170+
}
171+
172+
embedded, ok := response["_embedded"].(map[string]any)
173+
if !ok {
174+
return fmt.Errorf("artifacts not found in response")
175+
}
176+
artifactsRaw, ok := embedded["artifacts"]
177+
if !ok {
178+
return fmt.Errorf("artifacts not found in response")
179+
}
180+
artifactsSlice, ok := artifactsRaw.([]any)
181+
if !ok {
182+
return fmt.Errorf("artifacts not found in response")
183+
}
184+
artifacts := make([]map[string]any, len(artifactsSlice))
185+
for i, v := range artifactsSlice {
186+
artifact, ok := v.(map[string]any)
187+
if !ok {
188+
return fmt.Errorf("invalid artifact format in response")
189+
}
190+
artifacts[i] = artifact
191+
}
192+
if len(artifacts) == 0 {
193+
msg := "No artifacts were found"
194+
if page != 1 {
195+
msg = fmt.Sprintf("%s at page number %d", msg, page)
196+
}
197+
logger.Info(msg + ".")
198+
return nil
199+
}
200+
201+
header := []string{"COMMIT", "ARTIFACT", "STATE", "CREATED_AT"}
202+
rows := []string{}
203+
for _, artifact := range artifacts {
204+
gitCommit := artifact["commit"].(string)[:7]
205+
artifactName := artifact["name"].(string)
206+
207+
artifactDigest := artifact["fingerprint"].(string)
208+
compliant := artifact["compliant_in_trail"].(bool)
209+
artifactState := "COMPLIANT"
210+
if !compliant {
211+
artifactState = "NON-COMPLIANT"
212+
}
213+
createdAt, err := formattedTimestamp(artifact["created_at"], true)
214+
if err != nil {
215+
return err
216+
}
217+
218+
row := fmt.Sprintf("%s\tName: %s\t%s\t%s", gitCommit, artifactName, artifactState, createdAt)
219+
rows = append(rows, row)
220+
row = fmt.Sprintf("\tFingerprint: %s\t\t", artifactDigest)
221+
rows = append(rows, row)
222+
rows = append(rows, "\t\t\t")
223+
224+
}
225+
tabFormattedPrint(out, header, rows)
226+
227+
return nil
228+
229+
}

cmd/kosli/listArtifacts_test.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ListArtifactsCommandTestSuite struct {
1919
artifactName string
2020
artifactPath string
2121
fingerprint string
22+
repoName string
2223
}
2324

2425
func (suite *ListArtifactsCommandTestSuite) SetupTest() {
@@ -40,23 +41,45 @@ func (suite *ListArtifactsCommandTestSuite) SetupTest() {
4041
var err error
4142
suite.fingerprint, err = GetSha256Digest(suite.artifactPath, fingerprintOptions, logger)
4243
require.NoError(suite.Suite.T(), err)
43-
CreateArtifact(suite.flowName2, suite.fingerprint, suite.artifactName, suite.Suite.T())
44+
suite.repoName = "kosli/dev"
45+
SetEnvVars(map[string]string{
46+
"GITHUB_RUN_NUMBER": "1234",
47+
"GITHUB_SERVER_URL": "https://github.com",
48+
"GITHUB_REPOSITORY": suite.repoName,
49+
"GITHUB_REPOSITORY_ID": "1234567890",
50+
}, suite.Suite.T())
51+
CreateArtifactOnTrail(suite.flowName2, "trail-1", "backend", suite.fingerprint, suite.artifactName, suite.Suite.T())
52+
}
53+
54+
func (suite *ListArtifactsCommandTestSuite) TearDownTest() {
55+
UnSetEnvVars(map[string]string{
56+
"GITHUB_RUN_NUMBER": "",
57+
"GITHUB_SERVER_URL": "",
58+
"GITHUB_REPOSITORY": "",
59+
"GITHUB_REPOSITORY_ID": "",
60+
}, suite.Suite.T())
4461
}
4562

4663
func (suite *ListArtifactsCommandTestSuite) TestListArtifactsCmd() {
4764
tests := []cmdTestCase{
4865
{
4966
wantError: true,
50-
name: "missing flow flag causes an error",
67+
name: "missing both flow and repo flags causes an error",
5168
cmd: fmt.Sprintf(`list artifacts %s`, suite.defaultKosliArguments),
52-
golden: "Error: required flag(s) \"flow\" not set\n",
69+
golden: "Error: at least one of --flow, --repo is required\n",
5370
},
5471
{
5572
wantError: true,
5673
name: "non-existing flow causes an error",
5774
cmd: fmt.Sprintf(`list artifacts --flow non-existing %s`, suite.defaultKosliArguments),
5875
goldenRegex: "^Error: Flow named 'non-existing' does not exist for organization 'docs-cmd-test-user'",
5976
},
77+
{
78+
wantError: true,
79+
name: "non-existing repo causes an error",
80+
cmd: fmt.Sprintf(`list artifacts --repo non-existing %s`, suite.defaultKosliArguments),
81+
goldenRegex: "^Error: Repo 'non-existing' not found",
82+
},
6083
// TODO: the correct error is overwritten by the hack flag value check in root.go
6184
{
6285
wantError: true,
@@ -85,6 +108,11 @@ func (suite *ListArtifactsCommandTestSuite) TestListArtifactsCmd() {
85108
cmd: fmt.Sprintf(`list artifacts --flow %s %s`, suite.flowName2, suite.defaultKosliArguments),
86109
goldenFile: "output/list/list-artifacts.txt",
87110
},
111+
{
112+
name: "listing artifacts on a repo works",
113+
cmd: fmt.Sprintf(`list artifacts --repo %s %s`, suite.repoName, suite.defaultKosliArguments),
114+
goldenFile: "output/list/list-artifacts.txt",
115+
},
88116
}
89117

90118
runTestCmd(suite.Suite.T(), tests)

cmd/kosli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
258258
getAttestationTrailNameFlag = "[conditional] The name of the Kosli trail for the attestation. Cannot be used together with --fingerprint or --attestation-id."
259259
getAttestationFlowNameFlag = "[conditional] The name of the Kosli flow for the attestation. Required if ATTESTATION-NAME provided. Cannot be used together with --attestation-id."
260260
attestationIDFlag = "[conditional] The unique identifier of the attestation to retrieve. Cannot be used together with ATTESTATION-NAME."
261+
repoNameFlag = "The name of a git repo as it is registered in Kosli. e.g kosli-dev/cli"
261262
)
262263

263264
var global *GlobalOpts

0 commit comments

Comments
 (0)