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
102 changes: 67 additions & 35 deletions cmd/kosli/getAttestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,42 @@ import (
"github.com/spf13/cobra"
)

const getAttestationShortDesc = `Get attestation by name from a specified trail or artifact. `
const getAttestationShortDesc = `Get an attestation using its name or id. `

const getAttestationLongDesc = getAttestationShortDesc + `
You can get an attestation from a trail or artifact using its name. The attestation name should be given
WITHOUT dot-notation.

To get an attestation from a trail, specify the trail name using the --trail flag.
To get an attestation from an artifact, specify the artifact fingerprint using the --fingerprint flag.

In both cases the flow must also be specified using the --flow flag.

You can get an attestation from a trail or artifact using its name. The attestation name should be given
WITHOUT dot-notation.
To get an attestation from a trail, specify the trail name using the ^--trail^ flag.
To get an attestation from an artifact, specify the artifact fingerprint using the ^--fingerprint^ flag.
These flags cannot be used together. In both cases the flow must also be specified using the ^--flow^ flag.
If there are multiple attestations with the same name on the trail or artifact, a list of all will be returned.

You can also get an attestation by its id using the ^--attestation-id^ flag. This cannot be used with the attestation name,
or any of the ^--flow^, ^--trail^ or ^--fingerprint^ flags.
`

const getAttestationExample = `
# get an attestation from a trail (requires the --trail flag)
# get an attestation by name from a trail (requires the --trail flag)
kosli get attestation attestationName \
--flow flowName \
--trail trailName

# get an attestation from an artifact
# get an attestation by name from an artifact
kosli get attestation attestationName \
--flow flowName \
--fingerprint fingerprint

# get an attestation by its id
kosli get attestation --attestation-id attestationID
`

type getAttestationOptions struct {
output string
flow string
trail string
fingerprint string
output string
flow string
trail string
fingerprint string
attestationID string
}

type Attestation struct {
Expand All @@ -63,22 +68,41 @@ type GitCommitInfo struct {
Timestamp float64 `json:"timestamp"`
}

type listAttestationsResponse struct {
Data []Attestation `json:"data"`
}

func newGetAttestationCmd(out io.Writer) *cobra.Command {
o := new(getAttestationOptions)
cmd := &cobra.Command{
Use: "attestation ATTESTATION-NAME",
Use: "attestation [ATTESTATION-NAME]",
Short: getAttestationShortDesc,
Long: getAttestationLongDesc,
Example: getAttestationExample,
Args: cobra.ExactArgs(1),
Args: cobra.MaximumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true)
if err != nil {
return err
if len(args) == 0 && o.attestationID == "" {
return fmt.Errorf("one of ATTESTATION-NAME argument or --attestation-id flag is required")
}
if o.attestationID != "" {
if len(args) > 0 {
return fmt.Errorf("--attestation-id cannot be used when ATTESTATION-NAME is provided")
}
if o.flow != "" || o.trail != "" || o.fingerprint != "" {
return fmt.Errorf("--flow, --trail, and --fingerprint flags cannot be used with --attestation-id")
}
} else {
if o.flow == "" {
return fmt.Errorf("--flow is required when using ATTESTATION-NAME")
}
err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true)
if err != nil {
return fmt.Errorf("%s when using ATTESTATION-NAME", err)
}
}
return nil
},
Expand All @@ -88,30 +112,32 @@ func newGetAttestationCmd(out io.Writer) *cobra.Command {
}

cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().StringVarP(&o.flow, "flow", "f", "", flowNameFlag)
cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailFlag)
cmd.Flags().StringVarP(&o.flow, "flow", "f", "", getAttestationFlowNameFlag)
cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailNameFlag)
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", getAttestationFingerprintFlag)

err := RequireFlags(cmd, []string{"flow"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}
cmd.Flags().StringVar(&o.attestationID, "attestation-id", "", attestationIDFlag)

return cmd
}

func (o *getAttestationOptions) run(out io.Writer, args []string) error {
var url string
baseUrl := fmt.Sprintf("%s/api/v2/attestations/%s/%s", global.Host, global.Org, o.flow)
if o.trail != "" {
url = fmt.Sprintf("%s/trail/%s", baseUrl, o.trail)
}

if o.fingerprint != "" {
url = fmt.Sprintf("%s/artifact/%s", baseUrl, o.fingerprint)
}
baseUrl := fmt.Sprintf("%s/api/v2/attestations/%s", global.Host, global.Org)
if o.attestationID != "" {
url = fmt.Sprintf("%s?attestation_id=%s", baseUrl, o.attestationID)
} else {
flowBaseUrl := fmt.Sprintf("%s/%s", baseUrl, o.flow)
if o.trail != "" {
url = fmt.Sprintf("%s/trail/%s", flowBaseUrl, o.trail)
}

if o.fingerprint != "" {
url = fmt.Sprintf("%s/artifact/%s", flowBaseUrl, o.fingerprint)
}

url = fmt.Sprintf("%s/%s", url, args[0])
url = fmt.Sprintf("%s/%s", url, args[0])
}

reqParams := &requests.RequestParams{
Method: http.MethodGet,
Expand All @@ -132,10 +158,16 @@ func (o *getAttestationOptions) run(out io.Writer, args []string) error {
}

func printAttestationsAsTable(raw string, out io.Writer, pageNumber int) error {
response := &listAttestationsResponse{}
var attestations []Attestation

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

if len(attestations) == 0 {
Expand Down
81 changes: 65 additions & 16 deletions cmd/kosli/getAttestation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type GetAttestationCommandTestSuite struct {
artifactPath string
fingerprint string
trailName string
attestationId string
}

func (suite *GetAttestationCommandTestSuite) SetupTest() {
Expand Down Expand Up @@ -46,69 +47,117 @@ func (suite *GetAttestationCommandTestSuite) SetupTest() {
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "first-trail-attestation", suite.Suite.T())
CreateGenericArtifactAttestation(suite.flowName, suite.trailName, suite.fingerprint, "second-artifact-attestation", true, suite.Suite.T())
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "second-trail-attestation", suite.Suite.T())

suite.attestationId = GetAttestationId(suite.flowName, suite.trailName, "first-trail-attestation", suite.Suite.T())
}

func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() {
tests := []cmdTestCase{
{
wantError: false,
name: "if no attestation found, say so",
name: "01 if no attestation found when getting by name, say so",
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
golden: "No attestations found.\n",
},
{
wantError: false,
name: "if no attestation found return empty list in json format",
name: "02 if no attestation found when getting by name return empty list in json format",
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s --output json`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "[]"}},
},
{
wantError: true,
name: "providing more than one argument fails",
name: "03 providing more than one argument fails",
cmd: fmt.Sprintf(`get attestation first-attestation second-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
golden: "Error: accepts 1 arg(s), received 2\n",
golden: "Error: accepts at most 1 arg(s), received 2\n",
},
{
wantError: true,
name: "missing --flow fails",
name: "04 missing --flow fails when ATTESTATION-NAME is provided",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --trail %s %s`, suite.trailName, suite.defaultKosliArguments),
golden: "Error: required flag(s) \"flow\" not set\n",
golden: "Error: --flow is required when using ATTESTATION-NAME\n",
},
{
wantError: true,
name: "missing --api-token fails",
name: "05 missing --api-token fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --org orgX`, suite.flowName),
golden: "Error: --api-token is not set\nUsage: kosli get attestation ATTESTATION-NAME [flags]\n",
golden: "Error: --api-token is not set\nUsage: kosli get attestation [ATTESTATION-NAME] [flags]\n",
},
{
name: "getting an existing trail attestation works",
name: "06 getting an existing trail attestation works",
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
},
{
name: "getting an existing trail attestation with --output json works",
name: "07 getting an existing trail attestation with --output json works",
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s --output json %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "non-empty"}},
},
{
name: "getting an existing artifact attestation works",
name: "08 getting an existing artifact attestation works",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
},
{
name: "getting an existing artifact attestation with --output json works",
name: "09 getting an existing artifact attestation with --output json works",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s --output json %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"", "non-empty"}},
},
{
wantError: true,
name: "missing both trail and fingerprint fails",
name: "10 missing both trail and fingerprint fails if ATTESTATION-NAME provided",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s %s`, suite.flowName, suite.defaultKosliArguments),
golden: "Error: at least one of --trail, --fingerprint is required\n",
golden: "Error: at least one of --trail, --fingerprint is required when using ATTESTATION-NAME\n",
},
{
wantError: true,
name: "providing both trail and fingerprint fails",
name: "11 providing both trail and fingerprint fails",
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --trail %s --fingerprint %s %s`, suite.flowName, suite.trailName, suite.fingerprint, suite.defaultKosliArguments),
golden: "Error: only one of --trail, --fingerprint is allowed\n",
golden: "Error: only one of --trail, --fingerprint is allowed when using ATTESTATION-NAME\n",
},
{
name: "12 can get an attestation from its id",
cmd: fmt.Sprintf(`get attestation --attestation-id %s %s`, suite.attestationId, suite.defaultKosliArguments),
},
{
wantError: false,
name: "13 if no attestation found when getting by name, say so",
cmd: fmt.Sprintf(`get attestation --attestation-id %s %s`, "non-existent-attestation-id", suite.defaultKosliArguments),
golden: "No attestations found.\n",
},
{
wantError: false,
name: "14 if no attestation found when getting by id return empty list in json format",
cmd: fmt.Sprintf(`get attestation --attestation-id %s --output json %s`, "non-existent-attestation-id", suite.defaultKosliArguments),
goldenJson: []jsonCheck{{"data", "length:0"}},
},
{
wantError: true,
name: "15 providing both attestation id and attestation name fails",
cmd: fmt.Sprintf(`get attestation %s --attestation-id %s %s`, "first-artifact-attestation", suite.attestationId, suite.defaultKosliArguments),
golden: "Error: --attestation-id cannot be used when ATTESTATION-NAME is provided\n",
},
{
wantError: true,
name: "16 providing both attestation id and trail fails",
cmd: fmt.Sprintf(`get attestation --attestation-id %s --trail %s %s`, suite.attestationId, suite.trailName, suite.defaultKosliArguments),
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
},
{
wantError: true,
name: "17 providing both attestation id and fingerprint fails",
cmd: fmt.Sprintf(`get attestation --attestation-id %s --fingerprint %s %s`, suite.attestationId, suite.fingerprint, suite.defaultKosliArguments),
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
},
{
wantError: true,
name: "18 providing both attestation id and flow fails",
cmd: fmt.Sprintf(`get attestation --attestation-id %s --flow %s %s`, suite.attestationId, suite.flowName, suite.defaultKosliArguments),
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
},
{
wantError: true,
name: "19 providing neither attestation id or flow fails",
cmd: fmt.Sprintf(`get attestation %s`, suite.defaultKosliArguments),
golden: "Error: one of ATTESTATION-NAME argument or --attestation-id flag is required\n",
},
}

Expand Down
6 changes: 4 additions & 2 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,10 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
attestationTypeJqFlag = "[optional] The attestation type evaluation JQ rules."
envNameFlag = "The Kosli environment name to assert the artifact against."
pathsWatchFlag = "[optional] Watch the filesystem for changes and report snapshots of artifacts running in specific filesystem paths to Kosli."
getAttestationFingerprintFlag = "[conditional] The fingerprint of the artifact for the attestation. Cannot be used together with --trail."
getAttestationTrailFlag = "[conditional] The name of the Kosli trail for the attestation. Cannot be used together with --fingerprint."
getAttestationFingerprintFlag = "[conditional] The fingerprint of the artifact for the attestation. Cannot be used together with --trail or --attestation-id."
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."
)

var global *GlobalOpts
Expand Down
21 changes: 21 additions & 0 deletions cmd/kosli/testHelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,3 +560,24 @@ func CreateGenericTrailAttestation(flowName, trailName, attestationName string,
err := o.run([]string{})
require.NoError(t, err, "generic artifact attestation should be created without error")
}

func GetAttestationId(flowName, trailName, attestationName string, t *testing.T) string {
t.Helper()
o := &getAttestationOptions{
flow: flowName,
trail: trailName,
output: "json",
}
buffer := new(bytes.Buffer)
err := o.run(buffer, []string{attestationName})
require.NoError(t, err, "attestation should be retrieved without error")

var data []map[string]interface{}
err = json.Unmarshal(buffer.Bytes(), &data)
require.NoError(t, err, "failed to parse attestation JSON: %s", buffer.String())
require.Greater(t, len(data), 0, "expected at least one attestation")

id, ok := data[0]["attestation_id"].(string)
require.True(t, ok, "attestation_id field not found or not a string")
return id
}