Skip to content

Commit

Permalink
952 add commands for Flows with template and Trails (#60)
Browse files Browse the repository at this point in the history
* add hidden create flow v2 command

* add create trail command

* Start adding 'attest artifact' command

* rename create trail to beging trail and tidy up

* Use 'begin trail' in example [ci skip]

* Update logger info message when trail is created [ci skip]

* start adding attest generic command

* tidy up attest generic command

* update trail and flow creation/update output

* fix test setup and add flow with template creation tests

* add begin trail tests

* start adding tests for attest artifact

* add tests for attest generic

* refactor attest generic and add attest snyk

* add attest junit command

* add attest jira command

* add attest gitlab pr command

* add attest bitbucket and azure pr commands

* bump k8s dependency

* fix pr tests

---------

Co-authored-by: JonJagger <[email protected]>
  • Loading branch information
sami-alajrami and JonJagger authored Dec 6, 2023
1 parent a72d2b5 commit 47f9735
Show file tree
Hide file tree
Showing 41 changed files with 3,624 additions and 108 deletions.
29 changes: 29 additions & 0 deletions cmd/kosli/attest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"io"

"github.com/spf13/cobra"
)

const attestDesc = `All Kosli attest commands.`

func newAttestCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "attest",
Short: attestDesc,
Long: attestDesc,
Hidden: true,
}

// Add subcommands
cmd.AddCommand(
newAttestArtifactCmd(out),
newAttestGenericCmd(out),
newAttestSnykCmd(out),
newAttestJunitCmd(out),
newAttestJiraCmd(out),
newAttestPRCmd(out),
)
return cmd
}
208 changes: 208 additions & 0 deletions cmd/kosli/attestArtifact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package main

import (
"encoding/json"
"fmt"
"io"
"net/http"
"path/filepath"

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

type attestArtifactOptions struct {
fingerprintOptions *fingerprintOptions
flowName string
gitReference string
srcRepoRoot string
displayName string
payload AttestArtifactPayload
}

type AttestArtifactPayload struct {
Fingerprint string `json:"fingerprint"`
Filename string `json:"filename"`
GitCommit string `json:"git_commit"`
BuildUrl string `json:"build_url"`
CommitUrl string `json:"commit_url"`
RepoUrl string `json:"repo_url"`
CommitsList []*gitview.CommitInfo `json:"commits_list"`
Name string `json:"step_name"`
TrailName string `json:"trail_name"`
}

const attestArtifactShortDesc = `Attest an artifact creation to a Kosli flow. `

const attestArtifactLongDesc = attestArtifactShortDesc + `
` + fingerprintDesc

const attestArtifactExample = `
# Attest to a Kosli flow that a file type artifact has been created
kosli attest artifact FILE.tgz \
--artifact-type file \
--build-url https://exampleci.com \
--commit-url https://github.com/YourOrg/YourProject/commit/yourCommitShaThatThisArtifactWasBuiltFrom \
--git-commit yourCommitShaThatThisArtifactWasBuiltFrom \
--flow yourFlowName \
--trail yourTrailName \
--name yourTemplateArtifactName \
--api-token yourApiToken \
--org yourOrgName
# Attest to a Kosli flow that an artifact with a provided fingerprint (sha256) has been created
kosli attest artifact ANOTHER_FILE.txt \
--build-url https://exampleci.com \
--commit-url https://github.com/YourOrg/YourProject/commit/yourCommitShaThatThisArtifactWasBuiltFrom \
--git-commit yourCommitShaThatThisArtifactWasBuiltFrom \
--flow yourFlowName \
--fingerprint yourArtifactFingerprint \
--trail yourTrailName \
--name yourTemplateArtifactName \
--api-token yourApiToken \
--org yourOrgName
`

func newAttestArtifactCmd(out io.Writer) *cobra.Command {
o := new(attestArtifactOptions)
o.fingerprintOptions = new(fingerprintOptions)
cmd := &cobra.Command{
Use: "artifact {IMAGE-NAME | FILE-PATH | DIR-PATH}",
Short: attestArtifactShortDesc,
Long: attestArtifactLongDesc,
Example: attestArtifactExample,
Args: cobra.MaximumNArgs(1),
Hidden: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}

err = ValidateArtifactArg(args, o.fingerprintOptions.artifactType, o.payload.Fingerprint, true)
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return ValidateRegistryFlags(cmd, o.fingerprintOptions)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(args)
},
}

ci := WhichCI()
cmd.Flags().StringVarP(&o.payload.Fingerprint, "fingerprint", "F", "", fingerprintFlag)
cmd.Flags().StringVarP(&o.flowName, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.gitReference, "git-commit", "g", DefaultValue(ci, "git-commit"), gitCommitFlag)
cmd.Flags().StringVarP(&o.payload.BuildUrl, "build-url", "b", DefaultValue(ci, "build-url"), buildUrlFlag)
cmd.Flags().StringVarP(&o.payload.CommitUrl, "commit-url", "u", DefaultValue(ci, "commit-url"), commitUrlFlag)
cmd.Flags().StringVar(&o.srcRepoRoot, "repo-root", ".", repoRootFlag)
cmd.Flags().StringVarP(&o.payload.Name, "name", "n", "", templateArtifactName)
cmd.Flags().StringVarP(&o.displayName, "display-name", "N", "", artifactDisplayName)
cmd.Flags().StringVarP(&o.payload.TrailName, "trail", "T", "", trailNameFlag)
addFingerprintFlags(cmd, o.fingerprintOptions)

addDryRunFlag(cmd)

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

return cmd
}

func (o *attestArtifactOptions) run(args []string) error {

if o.displayName != "" {
o.payload.Filename = o.displayName
} else {
if o.fingerprintOptions.artifactType == "dir" || o.fingerprintOptions.artifactType == "file" {
o.payload.Filename = filepath.Base(args[0])
} else {
o.payload.Filename = args[0]
}
}

if o.payload.Fingerprint == "" {
var err error
o.payload.Fingerprint, err = GetSha256Digest(args[0], o.fingerprintOptions, logger)
if err != nil {
return err
}
}

gitView, err := gitview.New(o.srcRepoRoot)
if err != nil {
return err
}

commitObject, err := gitView.GetCommitInfoFromCommitSHA(o.gitReference)
if err != nil {
return err
}
o.payload.GitCommit = commitObject.Sha1

previousCommit, err := o.latestCommit(currentBranch(gitView))
if err == nil {
o.payload.CommitsList, err = gitView.ChangeLog(o.payload.GitCommit, previousCommit, logger)
if err != nil && !global.DryRun {
return err
}
} else if !global.DryRun {
return err
}

o.payload.RepoUrl, err = gitView.RepoUrl()
if err != nil {
logger.Warning("Repo URL will not be reported, %s", err.Error())
}

url := fmt.Sprintf("%s/api/v2/artifacts/%s/%s", global.Host, global.Org, o.flowName)

reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: url,
Payload: o.payload,
DryRun: global.DryRun,
Password: global.ApiToken,
}
_, err = kosliClient.Do(reqParams)
if err == nil && !global.DryRun {
logger.Info("artifact %s was attested with fingerprint: %s", o.payload.Filename, o.payload.Fingerprint)
}
return err
}

// latestCommit retrieves the git commit of the latest artifact for a flow in Kosli
func (o *attestArtifactOptions) latestCommit(branchName string) (string, error) {
latestCommitUrl := fmt.Sprintf(
"%s/api/v2/artifacts/%s/%s/%s/latest_commit%s",
global.Host, global.Org, o.flowName, o.payload.Fingerprint, asBranchParameter(branchName))

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

var latestCommitResponse map[string]interface{}
err = json.Unmarshal([]byte(response.Body), &latestCommitResponse)
if err != nil {
return "", err
}
latestCommit := latestCommitResponse["latest_commit"]
if latestCommit == nil {
logger.Debug("no previous artifacts were found for flow: %s", o.flowName)
return "", nil
} else {
logger.Debug("latest artifact for flow: %s has the git commit: %s", o.flowName, latestCommit.(string))
return latestCommit.(string), nil
}
}
78 changes: 78 additions & 0 deletions cmd/kosli/attestArtifact_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 AttestArtifactCommandTestSuite struct {
flowName string
trailName string
suite.Suite
defaultKosliArguments string
}

func (suite *AttestArtifactCommandTestSuite) SetupTest() {
suite.flowName = "attest-artifact"
suite.trailName = "test-123"
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --flow %s --trail %s --repo-root ../.. --host %s --org %s --api-token %s", suite.flowName, suite.trailName, global.Host, global.Org, global.ApiToken)
CreateFlowWithTemplate(suite.flowName, "testdata/valid_template.yml", suite.T())
BeginTrail(suite.trailName, suite.flowName, "", suite.T())
}

func (suite *AttestArtifactCommandTestSuite) TestAttestArtifactCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when more arguments are provided",
cmd: fmt.Sprintf("attest artifact foo bar %s", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2\n",
},
{
wantError: true,
name: "fails when missing a required flag",
cmd: fmt.Sprintf("attest artifact foo --artifact-type file --name bar --git-commit HEAD --build-url example.com %s", suite.defaultKosliArguments),
golden: "Error: required flag(s) \"commit-url\" not set\n",
},
{
wantError: true,
name: "fails when --fingerprint is invalid sha256 digest",
cmd: fmt.Sprintf("attest artifact foo --fingerprint xxxx --name bar --git-commit HEAD --build-url example.com --commit-url example.com %s", suite.defaultKosliArguments),
golden: "Error: xxxx is not a valid SHA256 fingerprint. It should match the pattern ^([a-f0-9]{64})$\nUsage: kosli attest artifact {IMAGE-NAME | FILE-PATH | DIR-PATH} [flags]\n",
},
{
wantError: true,
name: "fails when --name does not match artifact name in the template",
cmd: fmt.Sprintf("attest artifact testdata/file1 --artifact-type file --name bar --git-commit HEAD --build-url example.com --commit-url example.com %s", suite.defaultKosliArguments),
golden: "Error: Artifact 'bar' does not exist in trail template 'test-123'.\nAvailable artifacts: cli\n",
},
{
name: "can attest a file artifact",
cmd: fmt.Sprintf("attest artifact testdata/file1 --artifact-type file --name cli --git-commit HEAD --build-url example.com --commit-url example.com %s", suite.defaultKosliArguments),
golden: "artifact file1 was attested with fingerprint: 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9\n",
},
{
name: "can attest an artifact with --fingerprint",
cmd: fmt.Sprintf("attest artifact testdata/file1 --fingerprint 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 --name cli --git-commit HEAD --build-url example.com --commit-url example.com %s", suite.defaultKosliArguments),
golden: "artifact testdata/file1 was attested with fingerprint: 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9\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 TestAttestArtifactCommandTestSuite(t *testing.T) {
suite.Run(t, new(AttestArtifactCommandTestSuite))
}
Loading

0 comments on commit 47f9735

Please sign in to comment.