diff --git a/cmd/kosli/evaluateHelpers.go b/cmd/kosli/evaluateHelpers.go index 5bba23e2a..dfab5f178 100644 --- a/cmd/kosli/evaluateHelpers.go +++ b/cmd/kosli/evaluateHelpers.go @@ -101,24 +101,14 @@ func (o *commonEvaluateOptions) assertOnDeny() bool { return !o.noAssert } -func fetchAndEnrichTrail(flowName, trailName string, attestations []string) (any, error) { - trailURL, err := url.JoinPath(global.Host, "api/v2/trails", global.Org, flowName, trailName) - if err != nil { - return nil, err - } - - reqParams := &requests.RequestParams{ - Method: http.MethodGet, - URL: trailURL, - Token: global.ApiToken, - } - response, err := kosliClient.Do(reqParams) +func fetchAndEnrichTrail(ctx context.Context, flowName, trailName string, attestations []string) (any, error) { + trail, err := kosliAPI().GetTrail(ctx, flowName, trailName) if err != nil { return nil, err } var trailData any - err = json.Unmarshal([]byte(response.Body), &trailData) + err = json.Unmarshal(trail.Raw, &trailData) if err != nil { return nil, fmt.Errorf("failed to parse trail response: %v", err) } diff --git a/cmd/kosli/evaluateTrail.go b/cmd/kosli/evaluateTrail.go index fcaf9a66d..0c07e250c 100644 --- a/cmd/kosli/evaluateTrail.go +++ b/cmd/kosli/evaluateTrail.go @@ -1,6 +1,7 @@ package main import ( + "context" "io" "github.com/kosli-dev/cli/internal/evaluations" @@ -103,7 +104,7 @@ func newEvaluateTrailCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(out, args) + return o.run(cmd.Context(), out, args) }, } @@ -118,13 +119,13 @@ func newEvaluateTrailCmd(out io.Writer) *cobra.Command { return cmd } -func (o *evaluateTrailOptions) run(out io.Writer, args []string) error { +func (o *evaluateTrailOptions) run(ctx context.Context, out io.Writer, args []string) error { if o.serverSide { return evaluateServerSide(out, &o.commonEvaluateOptions, []evaluations.TrailRef{{Flow: o.flowName, Trail: args[0]}}) } - trailData, err := fetchAndEnrichTrail(o.flowName, args[0], o.attestations) + trailData, err := fetchAndEnrichTrail(ctx, o.flowName, args[0], o.attestations) if err != nil { return err } diff --git a/cmd/kosli/evaluateTrails.go b/cmd/kosli/evaluateTrails.go index 11e6eb907..b83daf956 100644 --- a/cmd/kosli/evaluateTrails.go +++ b/cmd/kosli/evaluateTrails.go @@ -1,6 +1,7 @@ package main import ( + "context" "io" "github.com/kosli-dev/cli/internal/evaluations" @@ -96,7 +97,7 @@ func newEvaluateTrailsCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(out, args) + return o.run(cmd.Context(), out, args) }, } @@ -111,7 +112,7 @@ func newEvaluateTrailsCmd(out io.Writer) *cobra.Command { return cmd } -func (o *evaluateTrailsOptions) run(out io.Writer, args []string) error { +func (o *evaluateTrailsOptions) run(ctx context.Context, out io.Writer, args []string) error { if o.serverSide { refs := make([]evaluations.TrailRef, 0, len(args)) for _, trailName := range args { @@ -122,7 +123,7 @@ func (o *evaluateTrailsOptions) run(out io.Writer, args []string) error { var trails []any for _, trailName := range args { - trailData, err := fetchAndEnrichTrail(o.flowName, trailName, o.attestations) + trailData, err := fetchAndEnrichTrail(ctx, o.flowName, trailName, o.attestations) if err != nil { return err } diff --git a/cmd/kosli/getTrail.go b/cmd/kosli/getTrail.go index ec3b2e322..1f313aa5f 100644 --- a/cmd/kosli/getTrail.go +++ b/cmd/kosli/getTrail.go @@ -1,16 +1,15 @@ package main import ( + "context" "encoding/json" "fmt" "io" - "net/http" "net/url" "sort" "strings" "github.com/kosli-dev/cli/internal/output" - "github.com/kosli-dev/cli/internal/requests" "github.com/spf13/cobra" ) @@ -36,7 +35,7 @@ func newGetTrailCmd(out io.Writer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, args []string) error { - return o.run(out, args) + return o.run(cmd.Context(), out, args) }, } @@ -51,23 +50,13 @@ func newGetTrailCmd(out io.Writer) *cobra.Command { return cmd } -func (o *getTrailOptions) run(out io.Writer, args []string) error { - url, err := url.JoinPath(global.Host, "api/v2/trails", global.Org, o.flowName, args[0]) +func (o *getTrailOptions) run(ctx context.Context, out io.Writer, args []string) error { + trail, err := kosliAPI().GetTrail(ctx, o.flowName, args[0]) if err != nil { return err } - reqParams := &requests.RequestParams{ - Method: http.MethodGet, - URL: url, - Token: global.ApiToken, - } - response, err := kosliClient.Do(reqParams) - if err != nil { - return err - } - - return output.FormattedPrint(response.Body, o.output, out, 0, + return output.FormattedPrint(string(trail.Raw), o.output, out, 0, map[string]output.FormatOutputFunc{ "table": printTrailAsTable, "json": output.PrintJson, diff --git a/cmd/kosli/main.go b/cmd/kosli/main.go index b4f56fb33..ca72bd255 100644 --- a/cmd/kosli/main.go +++ b/cmd/kosli/main.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/kosli-dev/cli/internal/kosli" log "github.com/kosli-dev/cli/internal/logger" "github.com/kosli-dev/cli/internal/requests" "github.com/kosli-dev/cli/internal/version" @@ -25,6 +26,17 @@ func init() { kosliClient, _ = requests.NewKosliClient("", 3, false, logger) } +// kosliAPI returns a Kosli API client for the global flags as they are now. +// It is built on every call rather than once, so that multi-host runs and +// tests that set global directly each get a client for their own values. +func kosliAPI() *kosli.Client { + var sender kosli.Sender = kosliClient + if global.DryRun { + sender = kosli.DryRun(sender) + } + return kosli.New(sender, global.Host, global.Org, global.ApiToken) +} + func main() { var err error if isMultiHost() { diff --git a/internal/kosli/client.go b/internal/kosli/client.go index c51a380da..2771afbc4 100644 --- a/internal/kosli/client.go +++ b/internal/kosli/client.go @@ -1,5 +1,6 @@ // Package kosli is a client for the Kosli API, with one file per tag of the -// API's OpenAPI spec. +// API's OpenAPI spec. Each endpoint method's godoc names the operationId it +// calls, so the hand-written methods can be matched against the spec. package kosli import ( diff --git a/internal/kosli/trails.go b/internal/kosli/trails.go new file mode 100644 index 000000000..60629b7ae --- /dev/null +++ b/internal/kosli/trails.go @@ -0,0 +1,18 @@ +package kosli + +import ( + "context" + "net/http" + + "github.com/kosli-dev/cli/internal/requests" +) + +// GetTrail calls get_trail. It returns the Trail called name in flow, +// undecoded: its callers either print the body or reshape it as arbitrary JSON. +func (c *Client) GetTrail(ctx context.Context, flow, name string) (*Result, error) { + endpoint, err := c.endpoint("trails", c.org, flow, name) + if err != nil { + return nil, err + } + return c.send(ctx, &requests.RequestParams{Method: http.MethodGet, URL: endpoint}) +} diff --git a/internal/kosli/trails_test.go b/internal/kosli/trails_test.go new file mode 100644 index 000000000..62fdb0133 --- /dev/null +++ b/internal/kosli/trails_test.go @@ -0,0 +1,41 @@ +package kosli + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetTrailRequestsTheTrailInTheOrgAndReturnsTheRawBody(t *testing.T) { + server, seen := newFakeServer(t, http.StatusOK, `{"name":"trail 1","compliance_status":{}}`) + client := newTestClient(t, server.URL) + + result, err := client.GetTrail(context.Background(), "flow-1", "trail 1") + + require.NoError(t, err) + require.Equal(t, http.MethodGet, seen.method) + require.Equal(t, "/api/v2/trails/test-org/flow-1/trail%201", seen.rawPath) + require.JSONEq(t, `{"name":"trail 1","compliance_status":{}}`, string(result.Raw)) +} + +func TestGetTrailOfAMissingTrailIsNotFound(t *testing.T) { + server, _ := newFakeServer(t, http.StatusNotFound, `{"message":"Trail not found"}`) + client := newTestClient(t, server.URL) + + _, err := client.GetTrail(context.Background(), "flow-1", "missing") + + require.True(t, IsNotFound(err)) +} + +func TestGetTrailInADryRunStillReadsTheTrail(t *testing.T) { + server, seen := newFakeServer(t, http.StatusOK, `{"name":"trail-1"}`) + client := New(DryRun(newHTTPClient(t)), server.URL, "test-org", "test-token") + + result, err := client.GetTrail(context.Background(), "flow-1", "trail-1") + + require.NoError(t, err) + require.Equal(t, 1, seen.hits) + require.JSONEq(t, `{"name":"trail-1"}`, string(result.Raw)) +}