Skip to content
Open
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
40 changes: 21 additions & 19 deletions cmd/kosli/attestGeneric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,24 @@ func (suite *AttestGenericCommandTestSuite) TestAttestGenericCmd() {
golden: "Error: accepts at most 1 arg(s), received 2 [foo bar]\n",
},
{
wantError: true,
name: "fails when artifact-name is provided and there is an --artifact-type flag and --compliant is not set with =",
cmd: fmt.Sprintf("attest generic testdata/file1 %s --artifact-type file --compliant false", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2 [testdata/file1 false]\nSee https://docs.kosli.com//faq/#boolean-flags\n",
name: "reports a non-compliant attestation when --compliant is given with a space",
cmd: fmt.Sprintf("attest generic testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url http://example.com --compliant false %s", suite.defaultKosliArguments),
golden: "generic attestation 'foo' is reported to trail: test-123\n",
},
{
name: "passes through a non-boolean flag whose value is the literal false",
cmd: fmt.Sprintf("attest generic testdata/file1 --artifact-type file --name false --commit HEAD --origin-url http://example.com %s", suite.defaultKosliArguments),
golden: "generic attestation 'false' is reported to trail: test-123\n",
},
{
name: "reports a non-compliant attestation when the -C shorthand is given with a space",
cmd: fmt.Sprintf("attest generic testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url http://example.com -C false %s", suite.defaultKosliArguments),
golden: "generic attestation 'foo' is reported to trail: test-123\n",
},
{
name: "reports a non-compliant attestation when --compliant is given with =",
cmd: fmt.Sprintf("attest generic testdata/file1 --artifact-type file --name foo --commit HEAD --origin-url http://example.com --compliant=false %s", suite.defaultKosliArguments),
golden: "generic attestation 'foo' is reported to trail: test-123\n",
},
{
wantError: true,
Expand All @@ -55,28 +69,16 @@ func (suite *AttestGenericCommandTestSuite) TestAttestGenericCmd() {
},
{
wantError: true,
name: "fails when artifact-name is provided (as _unused_ boolean 'space' arg) and there is no --artifact-type and no --fingerprint",
cmd: fmt.Sprintf("attest generic %s --compliant false", suite.defaultKosliArguments),
golden: "Error: --artifact-type or --fingerprint must be specified when artifact name ('false') argument is supplied.\nSee https://docs.kosli.com//faq/#boolean-flags\nUsage: kosli attest generic [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
name: "links to the boolean flags FAQ when a stray true|false argument remains",
cmd: fmt.Sprintf("attest generic foo false --artifact-type file --name bar %s", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2 [foo false]\nSee https://docs.kosli.com//faq/#boolean-flags\n",
},
{
wantError: true,
name: "fails when artifact-name is provided and there is no --artifact-type",
cmd: fmt.Sprintf("attest generic wibble %s", suite.defaultKosliArguments),
golden: "Error: --artifact-type or --fingerprint must be specified when artifact name ('wibble') argument is supplied.\nUsage: kosli attest generic [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
},
{
wantError: true,
name: "fails when there are extra args and gives custom help message when an argument is true|false",
cmd: fmt.Sprintf("attest generic foo -t file %s --compliant false", suite.defaultKosliArguments),
golden: "Error: accepts at most 1 arg(s), received 2 [foo false]\nSee https://docs.kosli.com//faq/#boolean-flags\n",
},
{
wantError: true,
name: "fails when there are extra args and gives custom help message when an argument is true|false",
cmd: fmt.Sprintf("attest generic %s --compliant false", suite.defaultKosliArguments),
golden: "Error: --artifact-type or --fingerprint must be specified when artifact name ('false') argument is supplied.\nSee https://docs.kosli.com//faq/#boolean-flags\nUsage: kosli attest generic [IMAGE-NAME | FILE-PATH | DIR-PATH] [flags]\n",
},
{
wantError: true,
name: "fails when both --fingerprint and --artifact-type",
Expand Down
6 changes: 6 additions & 0 deletions cmd/kosli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,13 @@ func enrichError(cmd *cobra.Command, err error) error {
return fmt.Errorf("[%s] %w", strings.Join(parts, " "), err)
}

// innerMain runs cmd against args (args[0] being the program name) and turns
// the outcome into the process-level error, printing the update notice on the
// --version path and reporting errors in the friendliest available form.
func innerMain(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
cmd.SetArgs(normalizeBoolFlagArgs(cmd, args[1:]))
}
executedCmd, err := cmd.ExecuteC()
if err == nil {
// Cobra handles --version internally and bypasses all hooks, so we print
Expand Down
7 changes: 7 additions & 0 deletions cmd/kosli/multiHost.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ func getMultiOpts() MultiOpts {
cmd.SetOut(writer)
cmd.SetErr(writer)

// Parse the same normalized args innerMain will run with, so a boolean flag
// written in the space form (eg --debug false) does not leave a stray
// positional argument that fails arg validation and empties MultiOpts.
if len(os.Args) > 1 {
cmd.SetArgs(normalizeBoolFlagArgs(cmd, os.Args[1:]))
}

fakeError := errors.New("")

cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
Expand Down
24 changes: 24 additions & 0 deletions cmd/kosli/multiHost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,30 @@ func (suite *MultiHostTestSuite) TestRunDoubledHost() {
}
}

// TestRunDoubledHostAcceptsSpaceSeparatedBoolFlag checks that a boolean flag
// written in the space form is honoured on the multi-host path too. --debug is
// false here, so the output must be the bare fingerprint: no per-host [debug]
// prefix and no secondary-call output. fingerprint is used because it needs no
// server, so the two hosts are never contacted.
func (suite *MultiHostTestSuite) TestRunDoubledHostAcceptsSpaceSeparatedBoolFlag() {
args := []string{
"kosli", "fingerprint", "testdata/person-schema.json",
"--artifact-type", "file",
"--debug", "false",
fmt.Sprintf("--host=%s,%s", localHost, localHost),
fmt.Sprintf("--api-token=%s,%s", apiToken, apiToken),
fmt.Sprintf("--org=%s", orgName),
}
want := []string{"1bef738d0bb1e690500f99a5b57d958caf3a5eb3e00d9012e1f4369fc6812e01", ""}

defer func(original []string) { os.Args = original }(os.Args)
os.Args = args
output, err := runMultiHost(args)

assert.Equal(suite.T(), error(nil), err)
assert.Equal(suite.T(), "", diff(want, strings.Split(output, "\n")))
}

func (suite *MultiHostTestSuite) TestRunTripledHost() {

multiHost := fmt.Sprintf("--host=%s,%s,%s", localHost, localHost, localHost)
Expand Down
75 changes: 75 additions & 0 deletions cmd/kosli/normalizeBoolFlagArgs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package main

import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// normalizeBoolFlagArgs rewrites boolean flags written in the space form
// ("--compliant false") into the "=" form ("--compliant=false"), which is the
// only form pflag accepts for an explicitly-valued boolean flag. All other
// tokens are returned unchanged, as is everything following a "--" terminator.
//
// The rewrite is positional: it matches on the tokens themselves and does not
// track which of them pflag will consume as the value of an earlier flag. Two
// pathological inputs are therefore read differently from the way pflag reads
// them, and both are accepted. A positional argument literally named "true" or
// "false" directly after a bare boolean flag becomes that flag's value. And in
// "--name --compliant false", where pflag gives --name the value "--compliant"
// and leaves "false" positional, this rewrite instead produces
// "--compliant=false". Kosli positionals are artifact names, fingerprints and
// file paths, and flag values are not flag tokens, so both inputs are far
// enough outside real usage to be worth the plain forms this rescues.
func normalizeBoolFlagArgs(root *cobra.Command, args []string) []string {
cmd, _, err := root.Find(args)
if err != nil {
return args
}
boolFlags := boolFlagTokens(cmd)
Comment thread
JonJagger marked this conversation as resolved.
normalized := make([]string, 0, len(args))
for i := 0; i < len(args); i++ {
if args[i] == "--" {
// pflag stops parsing flags here, so everything left is a
// positional argument and must be passed through untouched.
normalized = append(normalized, args[i:]...)
break
}
if boolFlags[args[i]] && i+1 < len(args) && isBoolLiteral(args[i+1]) {
Comment thread
JonJagger marked this conversation as resolved.
normalized = append(normalized, args[i]+"="+args[i+1])
i++
continue
}
normalized = append(normalized, args[i])
}
return normalized
}

// boolFlagTokens returns the command-line tokens ("--compliant", "-C") of every
// boolean flag known to cmd.
//
// Shorthands are listed individually, so a grouped token such as "-qC" is not a
// key here and "-qC false" is left alone: it keeps failing with the arg-count
// error and its link to the boolean flags FAQ. That is a deliberate choice.
// Grouping raises questions this rewrite has no good answer to, such as which
// member of the group the value belongs to and what to do when a group mixes
// boolean and non-boolean shorthands. Rescuing the plain forms is worth a
// low-level rewrite; guessing intent inside a grouped token is not.
func boolFlagTokens(cmd *cobra.Command) map[string]bool {
tokens := map[string]bool{}
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
if flag.Value.Type() != "bool" {
return
}
tokens["--"+flag.Name] = true
if flag.Shorthand != "" {
tokens["-"+flag.Shorthand] = true
}
})
return tokens
}

// isBoolLiteral reports whether token is one of the two values a rewritten
// boolean flag may be given.
func isBoolLiteral(token string) bool {
return token == "true" || token == "false"
}
181 changes: 181 additions & 0 deletions cmd/kosli/normalizeBoolFlagArgs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package main

import (
"io"
"testing"

"github.com/stretchr/testify/require"
)

// TestNormalizeBoolFlagArgsLeavesTrailingBareBoolFlagAlone covers the boundary
// where a boolean flag is the final token, so there is no following token to
// consider joining. The bare form keeps its NoOptDefVal of true.
func TestNormalizeBoolFlagArgsLeavesTrailingBareBoolFlagAlone(t *testing.T) {
args := []string{
"fingerprint", "testdata/person-schema.json",
"--artifact-type", "file",
"--debug",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, args, normalized)
}

// TestNormalizeBoolFlagArgsLeavesGroupedShorthandsAlone pins the deliberate
// decision not to rewrite a grouped shorthand token, so that "-qC false" keeps
// failing with the arg-count error and its link to the boolean flags FAQ. See
// boolFlagTokens for why. This test exists so that widening the rewrite to
// cover groups is a visible choice rather than a silent one.
func TestNormalizeBoolFlagArgsLeavesGroupedShorthandsAlone(t *testing.T) {
args := []string{
"attest", "generic", "testdata/file1",
"--artifact-type", "file",
"-qC", "false",
"--name", "foo",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, args, normalized)
}

// TestNormalizeBoolFlagArgsStopsAtTerminator checks that nothing after the "--"
// terminator is rewritten. pflag stops parsing flags there, so every later
// token is a positional argument, however much it looks like a flag. Rewriting
// past the terminator would alter arguments the flag parser never inspects.
func TestNormalizeBoolFlagArgsStopsAtTerminator(t *testing.T) {
args := []string{
"fingerprint", "--artifact-type", "file",
"--", "--debug", "false",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, args, normalized)
}

// TestNormalizeBoolFlagArgsLeavesUndocumentedBoolLiteralsAlone pins the
// deliberate restriction to the two literals Kosli documents. pflag's bool
// values go through strconv.ParseBool, so the "=" form also accepts 1, 0, t, f,
// TRUE, False and friends, but the space form is rewritten only for "true" and
// "false". Widening this would capture positionals named "0" or "1", which are
// far more plausible artifact names than "true", and would mean guessing intent
// on input no Kosli documentation describes. The undocumented forms therefore
// keep failing loudly instead of being interpreted.
func TestNormalizeBoolFlagArgsLeavesUndocumentedBoolLiteralsAlone(t *testing.T) {
for _, literal := range []string{"TRUE", "True", "FALSE", "False", "1", "0", "t", "f"} {
t.Run(literal, func(t *testing.T) {
args := []string{
"attest", "generic", "testdata/file1",
"--artifact-type", "file",
"--compliant", literal,
"--name", "foo",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, args, normalized)
})
}
}

// TestNormalizeBoolFlagArgsCapturesPositionalNamedTrue pins the one accepted
// ambiguity of the rewrite: a positional argument whose literal value is "true"
// or "false" immediately after a bare boolean flag is taken as that flag's
// value. Here the artifact being fingerprinted is a file named "true", and it
// is swallowed by --debug. This is deliberate: Kosli positionals are artifact
// names, fingerprints and file paths, for which such a name is pathological,
// and distinguishing the two cases would mean predicting each command's
// expected argument count.
func TestNormalizeBoolFlagArgsCapturesPositionalNamedTrue(t *testing.T) {
args := []string{"fingerprint", "--debug", "true", "--artifact-type", "file"}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, []string{"fingerprint", "--debug=true", "--artifact-type", "file"}, normalized)
}

// TestNormalizeBoolFlagArgsCapturesBoolFlagTokenUsedAsFlagValue pins the second
// accepted ambiguity of the rewrite: a boolean flag token given as the value of
// a preceding value-expecting flag is still rewritten. Here pflag would give
// --name the value "--compliant" and leave "false" as a positional argument,
// whereas the rewrite reads the same tokens as a space-form boolean. This is
// deliberate: telling the two apart would mean tracking which tokens pflag
// consumes as values, and a flag value that is itself a flag token is
// pathological.
func TestNormalizeBoolFlagArgsCapturesBoolFlagTokenUsedAsFlagValue(t *testing.T) {
args := []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--name", "--compliant", "false",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--name", "--compliant=false",
}, normalized)
}

// TestNormalizeBoolFlagArgsJoinsInheritedBoolFlag checks that a boolean flag
// inherited from a parent command is rewritten too, not just one declared on
// the subcommand itself.
func TestNormalizeBoolFlagArgsJoinsInheritedBoolFlag(t *testing.T) {
args := []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--debug", "false",
"--flow", "my-flow",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--debug=false",
"--flow", "my-flow",
}, normalized)
}

// TestNormalizeBoolFlagArgsJoinsSpaceSeparatedBoolValue checks that a boolean
// flag written in the space form is rewritten into the "=" form, so that
// `--compliant false` stops leaving "false" behind as a positional argument.
func TestNormalizeBoolFlagArgsJoinsSpaceSeparatedBoolValue(t *testing.T) {
args := []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--compliant", "false",
"--flow", "my-flow",
"--trail", "my-trail",
}
root, err := newRootCmd(io.Discard, io.Discard, args)
require.NoError(t, err)

normalized := normalizeBoolFlagArgs(root, args)

require.Equal(t, []string{
"attest", "generic", "Dockerfile",
"--artifact-type", "file",
"--compliant=false",
"--flow", "my-flow",
"--trail", "my-trail",
}, normalized)
}
Loading
Loading