Skip to content

Commit

Permalink
EC-237 Add validate input and use policy config
Browse files Browse the repository at this point in the history
This commit addresses EC-237 & EC-57 which marks `ec validate
definition` as deprecated, adds `ec validate input` and allows `ec
validate input` to utilize a policy config file in a similar manner as
the `ec validate image` command.
  • Loading branch information
robnester-rh committed Dec 12, 2023
1 parent e429725 commit e9b139b
Show file tree
Hide file tree
Showing 23 changed files with 1,398 additions and 98 deletions.
1 change: 1 addition & 0 deletions cmd/validate/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func validateDefinitionCmd(validate definitionValidationFn) *cobra.Command {
--data git::https://github.com/enterprise-contract/ec-policies//example/data
`),

Deprecated: "please use \"ec validate input\" instead.",
RunE: func(cmd *cobra.Command, args []string) error {
var allErrors error
report := definition.NewReport()
Expand Down
14 changes: 8 additions & 6 deletions cmd/validate/definition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"testing"

hd "github.com/MakeNowJust/heredoc"
Expand Down Expand Up @@ -73,7 +75,7 @@ func TestValidateDefinitionFileCommandOutput(t *testing.T) {
],
"success": true,
"ec-version": "development"
}`, out.String())
}`, strings.Split(out.String(), "\n")[1])
}

func TestValidateDefinitionFilePolicySources(t *testing.T) {
Expand Down Expand Up @@ -134,22 +136,22 @@ func TestDefinitionFileOutputFormats(t *testing.T) {
}{
{
name: "default output",
expectedStdout: testJSONText,
expectedStdout: fmt.Sprintf("Command \"definition\" is deprecated, please use \"ec validate input\" instead.\n%s", testJSONText),
},
{
name: "json stdout",
output: []string{"--output", "json"},
expectedStdout: testJSONText,
expectedStdout: fmt.Sprintf("Command \"definition\" is deprecated, please use \"ec validate input\" instead.\n%s", testJSONText),
},
{
name: "yaml stdout",
output: []string{"--output", "yaml"},
expectedStdout: testYAMLTest,
expectedStdout: fmt.Sprintf("Command \"definition\" is deprecated, please use \"ec validate input\" instead.\n%s", testYAMLTest),
},
{
name: "json and yaml to file",
output: []string{"--output", "json=out.json", "--output", "yaml=out.yaml"},
expectedStdout: "",
expectedStdout: "Command \"definition\" is deprecated, please use \"ec validate input\" instead.\n",
expectedFiles: map[string]string{
"out.json": testJSONText,
"out.yaml": testYAMLTest,
Expand Down Expand Up @@ -215,7 +217,7 @@ func TestValidateDefinitionFileCommandErrors(t *testing.T) {

err := cmd.Execute()
assert.Error(t, err, "2 errors occurred:\n\t* /path/file1.yaml\n\t* /path/file2.yaml\n")
assert.Equal(t, "", out.String())
assert.Equal(t, "Command \"definition\" is deprecated, please use \"ec validate input\" instead.\n", out.String())
}

func TestStrictOutput(t *testing.T) {
Expand Down
53 changes: 6 additions & 47 deletions cmd/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@ import (
"github.com/hashicorp/go-multierror"
app "github.com/redhat-appstudio/application-api/api/v1alpha1"
"github.com/sigstore/cosign/v2/pkg/cosign"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
"github.com/spf13/cobra"

"github.com/enterprise-contract/ec-cli/internal/applicationsnapshot"
"github.com/enterprise-contract/ec-cli/internal/evaluator"
"github.com/enterprise-contract/ec-cli/internal/format"
"github.com/enterprise-contract/ec-cli/internal/output"
"github.com/enterprise-contract/ec-cli/internal/policy"
"github.com/enterprise-contract/ec-cli/internal/policy/source"
"github.com/enterprise-contract/ec-cli/internal/utils"
validate_utils "github.com/enterprise-contract/ec-cli/internal/validate"
)

type imageValidationFunc func(context.Context, app.SnapshotComponent, policy.Policy, bool) (*output.Output, error)
Expand Down Expand Up @@ -190,50 +188,12 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
data.spec = s
}

// Check if policyConfiguration is a git url, if so, try to download a config file from git
if source.SourceIsGit(data.policyConfiguration) {
log.Debugf("Fetching policy config from git url %s", data.policyConfiguration)

// Create a temporary dir to download the config. It will be different to the
// workdir used later for downloading policy sources, but it won't matter
// because this dir is not used again once the config file has been read.
fs := utils.FS(ctx)
tmpDir, err := utils.CreateWorkDir(fs)
if err != nil {
allErrors = multierror.Append(allErrors, err)
return
}
defer utils.CleanupWorkDir(fs, tmpDir)

// Git download and find a suitable config file
configFile, err := source.GitConfigDownload(cmd.Context(), tmpDir, data.policyConfiguration)
if err != nil {
allErrors = multierror.Append(allErrors, err)
return
}

// Changing data.policyConfiguration to the name of the newly downloaded
// file means we can use the code below to load the config
data.policyConfiguration = configFile
}

// Check if policyConfiguration is a file path, if so, we read it into the var data.policyConfiguration
if utils.HasJsonOrYamlExt(data.policyConfiguration) {
fs := utils.FS(ctx)
policyBytes, err := afero.ReadFile(fs, data.policyConfiguration)
if err != nil {
allErrors = multierror.Append(allErrors, err)
return
}
// Check for empty file as that would cause a false "success"
if len(policyBytes) == 0 {
err := fmt.Errorf("file %s is empty", data.policyConfiguration)
allErrors = multierror.Append(allErrors, err)
return
}

data.policyConfiguration = string(policyBytes)
policyConfiguration, err := validate_utils.GetPolicyConfig(ctx, data.policyConfiguration)
if err != nil {
allErrors = multierror.Append(allErrors, err)
return
}
data.policyConfiguration = policyConfiguration

if p, err := policy.NewPolicy(cmd.Context(), policy.Options{
EffectiveTime: data.effectiveTime,
Expand Down Expand Up @@ -349,7 +309,6 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
}

if data.strict && !report.Success {
// TODO: replace this with proper message and exit code 1.
return errors.New("success criteria not met")
}

Expand Down
227 changes: 227 additions & 0 deletions cmd/validate/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Copyright The Enterprise Contract Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package validate

import (
"context"
"errors"
"fmt"
"sort"
"sync"

hd "github.com/MakeNowJust/heredoc"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"

"github.com/enterprise-contract/ec-cli/internal/evaluator"
"github.com/enterprise-contract/ec-cli/internal/format"
"github.com/enterprise-contract/ec-cli/internal/input"
"github.com/enterprise-contract/ec-cli/internal/output"
"github.com/enterprise-contract/ec-cli/internal/policy"
"github.com/enterprise-contract/ec-cli/internal/utils"
validate_utils "github.com/enterprise-contract/ec-cli/internal/validate"
)

type InputValidationFunc func(context.Context, string, policy.Policy, bool) (*output.Output, error)

func validateInputCmd(validate InputValidationFunc) *cobra.Command {
var data = struct {
effectiveTime string
filePaths []string
info bool
namespaces []string
output []string
policy policy.Policy
policyConfiguration string
strict bool
}{
strict: true,
}
cmd := &cobra.Command{
Use: "input",
Short: "Validate arbitrary JSON or yaml file input conformance with the Enterprise Contract",
Long: hd.Doc(`
Validate conformance of arbitrary JSON or yaml file input with the Enterprise Contract
For each file, validation is performed to determine if the file conforms to rego policies
defined in the the EnterpriseContractPolicy.
`),
Example: hd.Doc(`
Use an EnterpriseContractPolicy spec from a local YAML file to validate a single file
ec validate input --file /path/to/file.json --policy my-policy.yaml
Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can be repeated for multiple input files.
ec validate input --file /path/to/file.yaml --file /path/to/file2.yaml --policy my-policy.yaml
Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can take a comma separated series of files.
ec validate input --file="/path/to/file.json,/path/to/file2.json" --policy my-policy.yaml
Use a git url for the policy configuration. In the first example there should be a '.ec/policy.yaml'
or a 'policy.yaml' inside a directory called 'default' in the top level of the git repo. In the second
example there should be a '.ec/policy.yaml' or a 'policy.yaml' file in the top level
of the git repo. For git repos not hosted on 'github.com' or 'gitlab.com', prefix the url with
'git::'. For the policy configuration files you can use json instead of yaml if you prefer.
ec validate input --file /path/to/file.json --policy github.com/user/repo//default?ref=main
ec validate input --file /path/to/file.yaml --policy github.com/user/repo
`),
PreRunE: func(cmd *cobra.Command, args []string) (allErrors error) {
ctx := cmd.Context()

policyConfiguration, err := validate_utils.GetPolicyConfig(ctx, data.policyConfiguration)
if err != nil {
allErrors = multierror.Append(allErrors, err)
return
}
data.policyConfiguration = policyConfiguration

if p, err := policy.NewInputPolicy(cmd.Context(), data.policyConfiguration, data.effectiveTime); err != nil {
allErrors = multierror.Append(allErrors, err)
} else {
data.policy = p
}
return

Check warning on line 101 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L87-L101

Added lines #L87 - L101 were not covered by tests
},
RunE: func(cmd *cobra.Command, args []string) error {

type result struct {
err error
input input.Input
data []evaluator.Data
policyInput []byte
}

ch := make(chan result, len(data.filePaths))

var lock sync.WaitGroup

for _, f := range data.filePaths {
lock.Add(1)
go func(fpath string) {
defer lock.Done()

ctx := cmd.Context()
out, err := validate(ctx, fpath, data.policy, data.info)
res := result{
err: err,
input: input.Input{
FilePath: fpath,
Success: err == nil,
},
}
// Skip on err to not panic. Error is return on routine completion.
if err == nil {
res.input.Violations = out.Violations()
showSuccesses, _ := cmd.Flags().GetBool("show-successes")
res.input.Warnings = out.Warnings()

successes := out.Successes()
res.input.SuccessCount = len(successes)
if showSuccesses {
res.input.Successes = successes
}
res.data = out.Data

Check warning on line 141 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L103-L141

Added lines #L103 - L141 were not covered by tests
}
res.input.Success = err == nil && len(res.input.Violations) == 0
ch <- res

Check warning on line 144 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L143-L144

Added lines #L143 - L144 were not covered by tests
}(f)
}

lock.Wait()
close(ch)

var inputs []input.Input
var manyData [][]evaluator.Data
var manyPolicyInput [][]byte
var allErrors error = nil

for r := range ch {
if r.err != nil {
e := fmt.Errorf("error validating file %s: %w", r.input.FilePath, r.err)
allErrors = multierror.Append(allErrors, e)
} else {
inputs = append(inputs, r.input)
manyData = append(manyData, r.data)
manyPolicyInput = append(manyPolicyInput, r.policyInput)
}

Check warning on line 164 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L148-L164

Added lines #L148 - L164 were not covered by tests
}
if allErrors != nil {
return allErrors
}

Check warning on line 168 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L166-L168

Added lines #L166 - L168 were not covered by tests

// Ensure some consistency in output.
sort.Slice(inputs, func(i, j int) bool {
return inputs[i].FilePath > inputs[j].FilePath
})

Check warning on line 173 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L171-L173

Added lines #L171 - L173 were not covered by tests

report, err := input.NewReport(inputs, data.policy, manyData, manyPolicyInput)
if err != nil {
return err
}

Check warning on line 178 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L175-L178

Added lines #L175 - L178 were not covered by tests

p := format.NewTargetParser(input.JSON, cmd.OutOrStdout(), utils.FS(cmd.Context()))
if err := report.WriteAll(data.output, p); err != nil {
return err
}

Check warning on line 183 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L180-L183

Added lines #L180 - L183 were not covered by tests

if data.strict && !report.Success {
return errors.New("success criteria not met")
}

Check warning on line 187 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L185-L187

Added lines #L185 - L187 were not covered by tests

return nil

Check warning on line 189 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L189

Added line #L189 was not covered by tests
},
}

cmd.Flags().StringSliceVarP(&data.filePaths, "file", "f", data.filePaths, "path to input YAML/JSON file (required)")

cmd.Flags().StringVarP(&data.policyConfiguration, "policy", "p", data.policyConfiguration, hd.Doc(`
Policy configuration as:
* file (policy.yaml)
* git reference (github.com/user/repo//default?ref=main), or
* inline JSON ('{sources: {...}, configuration: {...}}')")`))

cmd.Flags().StringSliceVarP(&data.output, "output", "o", data.output, hd.Doc(`
Write output to a file in a specific format, e.g. yaml=/tmp/output.yaml. Use empty string
path for stdout, e.g. yaml. May be used multiple times. Possible formats are json and yaml`))

cmd.Flags().BoolVarP(&data.strict, "strict", "s", data.strict,
"Return non-zero status on non-successful validation")

cmd.Flags().StringVar(&data.effectiveTime, "effective-time", policy.Now, hd.Doc(`
Run policy checks with the provided time. Useful for testing rules with
effective dates in the future. The value can be "now" (default) - for
current time, or a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z.`))

cmd.Flags().BoolVar(&data.info, "info", data.info, hd.Doc(`
Include additional information on the failures. For instance for policy
violations, include the title and the description of the failed policy
rule.`))

if err := cmd.MarkFlagRequired("file"); err != nil {
panic(err)

Check warning on line 219 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L219

Added line #L219 was not covered by tests
}

if err := cmd.MarkFlagRequired("policy"); err != nil {
panic(err)

Check warning on line 223 in cmd/validate/input.go

View check run for this annotation

Codecov / codecov/patch

cmd/validate/input.go#L223

Added line #L223 was not covered by tests
}

return cmd
}
19 changes: 19 additions & 0 deletions cmd/validate/input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright The Enterprise Contract Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//go:build unit

package validate
Loading

0 comments on commit e9b139b

Please sign in to comment.