Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 100 additions & 10 deletions cmd/kosli/listArtifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/spf13/cobra"
)

const listArtifactsShortDesc = `List artifacts in a flow. `
const listArtifactsShortDesc = `List artifacts in a flow or repo. `

const listArtifactsLongDesc = listArtifactsShortDesc + `The results are paginated and ordered from latest to oldest.
By default, the page limit is 15 artifacts per page.
Expand All @@ -23,6 +23,12 @@ kosli list artifacts \
--api-token yourAPIToken \
--org yourOrgName

# list the last 15 artifacts for a repo:
kosli list artifacts \
--repo yourRepoName \
--api-token yourAPIToken \
--org yourOrgName

# list the last 30 artifacts for a flow:
kosli list artifacts \
--flow yourFlowName \
Expand All @@ -42,8 +48,11 @@ kosli list artifacts \
type listArtifactsOptions struct {
listOptions
flowName string
repoName string
}

var filter string

func newListArtifactsCmd(out io.Writer) *cobra.Command {
o := new(listArtifactsOptions)
cmd := &cobra.Command{
Expand All @@ -57,6 +66,10 @@ func newListArtifactsCmd(out io.Writer) *cobra.Command {
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
err = MuXRequiredFlags(cmd, []string{"flow", "repo"}, true)
if err != nil {
return err
}
return o.validate(cmd)
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -65,19 +78,23 @@ func newListArtifactsCmd(out io.Writer) *cobra.Command {
}

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

err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}

return cmd
}

func (o *listArtifactsOptions) run(out io.Writer) error {
url := fmt.Sprintf("%s/api/v2/artifacts/%s/%s?page=%d&per_page=%d",
global.Host, global.Org, o.flowName, o.pageNumber, o.pageLimit)
var url string
if o.flowName != "" {
filter = "flow"
url = fmt.Sprintf("%s/api/v2/artifacts/%s/%s?page=%d&per_page=%d",
global.Host, global.Org, o.flowName, o.pageNumber, o.pageLimit)
} else {
filter = "repo"
url = fmt.Sprintf("%s/api/v2/repos/%s/artifacts/%s?page=%d&per_page=%d",
global.Host, global.Org, o.repoName, o.pageNumber, o.pageLimit)
}

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

func printArtifactsListAsTable(raw string, out io.Writer, page int) error {
var artifacts []map[string]interface{}
if filter == "flow" {
return printArtifactsListForFlow(raw, out, page)
} else {
return printArtifactsListForRepo(raw, out, page)
}
}

func printArtifactsListForFlow(raw string, out io.Writer, page int) error {
var artifacts []map[string]any
err := json.Unmarshal([]byte(raw), &artifacts)
if err != nil {
return err
Expand All @@ -115,7 +140,6 @@ func printArtifactsListAsTable(raw string, out io.Writer, page int) error {
header := []string{"COMMIT", "ARTIFACT", "STATE", "CREATED_AT"}
rows := []string{}
for _, artifact := range artifacts {

gitCommit := artifact["git_commit"].(string)[:7]
artifactName := artifact["filename"].(string)

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

return nil
}

func printArtifactsListForRepo(raw string, out io.Writer, page int) error {
var response map[string]any
err := json.Unmarshal([]byte(raw), &response)
if err != nil {
return err
}

embedded, ok := response["_embedded"].(map[string]any)
if !ok {
return fmt.Errorf("artifacts not found in response")
}
artifactsRaw, ok := embedded["artifacts"]
if !ok {
return fmt.Errorf("artifacts not found in response")
}
artifactsSlice, ok := artifactsRaw.([]any)
if !ok {
return fmt.Errorf("artifacts not found in response")
}
artifacts := make([]map[string]any, len(artifactsSlice))
for i, v := range artifactsSlice {
artifact, ok := v.(map[string]any)
if !ok {
return fmt.Errorf("invalid artifact format in response")
}
artifacts[i] = artifact
}
if len(artifacts) == 0 {
msg := "No artifacts were found"
if page != 1 {
msg = fmt.Sprintf("%s at page number %d", msg, page)
}
logger.Info(msg + ".")
return nil
}

header := []string{"COMMIT", "ARTIFACT", "STATE", "CREATED_AT"}
rows := []string{}
for _, artifact := range artifacts {
gitCommit := artifact["commit"].(string)[:7]
artifactName := artifact["name"].(string)

artifactDigest := artifact["fingerprint"].(string)
compliant := artifact["compliant_in_trail"].(bool)
artifactState := "COMPLIANT"
if !compliant {
artifactState = "NON-COMPLIANT"
}
createdAt, err := formattedTimestamp(artifact["created_at"], true)
if err != nil {
return err
}

row := fmt.Sprintf("%s\tName: %s\t%s\t%s", gitCommit, artifactName, artifactState, createdAt)
rows = append(rows, row)
row = fmt.Sprintf("\tFingerprint: %s\t\t", artifactDigest)
rows = append(rows, row)
rows = append(rows, "\t\t\t")

}
tabFormattedPrint(out, header, rows)

return nil

}
34 changes: 31 additions & 3 deletions cmd/kosli/listArtifacts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ListArtifactsCommandTestSuite struct {
artifactName string
artifactPath string
fingerprint string
repoName string
}

func (suite *ListArtifactsCommandTestSuite) SetupTest() {
Expand All @@ -40,23 +41,45 @@ func (suite *ListArtifactsCommandTestSuite) SetupTest() {
var err error
suite.fingerprint, err = GetSha256Digest(suite.artifactPath, fingerprintOptions, logger)
require.NoError(suite.Suite.T(), err)
CreateArtifact(suite.flowName2, suite.fingerprint, suite.artifactName, suite.Suite.T())
suite.repoName = "kosli/dev"
SetEnvVars(map[string]string{
"GITHUB_RUN_NUMBER": "1234",
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_REPOSITORY": suite.repoName,
"GITHUB_REPOSITORY_ID": "1234567890",
}, suite.Suite.T())
CreateArtifactOnTrail(suite.flowName2, "trail-1", "backend", suite.fingerprint, suite.artifactName, suite.Suite.T())
}

func (suite *ListArtifactsCommandTestSuite) TearDownTest() {
UnSetEnvVars(map[string]string{
"GITHUB_RUN_NUMBER": "",
"GITHUB_SERVER_URL": "",
"GITHUB_REPOSITORY": "",
"GITHUB_REPOSITORY_ID": "",
}, suite.Suite.T())
}

func (suite *ListArtifactsCommandTestSuite) TestListArtifactsCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "missing flow flag causes an error",
name: "missing both flow and repo flags causes an error",
cmd: fmt.Sprintf(`list artifacts %s`, suite.defaultKosliArguments),
golden: "Error: required flag(s) \"flow\" not set\n",
golden: "Error: at least one of --flow, --repo is required\n",
},
{
wantError: true,
name: "non-existing flow causes an error",
cmd: fmt.Sprintf(`list artifacts --flow non-existing %s`, suite.defaultKosliArguments),
goldenRegex: "^Error: Flow named 'non-existing' does not exist for organization 'docs-cmd-test-user'",
},
{
wantError: true,
name: "non-existing repo causes an error",
cmd: fmt.Sprintf(`list artifacts --repo non-existing %s`, suite.defaultKosliArguments),
goldenRegex: "^Error: Repo 'non-existing' not found",
},
// TODO: the correct error is overwritten by the hack flag value check in root.go
{
wantError: true,
Expand Down Expand Up @@ -85,6 +108,11 @@ func (suite *ListArtifactsCommandTestSuite) TestListArtifactsCmd() {
cmd: fmt.Sprintf(`list artifacts --flow %s %s`, suite.flowName2, suite.defaultKosliArguments),
goldenFile: "output/list/list-artifacts.txt",
},
{
name: "listing artifacts on a repo works",
cmd: fmt.Sprintf(`list artifacts --repo %s %s`, suite.repoName, suite.defaultKosliArguments),
goldenFile: "output/list/list-artifacts.txt",
},
}

runTestCmd(suite.Suite.T(), tests)
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
getAttestationTrailNameFlag = "[conditional] The name of the Kosli trail for the attestation. Cannot be used together with --fingerprint or --attestation-id."
getAttestationFlowNameFlag = "[conditional] The name of the Kosli flow for the attestation. Required if ATTESTATION-NAME provided. Cannot be used together with --attestation-id."
attestationIDFlag = "[conditional] The unique identifier of the attestation to retrieve. Cannot be used together with ATTESTATION-NAME."
repoNameFlag = "The name of a git repo as it is registered in Kosli. e.g kosli-dev/cli"
)

var global *GlobalOpts
Expand Down