Skip to content
Merged
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ARG ALPINE_VERSION="3.21"


### Go Builder ###
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} as builder
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder

RUN apk add --update --no-cache git bash make ca-certificates

Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func newGetCmd(out io.Writer) *cobra.Command {
newGetPolicyCmd(out),
newGetAttestationTypeCmd(out),
newGetAttestationCmd(out),
newGetRepoCmd(out),
)
return cmd
}
136 changes: 136 additions & 0 deletions cmd/kosli/getRepo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
neturl "net/url"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/requests"
"github.com/spf13/cobra"
)

const getRepoShortDesc = `Get a repo for an org.`

const getRepoLongDesc = getRepoShortDesc + `
The name of the repo is specified as an argument (e.g. "my-org/my-repo").
Use --provider or --repo-id to narrow down the result when multiple repos
match the given name.`

const getRepoExample = `
# get a repo
kosli get repo my-org/my-repo \
--api-token yourAPIToken \
--org KosliOrgName

# get a repo filtering by provider
kosli get repo my-org/my-repo \
--provider github \
--api-token yourAPIToken \
--org KosliOrgName`

type getRepoOptions struct {
output string
provider string
repoID string
}

func newGetRepoCmd(out io.Writer) *cobra.Command {
o := new(getRepoOptions)
cmd := &cobra.Command{
Use: "repo REPO-NAME",
Hidden: true,
Short: getRepoShortDesc,
Long: getRepoLongDesc,
Example: getRepoExample,
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out, args)
},
}

cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().StringVar(&o.provider, "provider", "", "[optional] The VCS provider to filter repos by (e.g. github, gitlab).")
cmd.Flags().StringVar(&o.repoID, "repo-id", "", "[optional] The external repo ID to filter repos by.")

return cmd
}

func (o *getRepoOptions) run(out io.Writer, args []string) error {
params := neturl.Values{}
params.Set("name", args[0])
if o.provider != "" {
params.Set("provider", o.provider)
}
if o.repoID != "" {
params.Set("repo_id", o.repoID)
}
reqURL := fmt.Sprintf("%s/api/v2/repos/%s?%s", global.Host, global.Org, params.Encode())

reqParams := &requests.RequestParams{
Method: http.MethodGet,
URL: reqURL,
Token: global.ApiToken,
}
response, err := kosliClient.Do(reqParams)
if err != nil {
return err
}

var parsed struct {
Embedded struct {
Repos []map[string]any `json:"repos"`
} `json:"_embedded"`
}
if err := json.Unmarshal([]byte(response.Body), &parsed); err != nil {
return err
}
if len(parsed.Embedded.Repos) > 1 {
return fmt.Errorf("found %d repos matching %q. Use --provider or --repo-id to narrow down the search", len(parsed.Embedded.Repos), args[0])
}

return output.FormattedPrint(response.Body, o.output, out, 0,
map[string]output.FormatOutputFunc{
"table": printRepoAsTable,
"json": output.PrintJson,
})
}

func printRepoAsTable(raw string, out io.Writer, page int) error {
var response struct {
Embedded struct {
Repos []map[string]any `json:"repos"`
} `json:"_embedded"`
}

err := json.Unmarshal([]byte(raw), &response)
if err != nil {
return err
}

repos := response.Embedded.Repos
if len(repos) == 0 {
logger.Info("Repo was not found.")
return nil
}

repo := repos[0]
rows := []string{
fmt.Sprintf("Name:\t%s", repo["name"]),
fmt.Sprintf("URL:\t%s", repo["url"]),
fmt.Sprintf("Provider:\t%s", repo["provider"]),
fmt.Sprintf("Latest Activity:\t%s", repo["latest_activity"]),
}

tabFormattedPrint(out, []string{}, rows)
return nil
}
105 changes: 105 additions & 0 deletions cmd/kosli/getRepo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/suite"
)

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type GetRepoCommandTestSuite struct {
suite.Suite
defaultKosliArguments string
acmeOrgKosliArguments string
}

func (suite *GetRepoCommandTestSuite) SetupTest() {
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

global.Org = "iu-org-shared"
global.ApiToken = "qM9u2_grv6pJLbACwsMMMT5LIQy82tQj2k1zjZnlXti1smnFaGwCKW4jzk0La7ae9RrSYvEwCXSsXknD6YZqd-onLaaIUUKtEn6-B6yh53vWIe9EC5u85FCbKZjFbaicp_d0Me0Zcqq_KcCgrAZRX9xggl_pBb2oaCsNdllqNjk"
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
CreateFlowWithTemplate("get-repo", "testdata/valid_template.yml", suite.T())
SetEnvVars(map[string]string{
"GITHUB_RUN_NUMBER": "1234",
"GITHUB_SERVER_URL": "https://github.com",
"GITHUB_REPOSITORY": "kosli-dev/cli",
"GITHUB_REPOSITORY_ID": "1234567890",
}, suite.T())
BeginTrail("trail-name", "get-repo", "", suite.T())
}

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

func (suite *GetRepoCommandTestSuite) TestGetRepoCmd() {
tests := []cmdTestCase{
{
name: "01-getting a non-existing repo returns not-found message",
cmd: fmt.Sprintf(`get repo non-existing/repo %s`, suite.defaultKosliArguments),
golden: "Repo was not found.\n",
},
{
name: "02-getting an existing repo works",
cmd: fmt.Sprintf(`get repo kosli-dev/cli %s`, suite.acmeOrgKosliArguments),
},
{
name: "03-getting an existing repo with --output json works",
cmd: fmt.Sprintf(`get repo kosli-dev/cli --output json %s`, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
},
{
name: "04-getting an existing repo with matching --provider works",
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider github %s`, suite.acmeOrgKosliArguments),
},
{
name: "05-getting an existing repo with matching --provider and --output json works",
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider github --output json %s`, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
},
{
name: "06-getting a repo with a non-matching --provider returns not-found message",
cmd: fmt.Sprintf(`get repo kosli-dev/cli --provider gitlab %s`, suite.acmeOrgKosliArguments),
golden: "Repo was not found.\n",
},
{
name: "07-getting a repo with a non-matching --repo-id returns not-found message",
cmd: fmt.Sprintf(`get repo kosli-dev/cli --repo-id non-existing-id %s`, suite.acmeOrgKosliArguments),
golden: "Repo was not found.\n",
},
{
wantError: true,
name: "08-providing no argument fails",
cmd: fmt.Sprintf(`get repo %s`, suite.defaultKosliArguments),
golden: "Error: accepts 1 arg(s), received 0\n",
},
{
wantError: true,
name: "09-providing more than one argument fails",
cmd: fmt.Sprintf(`get repo foo bar %s`, suite.defaultKosliArguments),
golden: "Error: accepts 1 arg(s), received 2\n",
},
}

runTestCmd(suite.T(), tests)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestGetRepoCommandTestSuite(t *testing.T) {
suite.Run(t, new(GetRepoCommandTestSuite))
}
27 changes: 23 additions & 4 deletions cmd/kosli/listRepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
neturl "net/url"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/requests"
Expand All @@ -15,6 +16,9 @@ const listReposDesc = `List repos for an org.`

type listReposOptions struct {
listOptions
name string
provider string
repoID string
}

func newListReposCmd(out io.Writer) *cobra.Command {
Expand All @@ -38,16 +42,31 @@ func newListReposCmd(out io.Writer) *cobra.Command {
}

addListFlags(cmd, &o.listOptions)
cmd.Flags().StringVar(&o.name, "name", "", "[optional] The repo name to filter by.")
cmd.Flags().StringVar(&o.provider, "provider", "", "[optional] The VCS provider to filter repos by (e.g. github, gitlab).")
cmd.Flags().StringVar(&o.repoID, "repo-id", "", "[optional] The external repo ID to filter repos by.")

return cmd
}

func (o *listReposOptions) run(out io.Writer) error {
url := fmt.Sprintf("%s/api/v2/repos/%s?page=%d&per_page=%d", global.Host, global.Org, o.pageNumber, o.pageLimit)
params := neturl.Values{}
params.Set("page", fmt.Sprintf("%d", o.pageNumber))
params.Set("per_page", fmt.Sprintf("%d", o.pageLimit))
if o.name != "" {
params.Set("name", o.name)
}
if o.provider != "" {
params.Set("provider", o.provider)
}
if o.repoID != "" {
params.Set("repo_id", o.repoID)
}
reqURL := fmt.Sprintf("%s/api/v2/repos/%s?%s", global.Host, global.Org, params.Encode())

reqParams := &requests.RequestParams{
Method: http.MethodGet,
URL: url,
URL: reqURL,
Token: global.ApiToken,
}
response, err := kosliClient.Do(reqParams)
Expand Down Expand Up @@ -81,10 +100,10 @@ func printReposListAsTable(raw string, out io.Writer, page int) error {
return nil
}

header := []string{"NAME", "URL", "LAST_ACTIVITY"}
header := []string{"NAME", "URL", "PROVIDER", "LAST_ACTIVITY"}
rows := []string{}
for _, repo := range repos {
row := fmt.Sprintf("%s\t%s\t%s", repo["name"], repo["url"], repo["latest_activity"])
row := fmt.Sprintf("%s\t%s\t%s\t%s", repo["name"], repo["url"], repo["provider"], repo["latest_activity"])
rows = append(rows, row)
}
tabFormattedPrint(out, header, rows)
Expand Down
29 changes: 27 additions & 2 deletions cmd/kosli/listRepos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (suite *ListReposCommandTestSuite) SetupTest() {
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)

global.Org = "acme-org"
global.Org = "acme-org-shared"
global.ApiToken = "v3OWZiYWu9G2IMQStYg9BcPQUQ88lJNNnTJTNq8jfvmkR1C5wVpHSs7F00JcB5i6OGeUzrKt3CwRq7ndcN4TTfMeo8ASVJ5NdHpZT7DkfRfiFvm8s7GbsIHh2PtiQJYs2UoN13T8DblV5C4oKb6-yWH73h67OhotPlKfVKazR-c"
suite.acmeOrgKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
CreateFlowWithTemplate("list-repos", "testdata/valid_template.yml", suite.T())
Expand Down Expand Up @@ -57,7 +57,7 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() {
{
name: "02-listing repos works when there are no repos",
cmd: fmt.Sprintf(`list repos %s`, suite.acmeOrgKosliArguments),
goldenRegex: ".*\nkosli-dev/cli https://github.com/kosli-dev/cli Trail Started at.*",
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
},
{
name: "03-listing repos with --output json works when there are repos",
Expand Down Expand Up @@ -93,6 +93,31 @@ func (suite *ListReposCommandTestSuite) TestListReposCmd() {
cmd: fmt.Sprintf(`list repos --page-limit 15 --page 2 %s`, suite.defaultKosliArguments),
golden: "",
},
{
name: "09-listing repos with --name filter works",
cmd: fmt.Sprintf(`list repos --name kosli-dev/cli %s`, suite.acmeOrgKosliArguments),
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
},
{
name: "10-listing repos with --name filter and --output json works",
cmd: fmt.Sprintf(`list repos --name kosli-dev/cli --output json %s`, suite.acmeOrgKosliArguments),
goldenJson: []jsonCheck{{"_embedded.repos", "non-empty"}},
},
{
name: "11-listing repos with --provider filter works",
cmd: fmt.Sprintf(`list repos --provider github %s`, suite.acmeOrgKosliArguments),
goldenRegex: ".*\nkosli-dev/cli.*https://github.com/kosli-dev/cli.*github.*Trail Started at.*",
},
{
name: "12-listing repos with non-matching --provider returns no repos message",
cmd: fmt.Sprintf(`list repos --provider gitlab %s`, suite.acmeOrgKosliArguments),
golden: "No repos were found.\n",
},
{
name: "13-listing repos with non-matching --repo-id returns no repos message",
cmd: fmt.Sprintf(`list repos --repo-id non-existing-id %s`, suite.acmeOrgKosliArguments),
golden: "No repos were found.\n",
},
}

runTestCmd(suite.T(), tests)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect
github.com/containers/ocicrypt v1.2.1 // indirect
github.com/containers/storage v1.57.2 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containers/image/v5 v5.34.3 h1:/cMgfyA4Y7ILH7nzWP/kqpkE5Df35Ek4bp5ZPvJOVmI=
Expand Down
Loading