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
1 change: 1 addition & 0 deletions cmd/kosli/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func newArchiveCmd(out io.Writer) *cobra.Command {
newArchiveFlowCmd(out),
newArchiveEnvironmentCmd(out),
newArchiveAttestationTypeCmd(out),
newArchiveControlCmd(out),
)
return cmd
}
62 changes: 62 additions & 0 deletions cmd/kosli/archiveControl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package main

import (
"io"
"net/http"
"net/url"

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

const archiveControlShortDesc = `Archive a Kosli control.`

const archiveControlLongDesc = archiveControlShortDesc + `
An archived control is no longer active. It remains visible via ^kosli get control^ and
via ^kosli list controls --archived^, and can be restored with ^kosli unarchive control^.
`

const archiveControlExample = `
# archive a Kosli control:
kosli archive control yourControlIdentifier \
--api-token yourAPIToken \
--org yourOrgName
`

func newArchiveControlCmd(out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "control CONTROL-IDENTIFIER",
Short: archiveControlShortDesc,
Long: archiveControlLongDesc,
Example: archiveControlExample,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{betaCLIAnnotation: ""},
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
url, err := url.JoinPath(global.Host, "api/v2/controls", global.Org, args[0], "archive")
if err != nil {
return err
}

reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: url,
DryRun: global.DryRun,
Token: global.ApiToken,
}
_, err = kosliClient.Do(reqParams)
if err == nil && !global.DryRun {
logger.Info("control %s was archived", args[0])
}
return err
},
}
addDryRunFlag(cmd)
return cmd
}
51 changes: 51 additions & 0 deletions cmd/kosli/archiveControl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"fmt"
"testing"

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

type ArchiveControlCommandTestSuite struct {
suite.Suite
defaultKosliArguments string
}

func (suite *ArchiveControlCommandTestSuite) SetupTest() {
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
CreateControl(global.Org, "archive-me", "Archive me", suite.T())
}

func (suite *ArchiveControlCommandTestSuite) TestArchiveControlCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when no identifier argument is provided",
cmd: "archive control" + suite.defaultKosliArguments,
golden: "Error: accepts 1 arg(s), received 0\n",
},
{
name: "archiving an existing control works",
cmd: "archive control archive-me" + suite.defaultKosliArguments,
golden: "control archive-me was archived\n",
},
{
wantError: true,
name: "archiving a non-existing control gives a clear error",
cmd: "archive control no-such-control" + suite.defaultKosliArguments,
goldenRegex: "^Error: Control 'no-such-control' does not exist in org",
},
}

runTestCmd(suite.T(), tests)
}

func TestArchiveControlCommandTestSuite(t *testing.T) {
suite.Run(t, new(ArchiveControlCommandTestSuite))
}
4 changes: 1 addition & 3 deletions cmd/kosli/attestDecision.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"net/url"
"os"

"github.com/kosli-dev/cli/internal/docgen"
"github.com/kosli-dev/cli/internal/requests"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -97,8 +96,7 @@ func newAttestDecisionCmd(out io.Writer) *cobra.Command {
Short: attestDecisionShortDesc,
Long: attestDecisionLongDesc,
Example: attestDecisionExample,
Hidden: true,
Annotations: map[string]string{docgen.DocHiddenAnnotation: "", betaCLIAnnotation: ""},
Annotations: map[string]string{betaCLIAnnotation: ""},
PreRunE: func(cmd *cobra.Command, args []string) error {
err := CustomMaximumNArgs(1, args)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func newCreateCmd(out io.Writer) *cobra.Command {
newCreateFlowCmd(out),
newCreatePolicyCmd(out),
newCreateAttestationTypeCmd(out),
newCreateControlCmd(out),
newCreateApiKeyCmd(out),
newCreateServiceAccountCmd(out),
)
Expand Down
94 changes: 94 additions & 0 deletions cmd/kosli/createControl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"io"
"net/http"
"net/url"

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

const createControlShortDesc = `Create a Kosli control.`

const createControlLongDesc = createControlShortDesc + `
^CONTROL-IDENTIFIER^ must start with a letter or number, and only contain letters, numbers, ^.^, ^-^, ^_^, and ^~^.
`

const createControlExample = `
# create a Kosli control:
kosli create control yourControlIdentifier \
--name "Your control name" \
--description "what this control checks" \
--api-token yourAPIToken \
--org yourOrgName
`

type createControlOptions struct {
payload ControlPayload
}

type ControlPayload struct {
Identifier string `json:"identifier"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
}

func newCreateControlCmd(out io.Writer) *cobra.Command {
o := new(createControlOptions)
cmd := &cobra.Command{
Use: "control CONTROL-IDENTIFIER",
Short: createControlShortDesc,
Long: createControlLongDesc,
Example: createControlExample,
Args: cobra.ExactArgs(1),
Annotations: map[string]string{betaCLIAnnotation: ""},
PreRunE: func(cmd *cobra.Command, args []string) error {
err := RequireGlobalFlags(global, []string{"Org", "ApiToken"})
if err != nil {
return ErrorBeforePrintingUsage(cmd, err.Error())
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return o.run(args)
},
}

cmd.Flags().StringVarP(&o.payload.Name, "name", "n", "", controlNameFlag)
cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", controlDescriptionFlag)

addDryRunFlag(cmd)

err := RequireFlags(cmd, []string{"name"})
if err != nil {
logger.Error("failed to configure required flags: %v", err)
}

return cmd
}

func (o *createControlOptions) run(args []string) error {
o.payload.Identifier = args[0]
url, err := url.JoinPath(global.Host, "api/v2/controls", global.Org)
if err != nil {
return err
}

reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: url,
Payload: o.payload,
DryRun: global.DryRun,
Token: global.ApiToken,
// A 409 here means the identifier already exists — a permanent error,
// so surface it immediately rather than retrying.
DisableConflictRetry: true,
}
_, err = kosliClient.Do(reqParams)
if err == nil && !global.DryRun {
logger.Info("control %s was created", o.payload.Identifier)
}
return err
}
71 changes: 71 additions & 0 deletions cmd/kosli/createControl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"fmt"
"testing"

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

type CreateControlTestSuite struct {
suite.Suite
defaultKosliArguments string
}

func (suite *CreateControlTestSuite) SetupTest() {
global = &GlobalOpts{
ApiToken: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6ImNkNzg4OTg5In0.e8i_lA_QrEhFncb05Xw6E_tkCHU9QfcY4OLTVUCHffY",
Org: "docs-cmd-test-user",
Host: "http://localhost:8001",
}
suite.defaultKosliArguments = fmt.Sprintf(" --host %s --org %s --api-token %s", global.Host, global.Org, global.ApiToken)
}

func (suite *CreateControlTestSuite) TestCreateControlCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when no identifier argument is provided",
cmd: "create control --name 'My Control'" + suite.defaultKosliArguments,
golden: "Error: accepts 1 arg(s), received 0\n",
},
{
wantError: true,
name: "fails when --name is missing",
cmd: "create control my-control" + suite.defaultKosliArguments,
golden: "Error: required flag(s) \"name\" not set\n",
},
{
wantError: false,
name: "creates a control with identifier and --name",
cmd: "create control my-control --name 'My Control'" + suite.defaultKosliArguments,
golden: "control my-control was created\n",
},
{
wantError: false,
name: "creates a control with a --description",
cmd: "create control my-control-2 --name 'My Second Control' --description 'checks something'" + suite.defaultKosliArguments,
golden: "control my-control-2 was created\n",
},
{
// Relies on "my-control" created by the earlier test case (cases run
// sequentially against the same server within the suite).
wantError: true,
name: "fails with a clear error when the identifier already exists",
cmd: "create control my-control --name 'My Control'" + suite.defaultKosliArguments,
goldenRegex: "^Error: A control with identifier 'my-control' already exists in organization",
},
{
wantError: true,
name: "fails when the identifier contains invalid characters",
cmd: "create control 'bad identifier' --name 'My Control'" + suite.defaultKosliArguments,
goldenRegex: "^Error: Input payload validation failed:.*Control identifier 'bad identifier' contains invalid characters",
},
}

runTestCmd(suite.T(), tests)
}

func TestCreateControlTestSuite(t *testing.T) {
suite.Run(t, new(CreateControlTestSuite))
}
1 change: 1 addition & 0 deletions cmd/kosli/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func newGetCmd(out io.Writer) *cobra.Command {
newGetTrailCmd(out),
newGetPolicyCmd(out),
newGetAttestationTypeCmd(out),
newGetControlCmd(out),
newGetAttestationCmd(out),
newGetRepoCmd(out),
newGetServiceAccountCmd(out),
Expand Down
Loading
Loading