Skip to content

Commit f4133fb

Browse files
jumboduckFayeSGW
andauthored
get attestation by id (#624)
* Add --artifact-id flag to get attestation command * Tidy up tests and documentation --------- Co-authored-by: Faye <faye@kosli.com>
1 parent 31c1162 commit f4133fb

4 files changed

Lines changed: 157 additions & 53 deletions

File tree

cmd/kosli/getAttestation.go

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,42 @@ import (
1111
"github.com/spf13/cobra"
1212
)
1313

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

1616
const getAttestationLongDesc = getAttestationShortDesc + `
17-
You can get an attestation from a trail or artifact using its name. The attestation name should be given
18-
WITHOUT dot-notation.
19-
20-
To get an attestation from a trail, specify the trail name using the --trail flag.
21-
To get an attestation from an artifact, specify the artifact fingerprint using the --fingerprint flag.
22-
23-
In both cases the flow must also be specified using the --flow flag.
2417
18+
You can get an attestation from a trail or artifact using its name. The attestation name should be given
19+
WITHOUT dot-notation.
20+
To get an attestation from a trail, specify the trail name using the ^--trail^ flag.
21+
To get an attestation from an artifact, specify the artifact fingerprint using the ^--fingerprint^ flag.
22+
These flags cannot be used together. In both cases the flow must also be specified using the ^--flow^ flag.
2523
If there are multiple attestations with the same name on the trail or artifact, a list of all will be returned.
24+
25+
You can also get an attestation by its id using the ^--attestation-id^ flag. This cannot be used with the attestation name,
26+
or any of the ^--flow^, ^--trail^ or ^--fingerprint^ flags.
2627
`
2728

2829
const getAttestationExample = `
29-
# get an attestation from a trail (requires the --trail flag)
30+
# get an attestation by name from a trail (requires the --trail flag)
3031
kosli get attestation attestationName \
3132
--flow flowName \
3233
--trail trailName
3334
34-
# get an attestation from an artifact
35+
# get an attestation by name from an artifact
3536
kosli get attestation attestationName \
3637
--flow flowName \
3738
--fingerprint fingerprint
39+
40+
# get an attestation by its id
41+
kosli get attestation --attestation-id attestationID
3842
`
3943

4044
type getAttestationOptions struct {
41-
output string
42-
flow string
43-
trail string
44-
fingerprint string
45+
output string
46+
flow string
47+
trail string
48+
fingerprint string
49+
attestationID string
4550
}
4651

4752
type Attestation struct {
@@ -63,22 +68,41 @@ type GitCommitInfo struct {
6368
Timestamp float64 `json:"timestamp"`
6469
}
6570

71+
type listAttestationsResponse struct {
72+
Data []Attestation `json:"data"`
73+
}
74+
6675
func newGetAttestationCmd(out io.Writer) *cobra.Command {
6776
o := new(getAttestationOptions)
6877
cmd := &cobra.Command{
69-
Use: "attestation ATTESTATION-NAME",
78+
Use: "attestation [ATTESTATION-NAME]",
7079
Short: getAttestationShortDesc,
7180
Long: getAttestationLongDesc,
7281
Example: getAttestationExample,
73-
Args: cobra.ExactArgs(1),
82+
Args: cobra.MaximumNArgs(1),
7483
PreRunE: func(cmd *cobra.Command, args []string) error {
7584
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
7685
if err != nil {
7786
return ErrorBeforePrintingUsage(cmd, err.Error())
7887
}
79-
err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true)
80-
if err != nil {
81-
return err
88+
if len(args) == 0 && o.attestationID == "" {
89+
return fmt.Errorf("one of ATTESTATION-NAME argument or --attestation-id flag is required")
90+
}
91+
if o.attestationID != "" {
92+
if len(args) > 0 {
93+
return fmt.Errorf("--attestation-id cannot be used when ATTESTATION-NAME is provided")
94+
}
95+
if o.flow != "" || o.trail != "" || o.fingerprint != "" {
96+
return fmt.Errorf("--flow, --trail, and --fingerprint flags cannot be used with --attestation-id")
97+
}
98+
} else {
99+
if o.flow == "" {
100+
return fmt.Errorf("--flow is required when using ATTESTATION-NAME")
101+
}
102+
err = MuXRequiredFlags(cmd, []string{"trail", "fingerprint"}, true)
103+
if err != nil {
104+
return fmt.Errorf("%s when using ATTESTATION-NAME", err)
105+
}
82106
}
83107
return nil
84108
},
@@ -88,30 +112,32 @@ func newGetAttestationCmd(out io.Writer) *cobra.Command {
88112
}
89113

90114
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
91-
cmd.Flags().StringVarP(&o.flow, "flow", "f", "", flowNameFlag)
92-
cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailFlag)
115+
cmd.Flags().StringVarP(&o.flow, "flow", "f", "", getAttestationFlowNameFlag)
116+
cmd.Flags().StringVarP(&o.trail, "trail", "t", "", getAttestationTrailNameFlag)
93117
cmd.Flags().StringVarP(&o.fingerprint, "fingerprint", "F", "", getAttestationFingerprintFlag)
94-
95-
err := RequireFlags(cmd, []string{"flow"})
96-
if err != nil {
97-
logger.Error("failed to configure required flags: %v", err)
98-
}
118+
cmd.Flags().StringVar(&o.attestationID, "attestation-id", "", attestationIDFlag)
99119

100120
return cmd
101121
}
102122

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

110-
if o.fingerprint != "" {
111-
url = fmt.Sprintf("%s/artifact/%s", baseUrl, o.fingerprint)
112-
}
126+
baseUrl := fmt.Sprintf("%s/api/v2/attestations/%s", global.Host, global.Org)
127+
if o.attestationID != "" {
128+
url = fmt.Sprintf("%s?attestation_id=%s", baseUrl, o.attestationID)
129+
} else {
130+
flowBaseUrl := fmt.Sprintf("%s/%s", baseUrl, o.flow)
131+
if o.trail != "" {
132+
url = fmt.Sprintf("%s/trail/%s", flowBaseUrl, o.trail)
133+
}
134+
135+
if o.fingerprint != "" {
136+
url = fmt.Sprintf("%s/artifact/%s", flowBaseUrl, o.fingerprint)
137+
}
113138

114-
url = fmt.Sprintf("%s/%s", url, args[0])
139+
url = fmt.Sprintf("%s/%s", url, args[0])
140+
}
115141

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

134160
func printAttestationsAsTable(raw string, out io.Writer, pageNumber int) error {
161+
response := &listAttestationsResponse{}
135162
var attestations []Attestation
163+
136164
err := json.Unmarshal([]byte(raw), &attestations)
137165
if err != nil {
138-
return err
166+
err = json.Unmarshal([]byte(raw), &response)
167+
if err != nil {
168+
return err
169+
}
170+
attestations = response.Data
139171
}
140172

141173
if len(attestations) == 0 {

cmd/kosli/getAttestation_test.go

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type GetAttestationCommandTestSuite struct {
1919
artifactPath string
2020
fingerprint string
2121
trailName string
22+
attestationId string
2223
}
2324

2425
func (suite *GetAttestationCommandTestSuite) SetupTest() {
@@ -46,69 +47,117 @@ func (suite *GetAttestationCommandTestSuite) SetupTest() {
4647
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "first-trail-attestation", suite.Suite.T())
4748
CreateGenericArtifactAttestation(suite.flowName, suite.trailName, suite.fingerprint, "second-artifact-attestation", true, suite.Suite.T())
4849
CreateGenericTrailAttestation(suite.flowName, suite.trailName, "second-trail-attestation", suite.Suite.T())
50+
51+
suite.attestationId = GetAttestationId(suite.flowName, suite.trailName, "first-trail-attestation", suite.Suite.T())
4952
}
5053

5154
func (suite *GetAttestationCommandTestSuite) TestGetAttestationCmd() {
5255
tests := []cmdTestCase{
5356
{
5457
wantError: false,
55-
name: "if no attestation found, say so",
58+
name: "01 if no attestation found when getting by name, say so",
5659
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
5760
golden: "No attestations found.\n",
5861
},
5962
{
6063
wantError: false,
61-
name: "if no attestation found return empty list in json format",
64+
name: "02 if no attestation found when getting by name return empty list in json format",
6265
cmd: fmt.Sprintf(`get attestation non-existent-attestation --flow %s --trail %s %s --output json`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
6366
goldenJson: []jsonCheck{{"", "[]"}},
6467
},
6568
{
6669
wantError: true,
67-
name: "providing more than one argument fails",
70+
name: "03 providing more than one argument fails",
6871
cmd: fmt.Sprintf(`get attestation first-attestation second-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
69-
golden: "Error: accepts 1 arg(s), received 2\n",
72+
golden: "Error: accepts at most 1 arg(s), received 2\n",
7073
},
7174
{
7275
wantError: true,
73-
name: "missing --flow fails",
76+
name: "04 missing --flow fails when ATTESTATION-NAME is provided",
7477
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --trail %s %s`, suite.trailName, suite.defaultKosliArguments),
75-
golden: "Error: required flag(s) \"flow\" not set\n",
78+
golden: "Error: --flow is required when using ATTESTATION-NAME\n",
7679
},
7780
{
7881
wantError: true,
79-
name: "missing --api-token fails",
82+
name: "05 missing --api-token fails",
8083
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --org orgX`, suite.flowName),
81-
golden: "Error: --api-token is not set\nUsage: kosli get attestation ATTESTATION-NAME [flags]\n",
84+
golden: "Error: --api-token is not set\nUsage: kosli get attestation [ATTESTATION-NAME] [flags]\n",
8285
},
8386
{
84-
name: "getting an existing trail attestation works",
87+
name: "06 getting an existing trail attestation works",
8588
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
8689
},
8790
{
88-
name: "getting an existing trail attestation with --output json works",
91+
name: "07 getting an existing trail attestation with --output json works",
8992
cmd: fmt.Sprintf(`get attestation first-trail-attestation --flow %s --trail %s --output json %s`, suite.flowName, suite.trailName, suite.defaultKosliArguments),
9093
goldenJson: []jsonCheck{{"", "non-empty"}},
9194
},
9295
{
93-
name: "getting an existing artifact attestation works",
96+
name: "08 getting an existing artifact attestation works",
9497
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
9598
},
9699
{
97-
name: "getting an existing artifact attestation with --output json works",
100+
name: "09 getting an existing artifact attestation with --output json works",
98101
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --fingerprint %s --output json %s`, suite.flowName, suite.fingerprint, suite.defaultKosliArguments),
99102
goldenJson: []jsonCheck{{"", "non-empty"}},
100103
},
101104
{
102105
wantError: true,
103-
name: "missing both trail and fingerprint fails",
106+
name: "10 missing both trail and fingerprint fails if ATTESTATION-NAME provided",
104107
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s %s`, suite.flowName, suite.defaultKosliArguments),
105-
golden: "Error: at least one of --trail, --fingerprint is required\n",
108+
golden: "Error: at least one of --trail, --fingerprint is required when using ATTESTATION-NAME\n",
106109
},
107110
{
108111
wantError: true,
109-
name: "providing both trail and fingerprint fails",
112+
name: "11 providing both trail and fingerprint fails",
110113
cmd: fmt.Sprintf(`get attestation first-artifact-attestation --flow %s --trail %s --fingerprint %s %s`, suite.flowName, suite.trailName, suite.fingerprint, suite.defaultKosliArguments),
111-
golden: "Error: only one of --trail, --fingerprint is allowed\n",
114+
golden: "Error: only one of --trail, --fingerprint is allowed when using ATTESTATION-NAME\n",
115+
},
116+
{
117+
name: "12 can get an attestation from its id",
118+
cmd: fmt.Sprintf(`get attestation --attestation-id %s %s`, suite.attestationId, suite.defaultKosliArguments),
119+
},
120+
{
121+
wantError: false,
122+
name: "13 if no attestation found when getting by name, say so",
123+
cmd: fmt.Sprintf(`get attestation --attestation-id %s %s`, "non-existent-attestation-id", suite.defaultKosliArguments),
124+
golden: "No attestations found.\n",
125+
},
126+
{
127+
wantError: false,
128+
name: "14 if no attestation found when getting by id return empty list in json format",
129+
cmd: fmt.Sprintf(`get attestation --attestation-id %s --output json %s`, "non-existent-attestation-id", suite.defaultKosliArguments),
130+
goldenJson: []jsonCheck{{"data", "length:0"}},
131+
},
132+
{
133+
wantError: true,
134+
name: "15 providing both attestation id and attestation name fails",
135+
cmd: fmt.Sprintf(`get attestation %s --attestation-id %s %s`, "first-artifact-attestation", suite.attestationId, suite.defaultKosliArguments),
136+
golden: "Error: --attestation-id cannot be used when ATTESTATION-NAME is provided\n",
137+
},
138+
{
139+
wantError: true,
140+
name: "16 providing both attestation id and trail fails",
141+
cmd: fmt.Sprintf(`get attestation --attestation-id %s --trail %s %s`, suite.attestationId, suite.trailName, suite.defaultKosliArguments),
142+
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
143+
},
144+
{
145+
wantError: true,
146+
name: "17 providing both attestation id and fingerprint fails",
147+
cmd: fmt.Sprintf(`get attestation --attestation-id %s --fingerprint %s %s`, suite.attestationId, suite.fingerprint, suite.defaultKosliArguments),
148+
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
149+
},
150+
{
151+
wantError: true,
152+
name: "18 providing both attestation id and flow fails",
153+
cmd: fmt.Sprintf(`get attestation --attestation-id %s --flow %s %s`, suite.attestationId, suite.flowName, suite.defaultKosliArguments),
154+
golden: "Error: --flow, --trail, and --fingerprint flags cannot be used with --attestation-id\n",
155+
},
156+
{
157+
wantError: true,
158+
name: "19 providing neither attestation id or flow fails",
159+
cmd: fmt.Sprintf(`get attestation %s`, suite.defaultKosliArguments),
160+
golden: "Error: one of ATTESTATION-NAME argument or --attestation-id flag is required\n",
112161
},
113162
}
114163

cmd/kosli/root.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,10 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
254254
attestationTypeJqFlag = "[optional] The attestation type evaluation JQ rules."
255255
envNameFlag = "The Kosli environment name to assert the artifact against."
256256
pathsWatchFlag = "[optional] Watch the filesystem for changes and report snapshots of artifacts running in specific filesystem paths to Kosli."
257-
getAttestationFingerprintFlag = "[conditional] The fingerprint of the artifact for the attestation. Cannot be used together with --trail."
258-
getAttestationTrailFlag = "[conditional] The name of the Kosli trail for the attestation. Cannot be used together with --fingerprint."
257+
getAttestationFingerprintFlag = "[conditional] The fingerprint of the artifact for the attestation. Cannot be used together with --trail or --attestation-id."
258+
getAttestationTrailNameFlag = "[conditional] The name of the Kosli trail for the attestation. Cannot be used together with --fingerprint or --attestation-id."
259+
getAttestationFlowNameFlag = "[conditional] The name of the Kosli flow for the attestation. Required if ATTESTATION-NAME provided. Cannot be used together with --attestation-id."
260+
attestationIDFlag = "[conditional] The unique identifier of the attestation to retrieve. Cannot be used together with ATTESTATION-NAME."
259261
)
260262

261263
var global *GlobalOpts

cmd/kosli/testHelpers.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,3 +560,24 @@ func CreateGenericTrailAttestation(flowName, trailName, attestationName string,
560560
err := o.run([]string{})
561561
require.NoError(t, err, "generic artifact attestation should be created without error")
562562
}
563+
564+
func GetAttestationId(flowName, trailName, attestationName string, t *testing.T) string {
565+
t.Helper()
566+
o := &getAttestationOptions{
567+
flow: flowName,
568+
trail: trailName,
569+
output: "json",
570+
}
571+
buffer := new(bytes.Buffer)
572+
err := o.run(buffer, []string{attestationName})
573+
require.NoError(t, err, "attestation should be retrieved without error")
574+
575+
var data []map[string]interface{}
576+
err = json.Unmarshal(buffer.Bytes(), &data)
577+
require.NoError(t, err, "failed to parse attestation JSON: %s", buffer.String())
578+
require.Greater(t, len(data), 0, "expected at least one attestation")
579+
580+
id, ok := data[0]["attestation_id"].(string)
581+
require.True(t, ok, "attestation_id field not found or not a string")
582+
return id
583+
}

0 commit comments

Comments
 (0)