From 702a0f206886a06c4ec2b56c2419c39366a9f81f Mon Sep 17 00:00:00 2001 From: Simon Castagna Date: Thu, 18 Dec 2025 16:08:14 +0100 Subject: [PATCH 1/2] Add --artifact-id flag to get attestation command --- cmd/kosli/getAttestation.go | 75 ++++++++++++++++++++++---------- cmd/kosli/getAttestation_test.go | 49 +++++++++++++++++++-- cmd/kosli/testHelpers.go | 21 +++++++++ 3 files changed, 119 insertions(+), 26 deletions(-) diff --git a/cmd/kosli/getAttestation.go b/cmd/kosli/getAttestation.go index 061fffc1e..554304ecc 100644 --- a/cmd/kosli/getAttestation.go +++ b/cmd/kosli/getAttestation.go @@ -38,10 +38,11 @@ kosli get attestation attestationName \ ` type getAttestationOptions struct { - output string - flow string - trail string - fingerprint string + output string + flow string + trail string + fingerprint string + attestationID string } type Attestation struct { @@ -63,22 +64,42 @@ 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 { + err := RequireFlags(cmd, []string{"flow"}) + if err != nil { + logger.Error("failed to configure required flags: %v", err) + } + err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true) + if err != nil { + return err + } } return nil }, @@ -91,27 +112,29 @@ func newGetAttestationCmd(out io.Writer) *cobra.Command { cmd.Flags().StringVarP(&o.flow, "flow", "f", "", flowNameFlag) cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailFlag) 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", "", "The unique identifier of the attestation to retrieve.") 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) + } - url = fmt.Sprintf("%s/%s", url, args[0]) + if o.fingerprint != "" { + url = fmt.Sprintf("%s/artifact/%s", flowBaseUrl, o.fingerprint) + } + + url = fmt.Sprintf("%s/%s", url, args[0]) + } reqParams := &requests.RequestParams{ Method: http.MethodGet, @@ -132,10 +155,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 { diff --git a/cmd/kosli/getAttestation_test.go b/cmd/kosli/getAttestation_test.go index 47254d31f..6f06931b2 100644 --- a/cmd/kosli/getAttestation_test.go +++ b/cmd/kosli/getAttestation_test.go @@ -19,6 +19,7 @@ type GetAttestationCommandTestSuite struct { artifactPath string fingerprint string trailName string + attestationId string } func (suite *GetAttestationCommandTestSuite) SetupTest() { @@ -46,6 +47,8 @@ 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() { @@ -66,11 +69,11 @@ func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() { wantError: true, name: "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: "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", }, @@ -78,7 +81,7 @@ func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() { wantError: true, name: "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", @@ -110,6 +113,46 @@ func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() { 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", }, + { + name: "can get an attestation from its id", + cmd: fmt.Sprintf(`get attestation --attestation-id %s %s`, suite.attestationId, suite.defaultKosliArguments), + }, + { + wantError: false, + name: "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: "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: "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: "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: "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: "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", + }, } runTestCmd(suite.Suite.T(), tests) diff --git a/cmd/kosli/testHelpers.go b/cmd/kosli/testHelpers.go index 2fb788dc6..3f9e57483 100644 --- a/cmd/kosli/testHelpers.go +++ b/cmd/kosli/testHelpers.go @@ -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 +} From 02147addd9739a863e8ac0a87a054b0277abd31d Mon Sep 17 00:00:00 2001 From: Faye Date: Thu, 18 Dec 2025 17:25:23 +0100 Subject: [PATCH 2/2] Tidy up tests and documentation --- cmd/kosli/getAttestation.go | 37 +++++++++++++----------- cmd/kosli/getAttestation_test.go | 48 ++++++++++++++++++-------------- cmd/kosli/root.go | 6 ++-- 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/cmd/kosli/getAttestation.go b/cmd/kosli/getAttestation.go index 554304ecc..62e6a695e 100644 --- a/cmd/kosli/getAttestation.go +++ b/cmd/kosli/getAttestation.go @@ -11,30 +11,34 @@ 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 { @@ -92,13 +96,12 @@ func newGetAttestationCmd(out io.Writer) *cobra.Command { return fmt.Errorf("--flow, --trail, and --fingerprint flags cannot be used with --attestation-id") } } else { - err := RequireFlags(cmd, []string{"flow"}) - if err != nil { - logger.Error("failed to configure required flags: %v", err) + if o.flow == "" { + return fmt.Errorf("--flow is required when using ATTESTATION-NAME") } err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true) if err != nil { - return err + return fmt.Errorf("%s when using ATTESTATION-NAME", err) } } return nil @@ -109,10 +112,10 @@ 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) - cmd.Flags().StringVar(&o.attestationID, "attestation-id", "", "The unique identifier of the attestation to retrieve.") + cmd.Flags().StringVar(&o.attestationID, "attestation-id", "", attestationIDFlag) return cmd } diff --git a/cmd/kosli/getAttestation_test.go b/cmd/kosli/getAttestation_test.go index 6f06931b2..6e16885ff 100644 --- a/cmd/kosli/getAttestation_test.go +++ b/cmd/kosli/getAttestation_test.go @@ -55,101 +55,107 @@ 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 at most 1 arg(s), received 2\n", }, { wantError: true, - name: "missing --flow fails when ATTESTATION-NAME is provided", + 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", }, { - 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: "can get an attestation from its id", + 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: "if no attestation found when getting by id return empty list in json format", + 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: "providing both attestation id and attestation name fails", + 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: "providing both attestation id and trail fails", + 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: "providing both attestation id and fingerprint fails", + 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: "providing both attestation id and flow fails", + 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: "providing neither attestation id or flow fails", + 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", }, diff --git a/cmd/kosli/root.go b/cmd/kosli/root.go index ca647fc06..083e48122 100644 --- a/cmd/kosli/root.go +++ b/cmd/kosli/root.go @@ -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