From 27e871a628d5da1f7866d282ac8a74ddd4b94a47 Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Thu, 25 Jul 2024 10:33:24 +0200 Subject: [PATCH 1/4] `--logfile` to log to a file This is useful so we can separate in contexts where stderr/stdout is not as clear, e.g. when running as a Tekton Task. Reference: EC-564 --- cmd/root/root_cmd.go | 9 ++++++++- docs/modules/ROOT/pages/ec.adoc | 1 + docs/modules/ROOT/pages/ec_fetch.adoc | 1 + docs/modules/ROOT/pages/ec_fetch_policy.adoc | 1 + docs/modules/ROOT/pages/ec_init.adoc | 1 + docs/modules/ROOT/pages/ec_init_policies.adoc | 1 + docs/modules/ROOT/pages/ec_inspect.adoc | 1 + docs/modules/ROOT/pages/ec_inspect_policy-data.adoc | 1 + docs/modules/ROOT/pages/ec_inspect_policy.adoc | 1 + docs/modules/ROOT/pages/ec_opa.adoc | 1 + docs/modules/ROOT/pages/ec_opa_bench.adoc | 1 + docs/modules/ROOT/pages/ec_opa_build.adoc | 1 + docs/modules/ROOT/pages/ec_opa_capabilities.adoc | 1 + docs/modules/ROOT/pages/ec_opa_check.adoc | 1 + docs/modules/ROOT/pages/ec_opa_deps.adoc | 1 + docs/modules/ROOT/pages/ec_opa_eval.adoc | 1 + docs/modules/ROOT/pages/ec_opa_exec.adoc | 1 + docs/modules/ROOT/pages/ec_opa_fmt.adoc | 1 + docs/modules/ROOT/pages/ec_opa_inspect.adoc | 1 + docs/modules/ROOT/pages/ec_opa_parse.adoc | 1 + docs/modules/ROOT/pages/ec_opa_run.adoc | 1 + docs/modules/ROOT/pages/ec_opa_sign.adoc | 1 + docs/modules/ROOT/pages/ec_opa_test.adoc | 1 + docs/modules/ROOT/pages/ec_opa_version.adoc | 1 + docs/modules/ROOT/pages/ec_sigstore.adoc | 1 + docs/modules/ROOT/pages/ec_sigstore_initialize.adoc | 1 + docs/modules/ROOT/pages/ec_test.adoc | 1 + docs/modules/ROOT/pages/ec_track.adoc | 1 + docs/modules/ROOT/pages/ec_track_bundle.adoc | 1 + docs/modules/ROOT/pages/ec_validate.adoc | 1 + docs/modules/ROOT/pages/ec_validate_image.adoc | 1 + docs/modules/ROOT/pages/ec_validate_input.adoc | 1 + docs/modules/ROOT/pages/ec_validate_policy.adoc | 1 + docs/modules/ROOT/pages/ec_version.adoc | 1 + features/__snapshots__/opa.snap | 1 + internal/logging/logging.go | 11 ++++++++++- 36 files changed, 52 insertions(+), 2 deletions(-) diff --git a/cmd/root/root_cmd.go b/cmd/root/root_cmd.go index 76d2eff43..61dd772b4 100644 --- a/cmd/root/root_cmd.go +++ b/cmd/root/root_cmd.go @@ -18,9 +18,11 @@ package root import ( "context" + "io" "time" hd "github.com/MakeNowJust/heredoc" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/enterprise-contract/ec-cli/internal/kubernetes" @@ -35,6 +37,7 @@ var ( debug bool = false trace bool = false globalTimeout = 5 * time.Minute + logfile string ) func NewRootCmd() *cobra.Command { @@ -51,7 +54,7 @@ func NewRootCmd() *cobra.Command { SilenceUsage: true, PersistentPreRun: func(cmd *cobra.Command, _ []string) { - logging.InitLogging(verbose, quiet, debug, trace) + logging.InitLogging(verbose, quiet, debug, trace, logfile) // Create a new context now that flags have been parsed so a custom timeout can be used. ctx := cmd.Context() @@ -60,6 +63,9 @@ func NewRootCmd() *cobra.Command { }, PersistentPostRun: func(cmd *cobra.Command, _ []string) { + if f, ok := logrus.StandardLogger().Out.(io.Closer); ok { + f.Close() + } if cancel != nil { cancel() } @@ -77,5 +83,6 @@ func setFlags(rootCmd *cobra.Command) { rootCmd.PersistentFlags().BoolVar(&debug, "debug", debug, "same as verbose but also show function names and line numbers") rootCmd.PersistentFlags().BoolVar(&trace, "trace", trace, "enable trace logging") rootCmd.PersistentFlags().DurationVar(&globalTimeout, "timeout", globalTimeout, "max overall execution duration") + rootCmd.PersistentFlags().StringVar(&logfile, "logfile", "", "file to write the logging output. If not specified logging output will be written to stderr") kubernetes.AddKubeconfigFlag(rootCmd) } diff --git a/docs/modules/ROOT/pages/ec.adoc b/docs/modules/ROOT/pages/ec.adoc index a6d166ea1..f24448899 100644 --- a/docs/modules/ROOT/pages/ec.adoc +++ b/docs/modules/ROOT/pages/ec.adoc @@ -15,6 +15,7 @@ ec [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) -h, --help:: help for ec (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_fetch.adoc b/docs/modules/ROOT/pages/ec_fetch.adoc index 7db76b11b..974f67971 100644 --- a/docs/modules/ROOT/pages/ec_fetch.adoc +++ b/docs/modules/ROOT/pages/ec_fetch.adoc @@ -9,6 +9,7 @@ Fetch remote resources --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_fetch_policy.adoc b/docs/modules/ROOT/pages/ec_fetch_policy.adoc index 569d5bb94..d501de798 100644 --- a/docs/modules/ROOT/pages/ec_fetch_policy.adoc +++ b/docs/modules/ROOT/pages/ec_fetch_policy.adoc @@ -71,6 +71,7 @@ Notes: --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_init.adoc b/docs/modules/ROOT/pages/ec_init.adoc index 8d421c624..487915d43 100644 --- a/docs/modules/ROOT/pages/ec_init.adoc +++ b/docs/modules/ROOT/pages/ec_init.adoc @@ -9,6 +9,7 @@ Initialize a directory for use --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_init_policies.adoc b/docs/modules/ROOT/pages/ec_init_policies.adoc index 380f073cb..b122cf154 100644 --- a/docs/modules/ROOT/pages/ec_init_policies.adoc +++ b/docs/modules/ROOT/pages/ec_init_policies.adoc @@ -27,6 +27,7 @@ Initialize the "my-policy" directory with minimal EC policy scaffolding: --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_inspect.adoc b/docs/modules/ROOT/pages/ec_inspect.adoc index 80c3e0bb6..ae67f20f9 100644 --- a/docs/modules/ROOT/pages/ec_inspect.adoc +++ b/docs/modules/ROOT/pages/ec_inspect.adoc @@ -9,6 +9,7 @@ Inspect policy rules --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_inspect_policy-data.adoc b/docs/modules/ROOT/pages/ec_inspect_policy-data.adoc index 1556ed8ca..7b7233309 100644 --- a/docs/modules/ROOT/pages/ec_inspect_policy-data.adoc +++ b/docs/modules/ROOT/pages/ec_inspect_policy-data.adoc @@ -32,6 +32,7 @@ ec inspect policy-data --source git::https://github.com/enterprise-contract/ec-p --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_inspect_policy.adoc b/docs/modules/ROOT/pages/ec_inspect_policy.adoc index f3192a8e5..d50dd8c32 100644 --- a/docs/modules/ROOT/pages/ec_inspect_policy.adoc +++ b/docs/modules/ROOT/pages/ec_inspect_policy.adoc @@ -44,6 +44,7 @@ Display details about the latest Enterprise Contract release policy in json form --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa.adoc b/docs/modules/ROOT/pages/ec_opa.adoc index fce067d42..506af70c4 100644 --- a/docs/modules/ROOT/pages/ec_opa.adoc +++ b/docs/modules/ROOT/pages/ec_opa.adoc @@ -15,6 +15,7 @@ ec opa [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_bench.adoc b/docs/modules/ROOT/pages/ec_opa_bench.adoc index 5e23ba1a0..5ce09f19a 100644 --- a/docs/modules/ROOT/pages/ec_opa_bench.adoc +++ b/docs/modules/ROOT/pages/ec_opa_bench.adoc @@ -51,6 +51,7 @@ ec opa bench [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_build.adoc b/docs/modules/ROOT/pages/ec_opa_build.adoc index fa7a74674..7917964f0 100644 --- a/docs/modules/ROOT/pages/ec_opa_build.adoc +++ b/docs/modules/ROOT/pages/ec_opa_build.adoc @@ -188,6 +188,7 @@ ec opa build [ [...]] [flags] == Options inherited from parent commands --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_capabilities.adoc b/docs/modules/ROOT/pages/ec_opa_capabilities.adoc index c4277ca80..bf22c7cd0 100644 --- a/docs/modules/ROOT/pages/ec_opa_capabilities.adoc +++ b/docs/modules/ROOT/pages/ec_opa_capabilities.adoc @@ -60,6 +60,7 @@ ec opa capabilities [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_check.adoc b/docs/modules/ROOT/pages/ec_opa_check.adoc index 0bfbe360f..f346276d0 100644 --- a/docs/modules/ROOT/pages/ec_opa_check.adoc +++ b/docs/modules/ROOT/pages/ec_opa_check.adoc @@ -28,6 +28,7 @@ ec opa check [path [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_deps.adoc b/docs/modules/ROOT/pages/ec_opa_deps.adoc index 40574ca2c..2a24d31eb 100644 --- a/docs/modules/ROOT/pages/ec_opa_deps.adoc +++ b/docs/modules/ROOT/pages/ec_opa_deps.adoc @@ -51,6 +51,7 @@ ec opa deps [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_eval.adoc b/docs/modules/ROOT/pages/ec_opa_eval.adoc index 10eab18fa..e385aeec4 100644 --- a/docs/modules/ROOT/pages/ec_opa_eval.adoc +++ b/docs/modules/ROOT/pages/ec_opa_eval.adoc @@ -154,6 +154,7 @@ ec opa eval [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --trace:: enable trace logging (Default: false) --verbose:: more verbose output (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_exec.adoc b/docs/modules/ROOT/pages/ec_opa_exec.adoc index 08145ee72..36e47de2c 100644 --- a/docs/modules/ROOT/pages/ec_opa_exec.adoc +++ b/docs/modules/ROOT/pages/ec_opa_exec.adoc @@ -50,6 +50,7 @@ ec opa exec [ [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --trace:: enable trace logging (Default: false) --verbose:: more verbose output (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_fmt.adoc b/docs/modules/ROOT/pages/ec_opa_fmt.adoc index 98e24203d..6c36f0452 100644 --- a/docs/modules/ROOT/pages/ec_opa_fmt.adoc +++ b/docs/modules/ROOT/pages/ec_opa_fmt.adoc @@ -39,6 +39,7 @@ ec opa fmt [path [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_inspect.adoc b/docs/modules/ROOT/pages/ec_opa_inspect.adoc index cd82c12ee..00a2321ce 100644 --- a/docs/modules/ROOT/pages/ec_opa_inspect.adoc +++ b/docs/modules/ROOT/pages/ec_opa_inspect.adoc @@ -39,6 +39,7 @@ ec opa inspect [ [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_parse.adoc b/docs/modules/ROOT/pages/ec_opa_parse.adoc index d824c642b..6b465764b 100644 --- a/docs/modules/ROOT/pages/ec_opa_parse.adoc +++ b/docs/modules/ROOT/pages/ec_opa_parse.adoc @@ -18,6 +18,7 @@ ec opa parse [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_run.adoc b/docs/modules/ROOT/pages/ec_opa_run.adoc index 60969972e..a904a8717 100644 --- a/docs/modules/ROOT/pages/ec_opa_run.adoc +++ b/docs/modules/ROOT/pages/ec_opa_run.adoc @@ -178,6 +178,7 @@ ec opa run [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_sign.adoc b/docs/modules/ROOT/pages/ec_opa_sign.adoc index 2ee003427..b6cdb820f 100644 --- a/docs/modules/ROOT/pages/ec_opa_sign.adoc +++ b/docs/modules/ROOT/pages/ec_opa_sign.adoc @@ -102,6 +102,7 @@ ec opa sign [ [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_test.adoc b/docs/modules/ROOT/pages/ec_opa_test.adoc index 7a35ba94f..4137fb373 100644 --- a/docs/modules/ROOT/pages/ec_opa_test.adoc +++ b/docs/modules/ROOT/pages/ec_opa_test.adoc @@ -107,6 +107,7 @@ ec opa test [path [...]] [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_opa_version.adoc b/docs/modules/ROOT/pages/ec_opa_version.adoc index 916da1e33..3536454d5 100644 --- a/docs/modules/ROOT/pages/ec_opa_version.adoc +++ b/docs/modules/ROOT/pages/ec_opa_version.adoc @@ -16,6 +16,7 @@ ec opa version [flags] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_sigstore.adoc b/docs/modules/ROOT/pages/ec_sigstore.adoc index c8c91f8ca..1479f60cf 100644 --- a/docs/modules/ROOT/pages/ec_sigstore.adoc +++ b/docs/modules/ROOT/pages/ec_sigstore.adoc @@ -9,6 +9,7 @@ Perform certain sigstore operations --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc b/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc index 860686096..582cb1aae 100644 --- a/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc +++ b/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc @@ -45,6 +45,7 @@ ec initialize -mirror -root --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_test.adoc b/docs/modules/ROOT/pages/ec_test.adoc index 7c6982d43..78c8c79a1 100644 --- a/docs/modules/ROOT/pages/ec_test.adoc +++ b/docs/modules/ROOT/pages/ec_test.adoc @@ -97,6 +97,7 @@ the output will include a detailed trace of how the policy was evaluated, e.g. --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --timeout:: max overall execution duration (Default: 5m0s) --verbose:: more verbose output (Default: false) diff --git a/docs/modules/ROOT/pages/ec_track.adoc b/docs/modules/ROOT/pages/ec_track.adoc index 2385f33de..c724e8d02 100644 --- a/docs/modules/ROOT/pages/ec_track.adoc +++ b/docs/modules/ROOT/pages/ec_track.adoc @@ -9,6 +9,7 @@ Record resource references for tracking purposes --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_track_bundle.adoc b/docs/modules/ROOT/pages/ec_track_bundle.adoc index d01c8b087..ca345a763 100644 --- a/docs/modules/ROOT/pages/ec_track_bundle.adoc +++ b/docs/modules/ROOT/pages/ec_track_bundle.adoc @@ -72,6 +72,7 @@ Update existing acceptable bundles: --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_validate.adoc b/docs/modules/ROOT/pages/ec_validate.adoc index 521027c04..1b5105744 100644 --- a/docs/modules/ROOT/pages/ec_validate.adoc +++ b/docs/modules/ROOT/pages/ec_validate.adoc @@ -10,6 +10,7 @@ Validate conformance with the Enterprise Contract --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index ee89124a6..acadfc0df 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -153,6 +153,7 @@ JSON of the "spec" or a reference to a Kubernetes object [/] --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --show-successes:: (Default: false) --timeout:: max overall execution duration (Default: 5m0s) diff --git a/docs/modules/ROOT/pages/ec_validate_input.adoc b/docs/modules/ROOT/pages/ec_validate_input.adoc index c09f0409c..8e0a8683a 100644 --- a/docs/modules/ROOT/pages/ec_validate_input.adoc +++ b/docs/modules/ROOT/pages/ec_validate_input.adoc @@ -58,6 +58,7 @@ and summary (Default: []) --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --show-successes:: (Default: false) --timeout:: max overall execution duration (Default: 5m0s) diff --git a/docs/modules/ROOT/pages/ec_validate_policy.adoc b/docs/modules/ROOT/pages/ec_validate_policy.adoc index 151000971..2caf696d2 100644 --- a/docs/modules/ROOT/pages/ec_validate_policy.adoc +++ b/docs/modules/ROOT/pages/ec_validate_policy.adoc @@ -28,6 +28,7 @@ ec validate policy --policy-configuration github.com/org/repo/policy.yaml --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --show-successes:: (Default: false) --timeout:: max overall execution duration (Default: 5m0s) diff --git a/docs/modules/ROOT/pages/ec_version.adoc b/docs/modules/ROOT/pages/ec_version.adoc index 2b5116f1b..c9d72e7df 100644 --- a/docs/modules/ROOT/pages/ec_version.adoc +++ b/docs/modules/ROOT/pages/ec_version.adoc @@ -11,6 +11,7 @@ Print version information --debug:: same as verbose but also show function names and line numbers (Default: false) --kubeconfig:: path to the Kubernetes config file to use +--logfile:: file to write the logging output. If not specified logging output will be written to stderr --quiet:: less verbose output (Default: false) --timeout:: max overall execution duration (Default: 5m0s) --trace:: enable trace logging (Default: false) diff --git a/features/__snapshots__/opa.snap b/features/__snapshots__/opa.snap index 27c83d40c..9ac224237 100755 --- a/features/__snapshots__/opa.snap +++ b/features/__snapshots__/opa.snap @@ -27,6 +27,7 @@ Flags: Global Flags: --debug same as verbose but also show function names and line numbers --kubeconfig string path to the Kubernetes config file to use + --logfile string file to write the logging output. If not specified logging output will be written to stderr --quiet less verbose output --timeout duration max overall execution duration (default 5m0s) --trace enable trace logging diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 830ca08c1..37ccad962 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -19,6 +19,7 @@ package logging import ( "flag" "fmt" + "os" "path/filepath" "runtime" @@ -42,7 +43,7 @@ import ( // We're expecting only one of the bool params to be set, but if // there are multiple set we'll accept it and the more verbose // option will take precedence. -func InitLogging(verbose, quiet, debug, trace bool) { +func InitLogging(verbose, quiet, debug, trace bool, logfile string) { var level log.Level var v string switch { @@ -80,6 +81,14 @@ func InitLogging(verbose, quiet, debug, trace bool) { if err := flags.Set("v", v); err != nil { panic(err) } + + if logfile != "" { + if l, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600); err == nil { + log.SetOutput(l) + } else { + fmt.Fprintf(os.Stderr, "Unable to create log file %q, log lines will appear on standard error. Error was: %s\n", logfile, err.Error()) + } + } } func setupDebugMode() { From 6cb4b92762e535ee4c3c06bb3feae31fae4d572e Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Wed, 24 Jul 2024 12:30:41 +0200 Subject: [PATCH 2/4] Refactor show successes flag Makes showing of successes flag a field of the Report structure. Reference: EC-564 --- cmd/validate/image.go | 5 +- .../__snapshots__/report_test.snap | 93 ++++++++++++++ internal/applicationsnapshot/report.go | 4 +- internal/applicationsnapshot/report_test.go | 117 ++++++++++++++++-- .../templates/text_report.tmpl | 7 +- internal/applicationsnapshot/vsa_test.go | 2 +- 6 files changed, 213 insertions(+), 15 deletions(-) diff --git a/cmd/validate/image.go b/cmd/validate/image.go index 8082d168f..83be7fed3 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -313,6 +313,8 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { defer c.Destroy() } + showSuccesses, _ := cmd.Flags().GetBool("show-successes") + // worker is responsible for processing one component at a time from the jobs channel, // and for emitting a corresponding result for the component on the results channel. worker := func(id int, jobs <-chan app.SnapshotComponent, results chan<- result) { @@ -332,7 +334,6 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { // Skip on err to not panic. Error is return on routine completion. if err == nil { res.component.Violations = out.Violations() - showSuccesses, _ := cmd.Flags().GetBool("show-successes") res.component.Warnings = out.Warnings() successes := out.Successes() @@ -403,7 +404,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { data.output = append(data.output, fmt.Sprintf("%s=%s", applicationsnapshot.JSON, data.outputFile)) } - report, err := applicationsnapshot.NewReport(data.snapshot, components, data.policy, manyData, manyPolicyInput) + report, err := applicationsnapshot.NewReport(data.snapshot, components, data.policy, manyData, manyPolicyInput, showSuccesses) if err != nil { return err } diff --git a/internal/applicationsnapshot/__snapshots__/report_test.snap b/internal/applicationsnapshot/__snapshots__/report_test.snap index 4c9656de9..2f8f6afb5 100755 --- a/internal/applicationsnapshot/__snapshots__/report_test.snap +++ b/internal/applicationsnapshot/__snapshots__/report_test.snap @@ -64,3 +64,96 @@ | Result | | :x: | --- + +[Test_TextReport/nothing - 1] +Success: false +Result: SKIPPED +Violations: 0, Warnings: 0, Successes: 0 + +--- + +[Test_TextReport/bunch - 1] +Success: false +Result: FAILURE +Violations: 4, Warnings: 4, Successes: 4 + +Components: +- Name: + ImageRef: registry.io/repository/component-1:tag + Violations: 2, Warnings: 0, Successes: 0 + +- Name: + ImageRef: registry.io/repository/component-2:tag + Violations: 0, Warnings: 2, Successes: 0 + +- Name: + ImageRef: registry.io/repository/component-3:tag + Violations: 0, Warnings: 0, Successes: 2 + +- Name: + ImageRef: registry.io/repository/component-4:tag + Violations: 2, Warnings: 2, Successes: 2 + +Results: +✕ [Violation] violation-1 + ImageRef: registry.io/repository/component-1:tag + Reason: Violation 1 message + Title: Violation 1 title + Description: Violation 1 description + Solution: Violation 1 solution + +✕ [Violation] violation-2 + ImageRef: registry.io/repository/component-1:tag + Reason: Violation 2 message + +✕ [Violation] violation-1 + ImageRef: registry.io/repository/component-4:tag + Reason: Violation 1 message + Title: Violation 1 title + Description: Violation 1 description + Solution: Violation 1 solution + +✕ [Violation] violation-2 + ImageRef: registry.io/repository/component-4:tag + Reason: Violation 2 message + +› [Warning] warning-1 + ImageRef: registry.io/repository/component-2:tag + Reason: Warning 1 message + Title: Warning 1 title + Description: Warning 1 description + Solution: Warning 1 solution + +› [Warning] warning-2 + ImageRef: registry.io/repository/component-2:tag + Reason: Warning 2 message + +› [Warning] warning-1 + ImageRef: registry.io/repository/component-4:tag + Reason: Warning 1 message + Title: Warning 1 title + Description: Warning 1 description + Solution: Warning 1 solution + +› [Warning] warning-2 + ImageRef: registry.io/repository/component-4:tag + Reason: Warning 2 message + +✓ [Success] success-1 + ImageRef: registry.io/repository/component-3:tag + Title: Success 1 title + Description: Success 1 description + +✓ [Success] success-2 + ImageRef: registry.io/repository/component-3:tag + +✓ [Success] success-1 + ImageRef: registry.io/repository/component-4:tag + Title: Success 1 title + Description: Success 1 description + +✓ [Success] success-2 + ImageRef: registry.io/repository/component-4:tag + + +--- diff --git a/internal/applicationsnapshot/report.go b/internal/applicationsnapshot/report.go index 6331d8f4d..546dde0e8 100644 --- a/internal/applicationsnapshot/report.go +++ b/internal/applicationsnapshot/report.go @@ -61,6 +61,7 @@ type Report struct { Data any `json:"-"` EffectiveTime time.Time `json:"effective-time"` PolicyInput [][]byte `json:"-"` + ShowSuccesses bool `json:"-"` } type summary struct { @@ -128,7 +129,7 @@ var OutputFormats = []string{ // WriteReport returns a new instance of Report representing the state of // components from the snapshot. -func NewReport(snapshot string, components []Component, policy policy.Policy, data any, policyInput [][]byte) (Report, error) { +func NewReport(snapshot string, components []Component, policy policy.Policy, data any, policyInput [][]byte, showSuccesses bool) (Report, error) { success := true // Set the report success, remains true if all components are successful @@ -159,6 +160,7 @@ func NewReport(snapshot string, components []Component, policy policy.Policy, da Data: data, PolicyInput: policyInput, EffectiveTime: policy.EffectiveTime().UTC(), + ShowSuccesses: showSuccesses, }, nil } diff --git a/internal/applicationsnapshot/report_test.go b/internal/applicationsnapshot/report_test.go index 777e67b99..1b742785a 100644 --- a/internal/applicationsnapshot/report_test.go +++ b/internal/applicationsnapshot/report_test.go @@ -51,7 +51,7 @@ func Test_ReportJson(t *testing.T) { ctx := context.Background() testPolicy := createTestPolicy(t, ctx) - report, err := NewReport("snappy", components, testPolicy, "data here", nil) + report, err := NewReport("snappy", components, testPolicy, "data here", nil, true) assert.NoError(t, err) testEffectiveTime := testPolicy.EffectiveTime().UTC().Format(time.RFC3339Nano) @@ -109,7 +109,7 @@ func Test_ReportYaml(t *testing.T) { ctx := context.Background() testPolicy := createTestPolicy(t, ctx) - report, err := NewReport("snappy", components, testPolicy, "data here", nil) + report, err := NewReport("snappy", components, testPolicy, "data here", nil, true) assert.NoError(t, err) testEffectiveTime := testPolicy.EffectiveTime().UTC().Format(time.RFC3339Nano) @@ -256,7 +256,7 @@ func Test_GenerateMarkdownSummary(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { ctx := context.Background() - report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, nil) + report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, nil, true) assert.NoError(t, err) report.created = time.Unix(0, 0).UTC() @@ -503,7 +503,7 @@ func Test_ReportSummary(t *testing.T) { for _, tc := range tests { t.Run(fmt.Sprintf("NewReport=%s", tc.name), func(t *testing.T) { ctx := context.Background() - report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), "data here", nil) + report, err := NewReport(tc.snapshot, []Component{tc.input}, createTestPolicy(t, ctx), "data here", nil, true) assert.NoError(t, err) assert.Equal(t, tc.want, report.toSummary()) }) @@ -640,7 +640,7 @@ func Test_ReportAppstudio(t *testing.T) { assert.NoError(t, err) ctx := context.Background() - report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, nil) + report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), nil, nil, true) assert.NoError(t, err) assert.False(t, report.created.IsZero()) assert.Equal(t, c.success, report.Success) @@ -788,7 +788,7 @@ func Test_ReportHACBS(t *testing.T) { assert.NoError(t, err) ctx := context.Background() - report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), "data here", nil) + report, err := NewReport(c.snapshot, c.components, createTestPolicy(t, ctx), "data here", nil, true) assert.NoError(t, err) assert.False(t, report.created.IsZero()) assert.Equal(t, c.success, report.Success) @@ -820,7 +820,7 @@ func Test_ReportPolicyInput(t *testing.T) { } ctx := context.Background() - report, err := NewReport("snapshot", nil, createTestPolicy(t, ctx), "data", policyInput) + report, err := NewReport("snapshot", nil, createTestPolicy(t, ctx), "data", policyInput, true) require.NoError(t, err) p := format.NewTargetParser(JSON, defaultWriter, fs) @@ -830,6 +830,109 @@ func Test_ReportPolicyInput(t *testing.T) { matchesJSONLFile(t, fs, policyInput, "default") } +func Test_TextReport(t *testing.T) { + warnings := []evaluator.Result{ + { + Metadata: map[string]interface{}{ + "code": "warning-1", + "title": "Warning 1 title", + "description": "Warning 1 description", + "solution": "Warning 1 solution", + }, + Message: "Warning 1 message", + }, + { + Metadata: map[string]interface{}{ + "code": "warning-2", + }, + Message: "Warning 2 message", + }, + } + violations := []evaluator.Result{ + { + Metadata: map[string]interface{}{ + "code": "violation-1", + "title": "Violation 1 title", + "description": "Violation 1 description", + "solution": "Violation 1 solution", + }, + Message: "Violation 1 message", + }, + { + Metadata: map[string]interface{}{ + "code": "violation-2", + }, + Message: "Violation 2 message", + }, + } + successes := []evaluator.Result{ + { + Metadata: map[string]interface{}{ + "code": "success-1", + "title": "Success 1 title", + "description": "Success 1 description", + "solution": "Success 1 solution", + }, + Message: "Success 1 message", + }, + { + Metadata: map[string]interface{}{ + "code": "success-2", + }, + Message: "Success 2 message", + }, + } + + cases := []struct { + name string + report Report + }{ + {"nothing", Report{}}, + {"bunch", Report{ + ShowSuccesses: true, + Components: []Component{ + { + SnapshotComponent: app.SnapshotComponent{ + ContainerImage: "registry.io/repository/component-1:tag", + }, + Violations: violations, + }, + { + SnapshotComponent: app.SnapshotComponent{ + ContainerImage: "registry.io/repository/component-2:tag", + }, + Warnings: warnings, + }, + { + SnapshotComponent: app.SnapshotComponent{ + ContainerImage: "registry.io/repository/component-3:tag", + }, + Successes: successes, + SuccessCount: 2, + }, + { + SnapshotComponent: app.SnapshotComponent{ + ContainerImage: "registry.io/repository/component-4:tag", + }, + Warnings: warnings, + Violations: violations, + Successes: successes, + SuccessCount: 2, + }, + }, + }}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + output, err := generateTextReport(&c.report) + require.NoError(t, err) + + snaps.MatchSnapshot(t, string(output)) + }) + } +} + func matchesJSONLFile(t *testing.T, fs afero.Fs, expected [][]byte, filename string) { f, err := fs.Open(filename) require.NoError(t, err) diff --git a/internal/applicationsnapshot/templates/text_report.tmpl b/internal/applicationsnapshot/templates/text_report.tmpl index 5f77ab679..a99ec10d5 100644 --- a/internal/applicationsnapshot/templates/text_report.tmpl +++ b/internal/applicationsnapshot/templates/text_report.tmpl @@ -2,14 +2,12 @@ {{- $r := .Report -}} {{- $c := $r.Components -}} -{{- $showSuccesses := (index $c 0).Successes -}} - Success: {{ $r.Success }} Result: {{ $t.Result }} Violations: {{ $t.Failures }}, Warnings: {{ $t.Warnings }}, Successes: {{ $t.Successes }}{{ nl -}} {{- template "_components.tmpl" $c -}} - +{{- if or (or (gt $t.Failures 0) (gt $t.Warnings 0)) (gt $t.Successes 0) -}} Results:{{ nl -}} {{- if gt $t.Failures 0 -}} {{- template "_results.tmpl" (toMap "Components" $c "Type" "Violation") -}} @@ -19,6 +17,7 @@ Results:{{ nl -}} {{- template "_results.tmpl" (toMap "Components" $c "Type" "Warning") -}} {{- end -}} -{{- if and (gt $t.Successes 0) $showSuccesses -}} +{{- if and (gt $t.Successes 0) $r.ShowSuccesses -}} {{- template "_results.tmpl" (toMap "Components" $c "Type" "Success") -}} {{- end -}} +{{- end -}} diff --git a/internal/applicationsnapshot/vsa_test.go b/internal/applicationsnapshot/vsa_test.go index 2680860cc..522d960f0 100644 --- a/internal/applicationsnapshot/vsa_test.go +++ b/internal/applicationsnapshot/vsa_test.go @@ -87,7 +87,7 @@ func TestNewVSA(t *testing.T) { }) assert.NoError(t, err) - report, err := NewReport("snappy", components, testPolicy, "data here", nil) + report, err := NewReport("snappy", components, testPolicy, "data here", nil, true) assert.NoError(t, err) expected := ProvenanceStatementVSA{ From ed354229bfd60cc16686b2140b4fe7430b470891 Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Wed, 24 Jul 2024 15:05:19 +0200 Subject: [PATCH 3/4] Per output format options In addition to the global flag `--show-successes` that influences all outputs and this adds `--show-successes` equivalent per output format. This allows the full report with successes to be present in JSON or YAML outputs while the output in text format can be outputted without successes. This is done by appending options in URL query format to the output format following a question mark, e.g. `--output text=output.txt?show-successes=false`. Reference: EC-564 --- cmd/validate/definition.go | 4 +- cmd/validate/image.go | 6 +- cmd/validate/input.go | 17 ++- .../modules/ROOT/pages/ec_validate_image.adoc | 4 +- .../modules/ROOT/pages/ec_validate_input.adoc | 7 +- features/__snapshots__/validate_image.snap | 121 ++++++++++++++++++ features/validate_image.feature | 27 ++++ internal/applicationsnapshot/report.go | 13 +- internal/applicationsnapshot/report_test.go | 9 +- internal/definition/report.go | 9 +- internal/definition/report_test.go | 2 +- internal/format/target.go | 62 ++++++--- internal/format/target_test.go | 40 ++++-- internal/input/report.go | 7 +- 14 files changed, 275 insertions(+), 53 deletions(-) diff --git a/cmd/validate/definition.go b/cmd/validate/definition.go index 92cf0d72a..028434df0 100644 --- a/cmd/validate/definition.go +++ b/cmd/validate/definition.go @@ -85,6 +85,7 @@ func validateDefinitionCmd(validate definitionValidationFn) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { var allErrors error report := definition.NewReport() + showSuccesses, _ := cmd.Flags().GetBool("show-successes") for i := range data.filePaths { fpath := data.filePaths[i] var sources []source.PolicySource @@ -98,7 +99,6 @@ func validateDefinitionCmd(validate definitionValidationFn) *cobra.Command { if out, err := validate(ctx, fpath, sources, data.namespaces); err != nil { allErrors = multierror.Append(allErrors, err) } else { - showSuccesses, _ := cmd.Flags().GetBool("show-successes") if !showSuccesses { for i := range out.PolicyCheck { out.PolicyCheck[i].Successes = []evaluator.Result{} @@ -107,7 +107,7 @@ func validateDefinitionCmd(validate definitionValidationFn) *cobra.Command { report.Add(*out) } } - p := format.NewTargetParser(definition.JSONReport, cmd.OutOrStdout(), utils.FS(cmd.Context())) + p := format.NewTargetParser(definition.JSONReport, format.Options{ShowSuccesses: showSuccesses}, cmd.OutOrStdout(), utils.FS(cmd.Context())) for _, target := range data.output { if err := report.Write(target, p); err != nil { allErrors = multierror.Append(allErrors, err) diff --git a/cmd/validate/image.go b/cmd/validate/image.go index 83be7fed3..d1c5b1b63 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -408,7 +408,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { if err != nil { return err } - p := format.NewTargetParser(applicationsnapshot.JSON, cmd.OutOrStdout(), utils.FS(cmd.Context())) + p := format.NewTargetParser(applicationsnapshot.JSON, format.Options{ShowSuccesses: showSuccesses}, cmd.OutOrStdout(), utils.FS(cmd.Context())) utils.SetColorEnabled(data.noColor, data.forceColor) if err := report.WriteAll(data.output, p); err != nil { return err @@ -466,7 +466,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { cmd.Flags().StringSliceVar(&data.output, "output", data.output, hd.Doc(` write output to a file in a specific format. Use empty string path for stdout. May be used multiple times. Possible formats are: - `+strings.Join(validOutputFormats, ", ")+`. + `+strings.Join(validOutputFormats, ", ")+`. In following format and file path + additional options can be provided in key=value form following the question + mark (?) sign, for example: --output text=output.txt?show-successes=false `)) cmd.Flags().StringVarP(&data.outputFile, "output-file", "o", data.outputFile, diff --git a/cmd/validate/input.go b/cmd/validate/input.go index d7e462da7..bbbb21a3f 100644 --- a/cmd/validate/input.go +++ b/cmd/validate/input.go @@ -21,12 +21,14 @@ import ( "errors" "fmt" "sort" + "strings" "sync" hd "github.com/MakeNowJust/heredoc" "github.com/hashicorp/go-multierror" "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/input" @@ -56,7 +58,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command { 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. `), @@ -112,6 +114,8 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command { var lock sync.WaitGroup + showSuccesses, _ := cmd.Flags().GetBool("show-successes") + for _, f := range data.filePaths { lock.Add(1) go func(fpath string) { @@ -129,7 +133,6 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command { // 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() @@ -176,7 +179,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command { return err } - p := format.NewTargetParser(input.JSON, cmd.OutOrStdout(), utils.FS(cmd.Context())) + p := format.NewTargetParser(input.JSON, format.Options{ShowSuccesses: showSuccesses}, cmd.OutOrStdout(), utils.FS(cmd.Context())) if err := report.WriteAll(data.output, p); err != nil { return err } @@ -197,10 +200,14 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command { * git reference (github.com/user/repo//default?ref=main), or * inline JSON ('{sources: {...}, configuration: {...}}')")`)) + validOutputFormats := applicationsnapshot.OutputFormats 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, yaml, - and summary`)) + path for stdout, e.g. yaml. May be used multiple times. Possible formats are: + `+strings.Join(validOutputFormats, ", ")+`. In following format and file path + additional options can be provided in key=value form following the question + mark (?) sign, for example: --output text=output.txt?show-successes=false + `)) cmd.Flags().BoolVarP(&data.strict, "strict", "s", data.strict, "Return non-zero status on non-successful validation") diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index acadfc0df..4d6c85875 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -135,7 +135,9 @@ rule. (Default: false) --no-color:: Disable color when using text output even when the current terminal supports it (Default: false) --output:: write output to a file in a specific format. Use empty string path for stdout. May be used multiple times. Possible formats are: -json, yaml, text, appstudio, summary, summary-markdown, junit, data, attestation, policy-input, vsa. +json, yaml, text, appstudio, summary, summary-markdown, junit, data, attestation, policy-input, vsa. In following format and file path +additional options can be provided in key=value form following the question +mark (?) sign, for example: --output text=output.txt?show-successes=false (Default: []) -o, --output-file:: [DEPRECATED] write output to a file. Use empty string for stdout, default behavior -p, --policy:: Policy configuration as: diff --git a/docs/modules/ROOT/pages/ec_validate_input.adoc b/docs/modules/ROOT/pages/ec_validate_input.adoc index 8e0a8683a..fbef5e3e1 100644 --- a/docs/modules/ROOT/pages/ec_validate_input.adoc +++ b/docs/modules/ROOT/pages/ec_validate_input.adoc @@ -46,8 +46,11 @@ current time, or a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z. (Default: violations, include the title and the description of the failed policy rule. (Default: false) -o, --output:: 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, yaml, -and summary (Default: []) +path for stdout, e.g. yaml. May be used multiple times. Possible formats are: +json, yaml, text, appstudio, summary, summary-markdown, junit, data, attestation, policy-input, vsa. In following format and file path +additional options can be provided in key=value form following the question +mark (?) sign, for example: --output text=output.txt?show-successes=false + (Default: []) -p, --policy:: Policy configuration as: * file (policy.yaml) * git reference (github.com/user/repo//default?ref=main), or diff --git a/features/__snapshots__/validate_image.snap b/features/__snapshots__/validate_image.snap index 8c21f52e1..6b7fafca0 100755 --- a/features/__snapshots__/validate_image.snap +++ b/features/__snapshots__/validate_image.snap @@ -4962,3 +4962,124 @@ Error: success criteria not met [many components and sources:stderr - 1] --- + +[Format options:stdout - 1] +Success: false +Result: FAILURE +Violations: 3, Warnings: 0, Successes: 4 +Component: Unnamed +ImageRef: ${REGISTRY}/acceptance/image@sha256:${REGISTRY_acceptance/image:latest_DIGEST} + +Results: +✕ [Violation] main.reject_with_term + ImageRef: ${REGISTRY}/acceptance/image@sha256:${REGISTRY_acceptance/image:latest_DIGEST} + Reason: Fails always (term1) + +✕ [Violation] main.reject_with_term + ImageRef: ${REGISTRY}/acceptance/image@sha256:${REGISTRY_acceptance/image:latest_DIGEST} + Reason: Fails always (term2) + +✕ [Violation] main.rejector + ImageRef: ${REGISTRY}/acceptance/image@sha256:${REGISTRY_acceptance/image:latest_DIGEST} + Reason: Fails always + + +--- + +[Format options:stderr - 1] +Error: success criteria not met + +--- + +[Format options:${TMPDIR}/output.json - 1] +{ + "success": false, + "components": [ + { + "name": "Unnamed", + "containerImage": "${REGISTRY}/acceptance/image@sha256:${REGISTRY_acceptance/image:latest_DIGEST}", + "source": {}, + "violations": [ + { + "msg": "Fails always (term1)", + "metadata": { + "code": "main.reject_with_term" + } + }, + { + "msg": "Fails always (term2)", + "metadata": { + "code": "main.reject_with_term" + } + }, + { + "msg": "Fails always", + "metadata": { + "code": "main.rejector" + } + } + ], + "successes": [ + { + "msg": "Pass", + "metadata": { + "code": "builtin.attestation.signature_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "builtin.attestation.syntax_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "builtin.image.signature_check" + } + }, + { + "msg": "Pass", + "metadata": { + "code": "main.acceptor" + } + } + ], + "success": false, + "signatures": [ + { + "keyid": "", + "sig": "${IMAGE_SIGNATURE_acceptance/image}" + } + ], + "attestations": [ + { + "type": "https://in-toto.io/Statement/v0.1", + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicateBuildType": "https://tekton.dev/attestations/chains/pipelinerun@v2", + "signatures": [ + { + "keyid": "", + "sig": "${ATTESTATION_SIGNATURE_acceptance/image}" + } + ] + } + ] + } + ], + "key": "${known_PUBLIC_KEY_JSON}", + "policy": { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/my-policy.git" + ] + } + ], + "rekorUrl": "${REKOR}", + "publicKey": "${known_PUBLIC_KEY}" + }, + "ec-version": "${EC_VERSION}", + "effective-time": "${TIMESTAMP}" +} +--- diff --git a/features/validate_image.feature b/features/validate_image.feature index b0aea1659..6b10f1197 100644 --- a/features/validate_image.feature +++ b/features/validate_image.feature @@ -1120,3 +1120,30 @@ Feature: evaluate enterprise contract When ec command is run with "validate image --snapshot acceptance/multitude --policy acceptance/ec-policy --public-key ${known_PUBLIC_KEY} --rekor-url ${REKOR} --show-successes" Then the exit status should be 0 And the output should match the snapshot + + Scenario: Format options + Given a key pair named "known" + And an image named "acceptance/image" + And a valid image signature of "acceptance/image" image signed by the "known" key + And a valid Rekor entry for image signature of "acceptance/image" + And a valid attestation of "acceptance/image" signed by the "known" key + And a valid Rekor entry for attestation of "acceptance/image" + And a git repository named "my-policy" with + | happy_day.rego | examples/happy_day.rego | + | reject.rego | examples/reject.rego | + And policy configuration named "ec-policy" with specification + """ + { + "sources": [ + { + "policy": [ + "git::https://${GITHOST}/git/my-policy.git" + ] + } + ] + } + """ + When ec command is run with "validate image --image ${REGISTRY}/acceptance/image --policy acceptance/ec-policy --rekor-url ${REKOR} --public-key ${known_PUBLIC_KEY} --output text?show-successes=false --output json=${TMPDIR}/output.json --show-successes" + Then the exit status should be 1 + And the output should match the snapshot + And the "${TMPDIR}/output.json" file should match the snapshot diff --git a/internal/applicationsnapshot/report.go b/internal/applicationsnapshot/report.go index 546dde0e8..9f6cb1411 100644 --- a/internal/applicationsnapshot/report.go +++ b/internal/applicationsnapshot/report.go @@ -19,7 +19,6 @@ package applicationsnapshot import ( "bytes" "embed" - _ "embed" "encoding/json" "encoding/xml" "fmt" @@ -170,11 +169,17 @@ func (r Report) WriteAll(targets []string, p format.TargetParser) (allErrors err targets = append(targets, JSON) } for _, targetName := range targets { - target := p.Parse(targetName) + target, err := p.Parse(targetName) + if err != nil { + allErrors = multierror.Append(allErrors, err) + continue + } + r.applyOptions(target.Options) data, err := r.toFormat(target.Format) if err != nil { allErrors = multierror.Append(allErrors, err) + continue } if !bytes.HasSuffix(data, []byte{'\n'}) { @@ -256,6 +261,10 @@ func (r *Report) toSummary() summary { return pr } +func (r *Report) applyOptions(opts format.Options) { + r.ShowSuccesses = opts.ShowSuccesses +} + // condensedMsg reduces repetitive error messages. func condensedMsg(results []evaluator.Result) map[string][]string { maxErr := 1 diff --git a/internal/applicationsnapshot/report_test.go b/internal/applicationsnapshot/report_test.go index 1b742785a..436624a21 100644 --- a/internal/applicationsnapshot/report_test.go +++ b/internal/applicationsnapshot/report_test.go @@ -647,7 +647,7 @@ func Test_ReportAppstudio(t *testing.T) { report.created = time.Unix(0, 0).UTC() - p := format.NewTargetParser(JSON, defaultWriter, fs) + p := format.NewTargetParser(JSON, format.Options{}, defaultWriter, fs) assert.NoError(t, report.WriteAll([]string{"appstudio=report.json", "appstudio"}, p)) reportText, err := afero.ReadFile(fs, "report.json") @@ -795,7 +795,7 @@ func Test_ReportHACBS(t *testing.T) { report.created = time.Unix(0, 0).UTC() - p := format.NewTargetParser(JSON, defaultWriter, fs) + p := format.NewTargetParser(JSON, format.Options{}, defaultWriter, fs) assert.NoError(t, report.WriteAll([]string{"hacbs=report.json", "hacbs"}, p)) reportText, err := afero.ReadFile(fs, "report.json") @@ -823,7 +823,7 @@ func Test_ReportPolicyInput(t *testing.T) { report, err := NewReport("snapshot", nil, createTestPolicy(t, ctx), "data", policyInput, true) require.NoError(t, err) - p := format.NewTargetParser(JSON, defaultWriter, fs) + p := format.NewTargetParser(JSON, format.Options{}, defaultWriter, fs) require.NoError(t, report.WriteAll([]string{"policy-input=policy-input.yaml", "policy-input"}, p)) matchesJSONLFile(t, fs, policyInput, "policy-input.yaml") @@ -925,7 +925,8 @@ func Test_TextReport(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - output, err := generateTextReport(&c.report) + r := c.report + output, err := generateTextReport(&r) require.NoError(t, err) snaps.MatchSnapshot(t, string(output)) diff --git a/internal/definition/report.go b/internal/definition/report.go index cca2ca195..e162b5d0c 100644 --- a/internal/definition/report.go +++ b/internal/definition/report.go @@ -92,11 +92,12 @@ func (r *Report) Write(targetName string, p format.TargetParser) error { return nil } - var data []byte - var err error - - target := p.Parse(targetName) + target, err := p.Parse(targetName) + if err != nil { + return err + } + var data []byte switch target.Format { case JSONReport: if data, err = json.Marshal(r); err != nil { diff --git a/internal/definition/report_test.go b/internal/definition/report_test.go index 6dce5d995..25421ede4 100644 --- a/internal/definition/report_test.go +++ b/internal/definition/report_test.go @@ -142,7 +142,7 @@ func TestReport(t *testing.T) { r.Add(o) } fs := afero.NewMemMapFs() - parser := format.NewTargetParser("ignored", nil, fs) + parser := format.NewTargetParser("ignored", format.Options{}, nil, fs) for _, format := range []string{"json", "yaml"} { fname := "out." + format diff --git a/internal/format/target.go b/internal/format/target.go index 56077fda0..650083be6 100644 --- a/internal/format/target.go +++ b/internal/format/target.go @@ -18,6 +18,8 @@ package format import ( "io" + "net/url" + "strconv" "strings" "github.com/spf13/afero" @@ -25,8 +27,33 @@ import ( // Target represents a writer with a specified format. type Target struct { - Format string - writer io.Writer + Format string + Options Options + writer io.Writer +} + +// options that can be configured per Target +type Options struct { + ShowSuccesses bool +} + +// mutate parses the given string as URL query parameters and sets the fields +// according to the parsed values +func (o *Options) mutate(given string) error { + vals, err := url.ParseQuery(given) + if err != nil { + return err + } + + if v := vals.Get("show-successes"); v != "" { + if f, err := strconv.ParseBool(v); err == nil { + o.ShowSuccesses = f + } else { + return err + } + } + + return nil } // Write proxies the write operation to the underlying writer. @@ -36,37 +63,42 @@ func (t *Target) Write(data []byte) (int, error) { // TargetParser is responsible for creating Target objects. type TargetParser struct { - defaultFormat string - defaultWriter io.Writer - fs afero.Fs + defaultFormat string + defaultWriter io.Writer + defaultOptions Options + fs afero.Fs } // NewTargetParser creates a new TargetParser with the given options. -func NewTargetParser(targetName string, writer io.Writer, fs afero.Fs) TargetParser { - return TargetParser{defaultFormat: targetName, defaultWriter: writer, fs: fs} +func NewTargetParser(targetName string, options Options, writer io.Writer, fs afero.Fs) TargetParser { + return TargetParser{defaultFormat: targetName, defaultOptions: options, defaultWriter: writer, fs: fs} } // Parse creates a new Target given the provided target name. -func (tm *TargetParser) Parse(name string) Target { +func (tm *TargetParser) Parse(given string) (*Target, error) { target := Target{writer: tm.defaultWriter} - var path string + formatAndPath, opts, foundOpts := strings.Cut(given, "?") - parts := strings.SplitN(name, "=", 2) + target.Options = tm.defaultOptions + if foundOpts { + if err := target.Options.mutate(opts); err != nil { + return nil, err + } + } + + var path string + target.Format, path, _ = strings.Cut(formatAndPath, "=") - target.Format = parts[0] if target.Format == "" { target.Format = tm.defaultFormat } - if len(parts) == 2 { - path = parts[1] - } if path != "" { target.writer = &fileWriter{path: path, fs: tm.fs} } - return target + return &target, nil } // fileWriter implements a simple Writer wrapper for afero.Fs. diff --git a/internal/format/target_test.go b/internal/format/target_test.go index 547cd0d51..8cb82faa7 100644 --- a/internal/format/target_test.go +++ b/internal/format/target_test.go @@ -23,36 +23,48 @@ import ( "github.com/spf13/afero" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTargetParser(t *testing.T) { defaultFormat := "default" defaultPath := "default.out" + defaultOptions := Options{ + ShowSuccesses: false, + } + cases := []struct { - name string - expectFormat string - expectDefaultWriter bool - targetName string + name string + expectedFormat string + expectedPath string + expectedOptions Options + targetName string }{ - {name: "all defaults", expectFormat: defaultFormat, expectDefaultWriter: true}, - {name: "all defaults", expectFormat: "spam", targetName: "spam", expectDefaultWriter: true}, - {name: "all defaults", expectFormat: "spam", targetName: "spam=", expectDefaultWriter: true}, - {name: "all defaults", expectFormat: "spam", targetName: "spam=spam.out"}, + {name: "all defaults", expectedFormat: defaultFormat, expectedOptions: defaultOptions}, + {name: "format", expectedFormat: "spam", expectedOptions: defaultOptions, targetName: "spam"}, + {name: "format no file", expectedFormat: "spam", expectedOptions: defaultOptions, targetName: "spam="}, + {name: "format and file", expectedFormat: "spam", expectedOptions: defaultOptions, targetName: "spam=spam.out", expectedPath: "spam.out"}, + {name: "format and option", expectedFormat: "spam", expectedOptions: Options{ShowSuccesses: true}, targetName: "spam?show-successes=true"}, + {name: "format no file with option", expectedFormat: "spam", expectedOptions: Options{ShowSuccesses: true}, targetName: "spam=?show-successes=true"}, + {name: "format with file and option", expectedFormat: "spam", expectedOptions: Options{ShowSuccesses: true}, targetName: "spam=spam.out?show-successes=true", expectedPath: "spam.out"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { fs := afero.NewMemMapFs() defaultWriter := fileWriter{path: defaultPath, fs: fs} - parser := NewTargetParser(defaultFormat, defaultWriter, fs) - target := parser.Parse(c.targetName) + parser := NewTargetParser(defaultFormat, defaultOptions, defaultWriter, fs) + target, err := parser.Parse(c.targetName) + require.NoError(t, err) - assert.Equal(t, target.Format, c.expectFormat) - if c.expectDefaultWriter { - assert.Equal(t, target.writer, defaultWriter) + assert.Equal(t, c.expectedFormat, target.Format) + if c.expectedPath == "" { + assert.Equal(t, defaultWriter, target.writer) } else { - assert.NotEqual(t, target.writer, defaultWriter) + assert.Equal(t, c.expectedPath, target.writer.(*fileWriter).path) } + + assert.Equal(t, c.expectedOptions, target.Options) }) } } diff --git a/internal/input/report.go b/internal/input/report.go index 204c8c348..028b0e33f 100644 --- a/internal/input/report.go +++ b/internal/input/report.go @@ -123,11 +123,16 @@ func (r Report) WriteAll(targets []string, p format.TargetParser) (allErrors err targets = append(targets, JSON) } for _, targetName := range targets { - target := p.Parse(targetName) + target, err := p.Parse(targetName) + if err != nil { + allErrors = multierror.Append(allErrors, err) + continue + } data, err := r.toFormat(target.Format) if err != nil { allErrors = multierror.Append(allErrors, err) + continue } if !bytes.HasSuffix(data, []byte{'\n'}) { From 751f2e58ca3c5b9d02d9ff2504003d724cd960a0 Mon Sep 17 00:00:00 2001 From: Zoran Regvart Date: Thu, 18 Jul 2024 11:06:27 +0200 Subject: [PATCH 4/4] Humanize v-e-c Task logs The top of the logfile for a TaskRun of verify-enterprise-contract Task should be the text (human readable) report. Following it are the YAML and JSON logs and preceded by a break `----- DEBUG OUTPUT -----` are the debug logs and version information. This is to make the TaskRun's logs easier to understand by humans. Reference: EC-564 --- .../__snapshots__/task_validate_image.snap | 4 +-- features/task_validate_image.feature | 2 +- .../0.1/verify-enterprise-contract.yaml | 32 +++++++++++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 8791dc98d..95a5d5392 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -471,7 +471,7 @@ success: true --- [Initialize TUF succeeds:initialize-tuf - 1] -TUF_MIRROR not set. Skipping TUF root initialization. +TUF_MIRROR parameter not provided. Skipping TUF root initialization. --- @@ -522,7 +522,7 @@ TUF_MIRROR not set. Skipping TUF root initialization. --- [Outputs are there:initialize-tuf - 1] -TUF_MIRROR not set. Skipping TUF root initialization. +TUF_MIRROR parameter not provided. Skipping TUF root initialization. --- diff --git a/features/task_validate_image.feature b/features/task_validate_image.feature index b185ab7bd..e3792b7b7 100644 --- a/features/task_validate_image.feature +++ b/features/task_validate_image.feature @@ -296,7 +296,7 @@ Feature: Verify Enterprise Contract Tekton Tasks | IGNORE_REKOR | true | | EFFECTIVE_TIME | 2020-01-01T00:00:00Z | Then the task should succeed - And the task logs for step "validate" should contain "Using provided effective time 2020-01-01T00:00:00Z" + And the task logs for step "debug-log" should contain "Using provided effective time 2020-01-01T00:00:00Z" Scenario: SSL_CERT_DIR environment variable is customized Given a working namespace diff --git a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml index a4a5d0ac7..0fb1c8157 100644 --- a/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml +++ b/tasks/verify-enterprise-contract/0.1/verify-enterprise-contract.yaml @@ -135,11 +135,6 @@ spec: value: "$(params.HOMEDIR)" steps: - - name: version - image: quay.io/enterprise-contract/ec-cli:snapshot - command: [ec] - args: - - version - name: initialize-tuf image: quay.io/enterprise-contract/ec-cli:snapshot @@ -147,7 +142,7 @@ spec: set -euo pipefail if [[ -z "${TUF_MIRROR:-}" ]]; then - echo 'TUF_MIRROR not set. Skipping TUF root initialization.' + echo 'TUF_MIRROR parameter not provided. Skipping TUF root initialization.' exit fi @@ -160,6 +155,7 @@ spec: - name: validate image: quay.io/enterprise-contract/ec-cli:snapshot + onError: continue # progress even if the step fails so we can see the debug logs command: [ec] args: - validate @@ -182,11 +178,14 @@ spec: - "--effective-time=$(params.EFFECTIVE_TIME)" - "--extra-rule-data=$(params.EXTRA_RULE_DATA)" - "--output" + - "text?show-successes=false" + - "--output" - "yaml=$(params.HOMEDIR)/report.yaml" - "--output" - "appstudio=$(results.TEST_OUTPUT.path)" - "--output" - "json=$(params.HOMEDIR)/report-json.json" + - "--logfile=$(params.HOMEDIR)/debug.log" env: - name: SSL_CERT_DIR # The Tekton Operator automatically sets the SSL_CERT_DIR env to the value below but, @@ -211,23 +210,44 @@ spec: - name: report image: quay.io/enterprise-contract/ec-cli:snapshot + onError: continue # progress even if the step fails so we can see the debug logs command: [cat] args: - "$(params.HOMEDIR)/report.yaml" - name: report-json image: quay.io/enterprise-contract/ec-cli:snapshot + onError: continue # progress even if the step fails so we can see the debug logs command: [cat] args: - "$(params.HOMEDIR)/report-json.json" - name: summary image: quay.io/enterprise-contract/ec-cli:snapshot + onError: continue # progress even if the step fails so we can see the debug logs command: [jq] args: - "." - "$(results.TEST_OUTPUT.path)" + - name: info + image: quay.io/enterprise-contract/ec-cli:snapshot + command: [printf] + args: + - "----- DEBUG OUTPUT -----\n" + + - name: version + image: quay.io/enterprise-contract/ec-cli:snapshot + command: [ec] + args: + - version + + - name: debug-log + image: quay.io/enterprise-contract/ec-cli:snapshot + command: [cat] + args: + - "$(params.HOMEDIR)/debug.log" + - name: assert image: quay.io/enterprise-contract/ec-cli:snapshot command: [jq]