-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
EC-237 Add validate input and use policy config
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
1 parent
e429725
commit 6306bc1
Showing
10 changed files
with
1,361 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
// 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" | ||
log "github.com/sirupsen/logrus" | ||
"github.com/spf13/afero" | ||
|
||
"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/policy/source" | ||
"github.com/enterprise-contract/ec-cli/internal/utils" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
type InputValidationFunc func(context.Context, string, policy.Policy, []string, string, bool) (*output.Output, error) | ||
|
||
func validateInputCmd(validate InputValidationFunc) *cobra.Command { | ||
var data = struct { | ||
effectiveTime string | ||
filePaths []string | ||
info bool | ||
namespaces []string | ||
output []string | ||
outputFile string | ||
policy policy.Policy | ||
policyConfiguration string | ||
strict bool | ||
}{ | ||
strict: true, | ||
info: false, | ||
} | ||
cmd := &cobra.Command{ | ||
Use: "input", | ||
Short: "Validate arbitrary JSON or yaml file input conformance with the Enterprise Contract", | ||
Long: "", | ||
Example: "", | ||
PreRunE: func(cmd *cobra.Command, args []string) (allErrors error) { | ||
ctx := cmd.Context() | ||
|
||
// Check if policyConfiguration is a git url. If so, try to download a config file from git. | ||
// If successful we write that into the data.policyConfiguration var. | ||
if source.SourceIsGit(data.policyConfiguration) { | ||
log.Debugf("Fetching policy config from git url %s", data.policyConfiguration) | ||
|
||
// Create a temporary dir to download the config. This is separate from the workDir usd | ||
// later for downloading policy sources, but it doesn'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, try to read it. If successful we write | ||
// that into the data.policyConfiguration var. | ||
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 | ||
} | ||
log.Debugf("Loaded %s as policyConfiguration", data.policyConfiguration) | ||
data.policyConfiguration = string(policyBytes) | ||
} | ||
|
||
if p, err := policy.NewInputPolicy(cmd.Context(), data.policyConfiguration, data.effectiveTime); err != nil { | ||
allErrors = multierror.Append(allErrors, err) | ||
} else { | ||
data.policy = p | ||
} | ||
return | ||
}, | ||
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.namespaces, data.effectiveTime, false) | ||
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 | ||
} | ||
res.input.Success = err == nil && len(res.input.Violations) == 0 | ||
ch <- res | ||
}(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) | ||
} | ||
} | ||
if allErrors != nil { | ||
return allErrors | ||
} | ||
|
||
// Ensure some consistency in output. | ||
sort.Slice(inputs, func(i, j int) bool { | ||
return inputs[i].FilePath > inputs[j].FilePath | ||
}) | ||
|
||
if len(data.outputFile) > 0 { | ||
data.output = append(data.output, fmt.Sprintf("%s=%s", input.JSON, data.outputFile)) | ||
} | ||
|
||
report, err := input.NewReport(inputs, data.policy, manyData, manyPolicyInput) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
p := format.NewTargetParser(input.JSON, cmd.OutOrStdout(), utils.FS(cmd.Context())) | ||
if err := report.WriteAll(data.output, p); err != nil { | ||
return err | ||
} | ||
|
||
if data.strict && !report.Success { | ||
// TODO: replace this with proper message and exit code 1. | ||
return errors.New("success criteria not met") | ||
} | ||
|
||
return nil | ||
}, | ||
} | ||
|
||
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: | ||
* Kubernetes reference ([<namespace>/]<name>) | ||
* 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, "attestation" - for time from the youngest attestation, 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.`)) | ||
|
||
cmd.Flags().StringSliceVar(&data.namespaces, "namespace", data.namespaces, | ||
"the namespace containing the policy to run. May be used multiple times") | ||
|
||
cmd.Flags().StringVar(&data.outputFile, "output-file", "", 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`)) | ||
|
||
if err := cmd.MarkFlagRequired("file"); err != nil { | ||
panic(err) | ||
} | ||
|
||
if err := cmd.MarkFlagRequired("policy"); err != nil { | ||
panic(err) | ||
} | ||
|
||
return cmd | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.