Skip to content

Commit

Permalink
Merge branch 'main' into add-what-do-i-do-if-kosli-is-down-faq
Browse files Browse the repository at this point in the history
  • Loading branch information
JonJagger committed Dec 9, 2024
2 parents 38f6e86 + af4d109 commit e3baf42
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 2 deletions.
1 change: 1 addition & 0 deletions cmd/kosli/attestGeneric.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func newAttestGenericCmd(out io.Writer) *cobra.Command {
return ValidateRegistryFlags(cmd, o.fingerprintOptions)

},

RunE: func(cmd *cobra.Command, args []string) error {
return o.run(args)
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/kosli/beginTrail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (suite *BeginTrailCommandTestSuite) TestBeginTrailCmd() {
wantError: true,
name: "beginning a trail with an invalid template fails",
cmd: fmt.Sprintf("begin trail test-123 --flow %s --template-file testdata/invalid_template.yml %s", suite.flowName, suite.defaultKosliArguments),
goldenRegex: "Error: template file is invalid. 1 validation error for Template\n.*",
goldenRegex: "Error: 1 validation error for Template\n.*",
},
{
name: "can begin a trail with a valid template",
Expand Down
1 change: 1 addition & 0 deletions cmd/kosli/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func newCreateCmd(out io.Writer) *cobra.Command {
newCreateEnvironmentCmd(out),
newCreateFlowCmd(out),
newCreatePolicyCmd(out),
newCreateAttestationTypeCmd(out),
)
return cmd
}
129 changes: 129 additions & 0 deletions cmd/kosli/createAttestationType.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package main

import (
"fmt"
"io"
"net/http"

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

const createAttestationTypeShortDesc = `Create or update a Kosli attestation type.`

const createAttestationTypeLongDesc = createAttestationTypeShortDesc + `
^TYPE-NAME^ must start with a letter or number, and only contain letters, numbers, ^.^, ^-^, ^_^, and ^~^.
^--schema^ is a path to a file containing a JSON schema which will be used to validate attestations made using this type.
^--jq^ defines the evaluation rules for this attestation type. This can be repeated in order to add additional rules. All rules must return ^true^ for the evaluation to pass.
`

const createAttestationTypeExample = `
kosli create attestation type person-of-age \
--description "Attest that a person meets the age requirements." \
--schema person-schema.json \
--jq ".age >= 18"
--jq ".age < 65"
`

type createAttestationTypeOptions struct {
payload CreateAttestationTypePayload
schemaFilePath string
jqRules []string
}

type JQEvaluatorPayload struct {
ContentType string `json:"content_type"`
Rules []string `json:"rules"`
}

func NewJQEvaluatorPayload(rules []string) *JQEvaluatorPayload {
return &JQEvaluatorPayload{"jq", rules}
}

type CreateAttestationTypePayload struct {
TypeName string `json:"name"`
Description string `json:"description,omitempty"`
Evaluator *JQEvaluatorPayload `json:"evaluator,omitempty"`
}

func newCreateAttestationTypeCmd(out io.Writer) *cobra.Command {
o := new(createAttestationTypeOptions)
cmd := &cobra.Command{
Use: "attestation-type TYPE-NAME",
Short: createAttestationTypeShortDesc,
Long: createAttestationTypeLongDesc,
Example: createAttestationTypeExample,
Args: cobra.ExactArgs(1),
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)
},
Hidden: true,
}

cmd.Flags().StringVarP(&o.payload.Description, "description", "d", "", attestationTypeDescriptionFlag)
cmd.Flags().StringVarP(&o.schemaFilePath, "schema", "s", "", attestationTypeSchemaFlag)
cmd.Flags().StringArrayVar(&o.jqRules, "jq", []string{}, attestationTypeJqFlag)

addDryRunFlag(cmd)
return cmd
}

func (o *createAttestationTypeOptions) run(args []string) error {
o.payload.TypeName = args[0]
if len(o.jqRules) > 0 {
o.payload.Evaluator = NewJQEvaluatorPayload(o.jqRules)
}

form, err := prepareAttestationTypeForm(o.payload, o.schemaFilePath)
if err != nil {
return err
}

url := fmt.Sprintf("%s/api/v2/custom-attestation-types/%s", global.Host, global.Org)
reqParams := &requests.RequestParams{
Method: http.MethodPost,
URL: url,
Form: form,
DryRun: global.DryRun,
Token: global.ApiToken,
}
_, err = kosliClient.Do(reqParams)
if err == nil && !global.DryRun {
logger.Info("attestation-type %s was created", o.payload.TypeName)
}
return err
}

func prepareAttestationTypeForm(payload interface{}, schemaFilePath string) ([]requests.FormItem, error) {
form, err := newAttestationTypeForm(payload, schemaFilePath)
if err != nil {
return []requests.FormItem{}, err
}
return form, nil
}

// newAttestationTypeForm constructs a list of FormItems for an attestation-type
// form submission.
func newAttestationTypeForm(payload interface{}, schemaFilePath string) (
[]requests.FormItem, error,
) {
form := []requests.FormItem{
{Type: "field", FieldName: "data_json", Content: payload},
}

if schemaFilePath != "" {
form = append(form, requests.FormItem{Type: "file", FieldName: "type_schema", Content: schemaFilePath})
}

return form, nil
}
69 changes: 69 additions & 0 deletions cmd/kosli/createAttestationType_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"fmt"
"testing"

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

// Define the suite, and absorb the built-in basic suite
// functionality from testify - including a T() method which
// returns the current testing context
type CreateAttestationTypeTestSuite struct {
suite.Suite
defaultKosliArguments string
}

func (suite *CreateAttestationTypeTestSuite) 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 *CreateAttestationTypeTestSuite) TestCustomAttestationTypeCmd() {
tests := []cmdTestCase{
{
wantError: true,
name: "fails when no arguments are provided",
cmd: "create attestation-type" + suite.defaultKosliArguments,
golden: "Error: accepts 1 arg(s), received 0\n",
},
{
name: "type name is provided",
cmd: "create attestation-type wibble" + suite.defaultKosliArguments,
golden: "attestation-type wibble was created\n",
},
{
name: "type description is provided",
cmd: "create attestation-type wibble-2 --description 'description of attestation type'" + suite.defaultKosliArguments,
golden: "attestation-type wibble-2 was created\n",
},
{
name: "type schema is provided",
cmd: "create attestation-type wibble-4 --schema testdata/person-schema.json" + suite.defaultKosliArguments,
golden: "attestation-type wibble-4 was created\n",
},
{
name: "type jq evaluator is provided",
cmd: `create attestation-type wibble-5 --jq '.age > 21' --jq '.age < 50'` + suite.defaultKosliArguments,
golden: "attestation-type wibble-5 was created\n",
},
{
name: `jq evaluators can include bare "`,
cmd: `create attestation-type wibble-6 --jq '.name | startswith("B")'` + suite.defaultKosliArguments,
golden: "attestation-type wibble-6 was created\n",
},
}

runTestCmd(suite.T(), tests)
}

// In order for 'go test' to run this suite, we need to create
// a normal test function and pass our suite to suite.Run
func TestCreateAttestationTypeTestSuite(t *testing.T) {
suite.Run(t, new(CreateAttestationTypeTestSuite))
}
2 changes: 1 addition & 1 deletion cmd/kosli/createFlow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (suite *CreateFlowCommandTestSuite) TestCreateFlowCmd() {
wantError: true,
name: "creating a flow with an invalid template fails",
cmd: "create flow newFlowWithTemplate --template-file testdata/invalid_template.yml --description \"my new flow\" " + suite.defaultKosliArguments,
goldenRegex: "Error: template file is invalid. 1 validation error for Template\n.*",
goldenRegex: "Error: Input payload validation failed.*",
},
{
wantError: true,
Expand Down
3 changes: 3 additions & 0 deletions cmd/kosli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ The ^.kosli_ignore^ will be treated as part of the artifact like any other file,
sonarRevisionFlag = "[conditional] The revision of the SonarCloud/SonarQube project. Only required if you want to use the project key/revision to get the scan results rather than using Sonar's metadata file and you have overridden the default revision, or you aren't using a CI. Defaults to the value of the git commit flag."
logicalEnvFlag = "[required] The logical environment."
physicalEnvFlag = "[required] The physical environment."
attestationTypeDescriptionFlag = "[optional] The attestation type description."
attestationTypeSchemaFlag = "[optional] Path to the attestation type schema in JSON Schema format."
attestationTypeJqFlag = "[optional] The attestation type evaluation JQ rules."
)

var global *GlobalOpts
Expand Down
8 changes: 8 additions & 0 deletions cmd/kosli/testdata/person-schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "object",
"additionalProperties": true,
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
}
}

0 comments on commit e3baf42

Please sign in to comment.