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
68 changes: 65 additions & 3 deletions cmd/kosli/getRegoPolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"os"

"github.com/kosli-dev/cli/internal/output"
"github.com/kosli-dev/cli/internal/regopolicies"
Expand All @@ -21,7 +24,11 @@ one an evaluation runs, so it can be older than the newest version.

With ^--policy-version^, that version is shown with its status, its digest, who
published it and when, and the reason when it is rejected or failed. The command
exits 0 whatever the status of the version.`
exits 0 whatever the status of the version.

With ^--download^ and ^--policy-version^, the bundle of that version is written to the
given file as a gzipped tar, unchanged and whatever the status of the version, in place
of printing it. An existing file is not overwritten.`

const getRegoPolicyExample = `
# get a rego policy and its current version:
Expand All @@ -35,6 +42,13 @@ kosli get rego-policy pr-approved \
--api-token yourAPIToken \
--org yourOrgName

# download the bundle of version 3 of a rego policy to ./pr-approved-v3.tgz:
kosli get rego-policy pr-approved \
--policy-version 3 \
--download ./pr-approved-v3.tgz \
--api-token yourAPIToken \
--org yourOrgName

# get version 3 of a rego policy as JSON:
kosli get rego-policy pr-approved \
--policy-version 3 \
Expand All @@ -44,8 +58,9 @@ kosli get rego-policy pr-approved \
`

type getRegoPolicyOptions struct {
output string
version int
output string
version int
download string
}

func newGetRegoPolicyCmd(out io.Writer) *cobra.Command {
Expand All @@ -67,15 +82,22 @@ func newGetRegoPolicyCmd(out io.Writer) *cobra.Command {
if cmd.Flags().Changed("policy-version") && o.version < 1 {
return ErrorBeforePrintingUsage(cmd, "--policy-version must be a positive integer")
}
if o.download != "" && o.version == 0 {
return ErrorBeforePrintingUsage(cmd, "--download needs --policy-version")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if o.download != "" {
Comment thread
jumboduck marked this conversation as resolved.
return o.runDownload(out, args[0])
}
return o.run(out, args[0])
},
}

cmd.Flags().IntVar(&o.version, "policy-version", 0, regoPolicyVersionFlag)
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
cmd.Flags().StringVar(&o.download, "download", "", regoPolicyDownloadFlag)

return cmd
}
Expand Down Expand Up @@ -111,6 +133,46 @@ func (o *getRegoPolicyOptions) run(out io.Writer, identifier string) error {
})
}

func (o *getRegoPolicyOptions) runDownload(out io.Writer, identifier string) error {
endpoint, err := regopolicies.BundleURL(global.Host, global.Org, identifier, o.version)
if err != nil {
return err
}
response, err := kosliClient.Do(&requests.RequestParams{
Method: http.MethodGet,
URL: endpoint,
Token: global.ApiToken,
})
if err != nil {
return err
}
if err := writeNewFile(o.download, []byte(response.Body)); err != nil {
return err
}
_, err = fmt.Fprintf(out, "Downloaded version %d of rego policy '%s' to %s\n",
o.version, identifier, o.download)
return err
}

func writeNewFile(path string, content []byte) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
if errors.Is(err, fs.ErrExist) {
return fmt.Errorf("%s already exists; remove it or choose another file", path)
}
if err != nil {
return err
}
_, err = file.Write(content)
if closeErr := file.Close(); err == nil {
err = closeErr
}
if err != nil {
// A partial file would make a retry fail as "already exists".
_ = os.Remove(path)
}
return err
}

func printRegoPolicyAsTable(raw string, out io.Writer, page int) error {
var policy map[string]any
if err := json.Unmarshal([]byte(raw), &policy); err != nil {
Expand Down
91 changes: 91 additions & 0 deletions cmd/kosli/getRegoPolicy_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package main

import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -236,6 +241,92 @@ func (suite *GetRegoPolicyCommandTestSuite) TestItReturnsANotFoundInTheServersWo
require.Contains(suite.T(), combined, "Rego policy 'pr-approved' has no version 9 in organization 'test-org'")
}

// A gzipped tar, as the server sends it; the command must not look inside it.
func regoPolicyBundle(t *testing.T) string {
t.Helper()
var archive bytes.Buffer
zipped := gzip.NewWriter(&archive)
tw := tar.NewWriter(zipped)
body := "package policy\n"
require.NoError(t, tw.WriteHeader(&tar.Header{Name: "policy.rego", Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg}))
_, err := tw.Write([]byte(body))
require.NoError(t, err)
require.NoError(t, tw.Close())
require.NoError(t, zipped.Close())
return archive.String()
}

func (suite *GetRegoPolicyCommandTestSuite) TestItDownloadsTheBundleOfAVersionToAFile() {
bundle := regoPolicyBundle(suite.T())
server, fake := newFakeRegoPolicyRead(suite.T(), bundle)
file := filepath.Join(suite.T().TempDir(), "pr-approved-v3.tgz")

_, combined, _, _, err := executeCommandC(suite.getCmd(server.URL,
"pr-approved --policy-version 3 --download "+file))

require.NoError(suite.T(), err)
require.Equal(suite.T(), "/api/v2/rego-policies/test-org/pr-approved/versions/3/bundle", fake.escapedPath)
require.Equal(suite.T(),
fmt.Sprintf("Downloaded version 3 of rego policy 'pr-approved' to %s\n", file),
combined)
written, err := os.ReadFile(file)
require.NoError(suite.T(), err)
require.Equal(suite.T(), bundle, string(written))
}

func (suite *GetRegoPolicyCommandTestSuite) TestItRefusesToOverwriteAnExistingFile() {
server, _ := newFakeRegoPolicyRead(suite.T(), regoPolicyBundle(suite.T()))
file := filepath.Join(suite.T().TempDir(), "pr-approved-v3.tgz")
require.NoError(suite.T(), os.WriteFile(file, []byte("mine\n"), 0o644))

_, combined, _, _, err := executeCommandC(suite.getCmd(server.URL,
"pr-approved --policy-version 3 --download "+file))

require.Error(suite.T(), err)
require.Contains(suite.T(), combined, file+" already exists; remove it or choose another file")
kept, err := os.ReadFile(file)
require.NoError(suite.T(), err)
require.Equal(suite.T(), "mine\n", string(kept))
}

func (suite *GetRegoPolicyCommandTestSuite) TestItRefusesADownloadWithoutAVersion() {
server, fake := newFakeRegoPolicyRead(suite.T(), regoPolicyWithCurrentVersion)

_, combined, _, _, err := executeCommandC(suite.getCmd(server.URL,
"pr-approved --download "+filepath.Join(suite.T().TempDir(), "pr-approved.tgz")))

require.Error(suite.T(), err)
require.Contains(suite.T(), combined, "--download needs --policy-version")
require.Equal(suite.T(), 0, fake.calls)
}

// An unset variable in --download "$OUT" must fail, not fall back to printing the version.
func (suite *GetRegoPolicyCommandTestSuite) TestItRefusesAnEmptyDownloadFile() {
server, fake := newFakeRegoPolicyRead(suite.T(), regoPolicyValid)

_, combined, _, _, err := executeCommandC(suite.getCmd(server.URL,
`pr-approved --policy-version 1 --download ""`))

require.Error(suite.T(), err)
require.Contains(suite.T(), combined, "flag '--download' was given an empty value")
require.Equal(suite.T(), 0, fake.calls)
}

func (suite *GetRegoPolicyCommandTestSuite) TestItWritesNothingWhenTheVersionIsNotFound() {
server, fake := newFakeRegoPolicyRead(suite.T(),
`{"message":"Rego policy 'pr-approved' has no version 9 in organization 'test-org'"}`)
fake.status = http.StatusNotFound
file := filepath.Join(suite.T().TempDir(), "pr-approved-v9.tgz")

_, combined, _, _, err := executeCommandC(suite.getCmd(server.URL,
"pr-approved --policy-version 9 --download "+file))

require.Error(suite.T(), err)
require.Contains(suite.T(), combined, "Rego policy 'pr-approved' has no version 9 in organization 'test-org'")
require.Equal(suite.T(), 1, fake.calls)
require.NoFileExists(suite.T(), file)
}

func TestGetRegoPolicyCommandTestSuite(t *testing.T) {
suite.Run(t, new(GetRegoPolicyCommandTestSuite))
}
1 change: 1 addition & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Paths the list already matches stay excluded whatever is later added there, so k
regoPolicyDescriptionFlag = "[optional] The description of the rego policy. Without it the current description is kept."
regoPolicyCommentFlag = "[optional] Why this version is published. It is kept with the version."
regoPolicyVersionFlag = "[optional] The version of the rego policy to get. Without it, the policy and its current version are shown."
regoPolicyDownloadFlag = "[optional] The file to write the bundle of the version given by --policy-version to, as a gzipped tar. An existing file is not overwritten."
regoPolicySearchFlag = "[optional] Only list rego policies whose name or identifier contains this substring (case-insensitive)."
regoPolicySortFlag = "[optional] The field to sort rego policies by. Valid values are: [updated_at, identifier]. (defaults to updated_at)"
regoPolicySortDirectionFlag = "[optional] The direction to sort rego policies in. Valid values are: [asc, desc]. (defaults to desc)"
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/testdata/empty-flag-audit-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@
"output": "string"
},
"get rego-policy": {
"download": "string",
"output": "string",
"policy-version": "int"
},
Expand Down
2 changes: 2 additions & 0 deletions hack/empty-flag-audit/spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -1562,10 +1562,12 @@
"error": "the server the audit runs against does not offer the rego policy endpoints",
"needs": "server-side evaluation",
"flags_to_test": [
"download",
"output",
"policy-version"
],
"flag_values": {
"download": "probe-bundle",
"output": "json",
"policy-version": "1"
},
Expand Down
9 changes: 9 additions & 0 deletions internal/regopolicies/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ func VersionURL(host, org, identifier string, version int) (string, error) {
return url.JoinPath(endpoint, "versions", strconv.Itoa(version))
}

// BundleURL is the endpoint of the gzipped tar of one version's files.
func BundleURL(host, org, identifier string, version int) (string, error) {
endpoint, err := VersionURL(host, org, identifier, version)
if err != nil {
return "", err
}
return url.JoinPath(endpoint, "bundle")
}

// The server refuses unknown fields, so the wire shape is stated here exactly.
type publishPayload struct {
Name string `json:"name,omitempty"`
Expand Down
7 changes: 7 additions & 0 deletions internal/regopolicies/urls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ func TestListURLNamesTheOrg(t *testing.T) {
require.NoError(t, err)
require.Equal(t, "https://app.kosli.com/api/v2/rego-policies/my%20org", endpoint)
}

func TestBundleURLNamesTheBundleOfTheVersion(t *testing.T) {
endpoint, err := BundleURL("https://app.kosli.com", "my-org", "pr-approved", 3)

require.NoError(t, err)
require.Equal(t, "https://app.kosli.com/api/v2/rego-policies/my-org/pr-approved/versions/3/bundle", endpoint)
}
Loading