Skip to content
Draft
49 changes: 11 additions & 38 deletions cmd/kosli/assertArtifact.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package main

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"

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

Expand Down Expand Up @@ -150,7 +149,7 @@ func newAssertArtifactCmd(out io.Writer) *cobra.Command {
return ValidateRegistryFlags(cmd, o.fingerprintOptions)
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(out, args)
return o.run(cmd.Context(), out, args)
},
}

Expand All @@ -167,7 +166,7 @@ func newAssertArtifactCmd(out io.Writer) *cobra.Command {
return cmd
}

func (o *assertArtifactOptions) run(out io.Writer, args []string) error {
func (o *assertArtifactOptions) run(ctx context.Context, out io.Writer, args []string) error {
var err error
if o.fingerprint == "" {
o.fingerprint, err = GetSha256Digest(args[0], o.fingerprintOptions, logger)
Expand All @@ -176,42 +175,16 @@ func (o *assertArtifactOptions) run(out io.Writer, args []string) error {
}
}

baseURL, err := url.JoinPath(global.Host, "api/v2/asserts", global.Org, "fingerprint", o.fingerprint)
assertion, err := kosliAPI().AssertArtifact(ctx, o.fingerprint, kosli.AssertArtifactFilter{
Flow: o.flowName,
Environment: o.envName,
Policies: o.policyNames,
})
if err != nil {
return err
}
params := url.Values{}

if o.flowName != "" {
params.Add("flow_name", o.flowName)
}

if o.envName != "" {
params.Add("environment_name", o.envName)
}

if len(o.policyNames) > 0 {
for _, policy := range o.policyNames {
params.Add("policy_name", policy)
}
}

fullURL := baseURL
if len(params) > 0 {
fullURL += "?" + params.Encode()
}

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

err = output.FormattedPrint(response.Body, o.output, out, 0,
err = output.FormattedPrint(string(assertion.Raw), o.output, out, 0,
map[string]output.FormatOutputFunc{
"table": printAssertAsTable,
"json": output.PrintJson,
Expand All @@ -220,7 +193,7 @@ func (o *assertArtifactOptions) run(out io.Writer, args []string) error {
return err
}

return assertCompliant(response.Body)
return assertCompliant(string(assertion.Raw))
}

// assertCompliant decodes only the compliant field, so the exit code CI gates
Expand Down
113 changes: 33 additions & 80 deletions cmd/kosli/getArtifact.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package main

import (
"encoding/json"
"context"
"fmt"
"io"
"net/http"
neturl "net/url"
"sort"
"strings"

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

Expand Down Expand Up @@ -62,33 +60,6 @@ type getArtifactOptions struct {
trail string
}

type artifactResponse struct {
Filename string `json:"filename"`
FlowName string `json:"flow_name"`
TrailName *string `json:"trail_name"`
TemplateReferenceName *string `json:"template_reference_name"`
Fingerprint string `json:"fingerprint"`
CreatedAt json.Number `json:"created_at"`
GitCommit string `json:"git_commit"`
CommitURL string `json:"commit_url"`
BuildURL string `json:"build_url"`
HTMLURL string `json:"html_url"`
State string `json:"state"`
Running []environmentRef `json:"running"`
Exited []environmentRef `json:"exited"`
History []artifactEvent `json:"history"`
}

type environmentRef struct {
EnvironmentName string `json:"environment_name"`
SnapshotIndex float64 `json:"snapshot_index"`
}

type artifactEvent struct {
Event string `json:"event"`
Timestamp json.Number `json:"timestamp"`
}

func newGetArtifactCmd(out io.Writer) *cobra.Command {
o := new(getArtifactOptions)
cmd := &cobra.Command{
Expand All @@ -105,7 +76,7 @@ func newGetArtifactCmd(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)
},
}

Expand All @@ -115,64 +86,46 @@ func newGetArtifactCmd(out io.Writer) *cobra.Command {
return cmd
}

func (o *getArtifactOptions) run(out io.Writer, args []string) error {
func (o *getArtifactOptions) run(ctx context.Context, out io.Writer, args []string) error {
flowName, id, separator, err := handleArtifactExpression(args[0])
if err != nil {
return err
}
var idType string

var raw []byte
var artifacts func() ([]kosli.Artifact, error)
if separator == "@" {
idType = "fingerprint"
artifact, err := kosliAPI().GetArtifactByFingerprint(ctx, flowName, id, o.trail)
if err != nil {
return err
}
raw = artifact.Raw
artifacts = func() ([]kosli.Artifact, error) {
value, err := artifact.Value()
return []kosli.Artifact{value}, err
}
} else {
idType = "commit_sha"
}

url, err := neturl.JoinPath(global.Host, "api/v2/artifacts", global.Org, flowName, idType, id)
if err != nil {
return err
}
if o.trail != "" {
q := neturl.Values{}
q.Set("trail", o.trail)
url += "?" + q.Encode()
}
reqParams := &requests.RequestParams{
Method: http.MethodGet,
URL: url,
Token: global.ApiToken,
}

response, err := kosliClient.Do(reqParams)
if err != nil {
return err
listed, err := kosliAPI().ListArtifactsByCommitSHA(ctx, flowName, id, o.trail)
if err != nil {
return err
}
raw, artifacts = listed.Raw, listed.Value
}

return output.FormattedPrint(response.Body, o.output, out, 0,
return output.FormattedPrint(string(raw), o.output, out, 0,
Comment thread
claude[bot] marked this conversation as resolved.
map[string]output.FormatOutputFunc{
"table": printArtifactAsTableWrapper,
"json": output.PrintJson,
"table": func(_ string, out io.Writer, pageNumber int) error {
value, err := artifacts()
if err != nil {
return err
}
return printArtifactsAsTable(value, out, pageNumber)
},
"json": output.PrintJson,
})
}

func printArtifactAsTableWrapper(artifactRaw string, out io.Writer, pageNumber int) error {
// TODO: we have this function for backward compatibility with API.
// API returns array when querying with commit and returns single map for sha256.
// In the future, the return json should always be an array
if artifactRaw[0] != '[' {
artifactRaw = "[" + artifactRaw + "]"
}
return printArtifactsAsTable(artifactRaw, out, pageNumber)
}

func printArtifactsAsTable(artifactRaw string, out io.Writer, pageNumber int) error {
var artifacts []artifactResponse
if err := json.Unmarshal([]byte(artifactRaw), &artifacts); err != nil {
return err
}
return printArtifactsJsonAsTable(artifacts, out, pageNumber)
}

func printArtifactsJsonAsTable(artifacts []artifactResponse, out io.Writer, pageNumber int) error {
func printArtifactsAsTable(artifacts []kosli.Artifact, out io.Writer, pageNumber int) error {
separator := ""
for _, artifact := range artifacts {
rows, err := artifactTableRows(artifact)
Expand All @@ -186,7 +139,7 @@ func printArtifactsJsonAsTable(artifacts []artifactResponse, out io.Writer, page
return nil
}

func artifactTableRows(artifact artifactResponse) ([]string, error) {
func artifactTableRows(artifact kosli.Artifact) ([]string, error) {
rows := []string{
fmt.Sprintf("Name:\t%s", artifact.Filename),
fmt.Sprintf("Flow:\t%s", artifact.FlowName),
Expand Down Expand Up @@ -234,7 +187,7 @@ func artifactTableRows(artifact artifactResponse) ([]string, error) {
}

// environmentRefNames renders each reference as name#snapshot, e.g. "prod#12".
func environmentRefNames(refs []environmentRef) []string {
func environmentRefNames(refs []kosli.EnvironmentRef) []string {
names := make([]string, 0, len(refs))
for _, ref := range refs {
names = append(names, fmt.Sprintf("%s#%.0f", ref.EnvironmentName, ref.SnapshotIndex))
Expand Down
48 changes: 35 additions & 13 deletions cmd/kosli/getArtifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"github.com/kosli-dev/cli/internal/kosli"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
Expand Down Expand Up @@ -107,13 +111,13 @@ func TestGetArtifactCommandTestSuite(t *testing.T) {
}

func TestPrintArtifactsAsTableToleratesMissingCollections(t *testing.T) {
// a server response without created_at, running, exited or history must
// render N/A and omit those sections rather than panic
raw := `[{"filename":"arti","flow_name":"flow-1","fingerprint":"abc","git_commit":"0fc1ba9",
"commit_url":"https://c","build_url":"https://b","html_url":"https://h","state":"COMPLIANT"}]`
// an artifact without created_at, running, exited or history must render
// N/A and omit those sections rather than panic
artifacts := []kosli.Artifact{{Filename: "arti", FlowName: "flow-1", Fingerprint: "abc", GitCommit: "0fc1ba9",
CommitURL: "https://c", BuildURL: "https://b", HTMLURL: "https://h", State: "COMPLIANT"}}
var buf bytes.Buffer
require.NotPanics(t, func() {
require.NoError(t, printArtifactsAsTable(raw, &buf, 0))
require.NoError(t, printArtifactsAsTable(artifacts, &buf, 0))
})
out := buf.String()
require.Regexp(t, `Created on:\s+N/A\n`, out)
Expand All @@ -122,12 +126,11 @@ func TestPrintArtifactsAsTableToleratesMissingCollections(t *testing.T) {
require.NotContains(t, out, "Trail:")
}

func TestPrintArtifactsAsTableAcceptsStringTimestamps(t *testing.T) {
// the artifacts API returns created_at as a numeric string, not a number
raw := `[{"filename":"arti","flow_name":"flow-1","fingerprint":"abc","created_at":"1452902400.25",
"history":[{"event":"cli reported","timestamp":"1452902400"}]}]`
func TestPrintArtifactsAsTablePrintsFractionalTimestamps(t *testing.T) {
artifacts := []kosli.Artifact{{Filename: "arti", CreatedAt: "1452902400.25",
History: []kosli.ArtifactEvent{{Event: "cli reported", Timestamp: "1452902400"}}}}
var buf bytes.Buffer
require.NoError(t, printArtifactsAsTable(raw, &buf, 0))
require.NoError(t, printArtifactsAsTable(artifacts, &buf, 0))
out := buf.String()
require.NotRegexp(t, `Created on:\s+N/A`, out)
require.Contains(t, out, "History:\n")
Expand All @@ -137,11 +140,30 @@ func TestPrintArtifactsAsTableAcceptsStringTimestamps(t *testing.T) {
func TestPrintArtifactsAsTablePrintsPresentButEmptyTrailRows(t *testing.T) {
// a key the server sends prints its row even when empty; only an absent
// or null key leaves the row out
raw := `[{"filename":"arti","flow_name":"flow-1","fingerprint":"abc",
"trail_name":"","template_reference_name":""}]`
empty := ""
artifacts := []kosli.Artifact{{Filename: "arti", TrailName: &empty, TemplateReferenceName: &empty}}
var buf bytes.Buffer
require.NoError(t, printArtifactsAsTable(raw, &buf, 0))
require.NoError(t, printArtifactsAsTable(artifacts, &buf, 0))
out := buf.String()
require.Regexp(t, `(?m)^Trail:\s*$`, out)
require.Regexp(t, `(?m)^Name in template:\s*$`, out)
}

func TestGetArtifactPrintsJSONThatTheTableCannotDecode(t *testing.T) {
// -o json prints what the server sent, so a field the typed table cannot
// decode must not fail it; only the table reports the decode error
body := `{"filename":"arti","created_at":""}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte(body))
assert.NoError(t, err)
}))
defer server.Close()
flags := fmt.Sprintf("--host %s --org test-org --api-token test-token", server.URL)

_, _, stdout, _, err := executeCommandC("get artifact flow-1@abc --output json " + flags)
require.NoError(t, err)
require.JSONEq(t, body, stdout)

_, _, _, _, err = executeCommandC("get artifact flow-1@abc " + flags)
require.ErrorContains(t, err, "failed to decode the Kosli API response")
}
Loading
Loading