diff --git a/README.md b/README.md index 268e049..d0edec4 100644 --- a/README.md +++ b/README.md @@ -222,9 +222,7 @@ other source types use custom code in the `kubeapply` binary. This validates all of the expanded configs for the cluster using the [`kubeconform`](https://github.com/yannh/kubeconform) library. It also, optionally, supports validating configs using one or more [OPA](https://www.openpolicyagent.org/) policies in -rego format. The latter allows checking that configs satisfy organization-specific standards, -e.g. that resource labels are in the correct format, that images are only pulled from the -expected registries, etc. +rego format; see the "Experimental features" section below for more details. #### Diff @@ -338,6 +336,24 @@ where the `url`s are in the same format as those for Helm chart locations, e.g. `file://path/to/my/file`. The outputs of each profile will be expanded into `[expanded dir]/[profile name]/...`. +### OPA policy checks + +The `kubeapply validate` subcommand now supports checking expanded configs against policies in +[Open Policy Agent (OPA)](https://www.openpolicyagent.org/) format. This can be helpful for +enforcing organization-specific standards, e.g. that images need to be pulled from a particular +private registry, that all labels are in a consistent format, etc. + +To use this, write up your policies as `.rego` files as described in the OPA documentation and run +the former subcommand with one or more `--policy=[path to policy]` arguments. By default, policies +should be in the `com.segment.kubeapply` package. Denial reasons, if any, are returned by +setting a `deny` variable with a set of denial reason strings. If this set is empty, +`kubeapply` will assume that the config has passed all checks in the policy file. + +If a denial reason begins with the string `warn:`, then that denial will be treated as a +non-blocking warning as opposed to an error that causes validation to fail. + +See [this unit test](/pkg/validation/policy_test.go) for some examples. + ## Testing ### Unit tests diff --git a/cmd/kubeapply/subcmd/validate.go b/cmd/kubeapply/subcmd/validate.go index 5965bb7..116068a 100644 --- a/cmd/kubeapply/subcmd/validate.go +++ b/cmd/kubeapply/subcmd/validate.go @@ -2,6 +2,7 @@ package subcmd import ( "context" + "errors" "fmt" "path/filepath" @@ -144,69 +145,27 @@ func execValidation(ctx context.Context, clusterConfig *config.ClusterConfig) er return err } - numInvalidResourceChecks := 0 - numValidResourceChecks := 0 - numSkippedResourceChecks := 0 - - for _, result := range results { - for _, checkResult := range result.CheckResults { - switch checkResult.Status { - case validation.StatusValid: - numValidResourceChecks++ - log.Debugf( - "Resource %s in file %s OK according to check %s", - result.Resource.PrettyName(), - result.Resource.Path, - checkResult.CheckName, - ) - case validation.StatusSkipped: - numSkippedResourceChecks++ - log.Debugf( - "Resource %s in file %s was skipped by check %s", - result.Resource.PrettyName(), - result.Resource.Path, - checkResult.CheckName, - ) - case validation.StatusError: - numInvalidResourceChecks++ - log.Errorf( - "Resource %s in file %s could not be processed by check %s: %s", - result.Resource.PrettyName(), - result.Resource.Path, - checkResult.CheckName, - checkResult.Message, - ) - case validation.StatusInvalid: - numInvalidResourceChecks++ - log.Errorf( - "Resource %s in file %s is invalid according to check %s: %s", - result.Resource.PrettyName(), - result.Resource.Path, - checkResult.CheckName, - checkResult.Message, - ) - case validation.StatusEmpty: - default: - log.Infof("Unrecognized result type: %+v", result) - } + counts := validation.CountsByStatus(results) + resultsWithIssues := validation.ResultsWithIssues(results) + + if len(resultsWithIssues) > 0 { + log.Warnf("Found %d resources with potential issues", len(resultsWithIssues)) + for _, result := range resultsWithIssues { + fmt.Println( + validation.ResultTable( + result, + clusterConfig.DescriptiveName(), + clusterConfig.ExpandedPath, + debug, + ), + ) } } - if numInvalidResourceChecks > 0 { - return fmt.Errorf( - "Validation failed for %d resources in cluster %s (%d checks valid, %d skipped)", - numInvalidResourceChecks, - clusterConfig.DescriptiveName(), - numValidResourceChecks, - numSkippedResourceChecks, - ) + if counts[validation.StatusError]+counts[validation.StatusInvalid] > 0 { + return errors.New("Validation failed") } - log.Infof( - "Validation of cluster %s passed (%d checks valid, %d skipped)", - clusterConfig.DescriptiveName(), - numValidResourceChecks, - numSkippedResourceChecks, - ) + log.Infof("Validation passed") return nil } diff --git a/data/data.go b/data/data.go index 8c4588b..c22b5ff 100644 --- a/data/data.go +++ b/data/data.go @@ -8,10 +8,10 @@ // scripts/cluster-summary/__init__.py (0) // scripts/cluster-summary/cluster_summary.py (4.488kB) // scripts/cluster-summary/tabulate.py (57.091kB) -// scripts/create-lambda-bundle.sh (819B) +// scripts/create-lambda-bundle.sh (791B) // scripts/kdiff-wrapper.sh (247B) // scripts/kindctl.sh (1.76kB) -// scripts/pull-deps.sh (2.203kB) +// scripts/pull-deps.sh (1.797kB) // scripts/raw-diff.sh (143B) package data @@ -241,7 +241,7 @@ func scriptsClusterSummaryTabulatePy() (*asset, error) { return a, nil } -var _scriptsCreateLambdaBundleSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x92\x51\x6b\xdb\x30\x14\x85\xdf\xef\xaf\xb8\x53\x4c\x68\x1e\x64\xaf\x7b\x5b\x21\x85\xac\xf1\x58\x60\x5d\x42\xea\xc0\x68\x29\x41\xb6\x6e\x6a\x11\xd9\x16\xb2\xd4\xd0\x1a\xff\xf7\xe1\xa4\x71\x17\x96\xee\xc9\x86\xfb\x9d\x8b\xce\x39\x77\xf0\x29\x4a\x55\x19\xa5\xa2\xce\x01\x06\x78\x63\x49\x38\x42\x81\xa9\x2f\xa5\x26\xdc\x54\x16\xb5\x28\x52\x29\x42\x18\xc0\x00\x57\xb5\x78\xa2\x2b\x18\x20\x62\x18\xd5\x99\x55\xc6\xd5\x51\xb6\x17\xf1\x03\xc7\x0f\xca\xb0\xce\xf1\xe1\x55\x19\xac\xbc\x33\xde\xe1\x46\x69\xc2\x52\x14\xf4\x08\x50\x93\x43\x4e\x00\xcb\x78\x31\x5f\x2f\xe7\xf3\x64\xcc\x82\x0b\xcc\x24\x76\x1f\xa9\x6c\x87\x21\x0b\x9a\x6f\x93\xbb\x1f\xeb\xbb\xf9\x6a\x79\x13\x3f\x7c\x7e\x6c\x19\x8e\x18\x5e\x47\x92\x9e\xa3\xd2\x6b\x8d\x5f\xae\x87\x97\x38\x1c\x76\xc2\x30\xec\x7e\xcc\x4e\xe2\x88\xc1\x34\xfe\x3e\x59\xfd\x4c\xd6\xf3\x55\xb2\x58\x25\xeb\x5f\x93\xdb\x78\xcc\xde\x4c\xbc\x2a\xc3\xe0\xef\x41\xd0\x5c\x5e\xf1\xe0\x8c\xa4\x3d\x62\xf7\xb3\xc5\x38\x68\xfa\xb7\xb6\x51\xd0\x9c\x60\x40\x59\x5e\x21\xdb\x27\xa7\xca\xa7\x63\x72\xc2\x61\x0f\xde\xcf\x16\x2d\x03\x30\xbe\xce\x25\x9e\xec\x4a\xbd\xd2\x12\xba\x9c\xb8\xfd\x8a\xc1\x3b\x8f\x5b\x9f\x92\x30\x46\xbf\xbc\xe5\xfa\x7f\x08\x4c\x65\x24\x40\x12\xdf\x2e\xd6\xd3\xd9\x72\x1c\x5c\x14\x5b\x47\x85\x41\x2e\x47\x00\x1b\x5f\x66\x4e\x55\x25\x66\x9a\x44\xe9\x0d\x36\x80\x68\x0b\xe4\x76\xd3\xe5\x7c\x54\xb5\x0c\x10\x0f\x6e\xa6\xa4\xc9\x91\xc4\xfd\x8e\x5d\x65\xb7\x9d\x33\xa9\x2c\x65\xae\xb2\x2f\x78\xa2\x69\x01\x9c\x15\xa6\x5f\x1e\xff\x9e\x25\xef\x66\x7b\x10\x20\xe8\x8d\xf7\xa7\x63\xbc\xd6\x5c\x92\xa9\xc3\xee\xfe\xce\x59\xcc\x49\x17\x67\x07\x62\x57\x73\x25\x0a\x2e\xbc\xcb\xa9\x74\x2a\x13\xae\xb2\x1f\xa6\xf4\x2c\xf4\x87\xb3\xcc\xe9\x93\x16\x49\x1e\x4b\xfc\xa7\xc1\x2e\xe5\x3f\x01\x00\x00\xff\xff\x94\xb8\x88\x3b\x33\x03\x00\x00") +var _scriptsCreateLambdaBundleSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x92\xd1\x6a\xdb\x30\x14\x86\xef\xcf\x53\x9c\x29\xa6\x34\x17\xb2\xd7\xdd\xad\x90\x42\xd6\x78\x2c\xb0\x2e\x21\x75\x60\xb4\x14\x23\x5b\x27\xb5\x88\x6c\x0b\x59\x5a\x68\x8d\xdf\x7d\x38\x69\xdc\x85\x65\xbb\xb2\x41\xdf\x7f\xd0\xff\x1d\x8d\x3e\x44\x99\xaa\xa2\x4c\x34\x05\xc0\x08\x6f\x2d\x09\x47\x28\x30\xf3\x95\xd4\x84\x9b\xda\xa2\x16\x65\x26\x45\x08\x23\x18\xe1\xba\x11\xcf\x74\x0d\x23\x44\x0c\xa3\x26\xb7\xca\xb8\x26\xca\xf7\x21\x7e\xe0\xf8\x21\x19\x36\x05\x3e\xbe\x2a\x83\xb5\x77\xc6\x3b\xdc\x28\x4d\x58\x89\x92\x9e\x00\x1a\x72\xc8\x09\x60\x15\x2f\x17\xe9\x6a\xb1\x48\x26\x2c\xb8\xc4\x5c\x62\xff\x91\xca\xf6\x18\xb2\xa0\xfd\x32\xbd\xff\x96\xde\x2f\xd6\xab\xdb\xf8\xf1\xe3\x53\xc7\x70\xcc\xf0\x26\x92\xf4\x2b\xaa\xbc\xd6\xf8\xe9\xe6\xe2\x0a\x2f\x2e\xfa\x60\x18\xf6\x3f\x66\x27\x71\xcc\x60\x16\x7f\x9d\xae\xbf\x27\xe9\x62\x9d\x2c\xd7\x49\xfa\x63\x7a\x17\x4f\xd8\x5b\x89\x57\x65\x18\xfc\x79\x10\xb4\x57\xd7\x3c\x38\x13\xe9\x8e\xd8\xc3\x7c\x39\x09\xda\xe1\xae\x5d\x14\xb4\x27\x18\x50\x5e\xd4\xc8\xf6\xe6\x54\xf5\x7c\x34\x27\x1c\x0e\xe0\xc3\x7c\xd9\x31\x00\xe3\x9b\x42\xe2\xc9\xac\xcc\x2b\x2d\xa1\xf7\xc4\xed\x67\x0c\xde\x79\xdc\xfa\x8c\x84\x31\xfa\xe5\xcd\xeb\xff\x21\x30\xb5\x91\x00\x49\x7c\xb7\x4c\x67\xf3\xd5\x24\xb8\x2c\xb7\x8e\x4a\x83\x5c\x8e\x01\x36\xbe\xca\x9d\xaa\x2b\xcc\x35\x89\xca\x1b\x6c\x01\xd1\x96\xc8\xed\xa6\xf7\x7c\x4c\x75\x0c\x10\x0f\x6d\x66\xa4\xc9\x91\xc4\xfd\x8c\x5d\x6d\xb7\x7d\x33\xa9\x2c\xe5\xae\xb6\x2f\x78\x92\xe9\x00\x9c\x15\x66\x18\x1e\xff\x9c\x27\xef\x65\x07\x10\x20\x18\x8a\x0f\x4f\xc7\x78\xad\xb9\x24\xd3\x84\xfd\xfb\x3b\x57\xb1\x20\x5d\x9e\x3d\x10\xbb\x86\x2b\x51\x72\xe1\x5d\x41\x95\x53\xb9\x70\xb5\xfd\xa7\xa5\xdc\xe9\x93\x4d\x91\x3c\x2e\xea\xaf\x2d\xf5\x26\x7f\x07\x00\x00\xff\xff\xe6\x27\xe5\x20\x17\x03\x00\x00") func scriptsCreateLambdaBundleShBytes() ([]byte, error) { return bindataRead( @@ -256,8 +256,8 @@ func scriptsCreateLambdaBundleSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "scripts/create-lambda-bundle.sh", size: 819, mode: os.FileMode(0755), modTime: time.Unix(1611633754, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x53, 0x3c, 0xc6, 0xbf, 0x2d, 0x5c, 0xb1, 0x3b, 0x37, 0x5, 0xa4, 0x16, 0x52, 0x5a, 0xd4, 0x49, 0xbb, 0xf8, 0x4, 0x83, 0x3c, 0xbf, 0x10, 0x39, 0xcf, 0x18, 0xbf, 0x52, 0x35, 0xee, 0xca, 0x3}} + info := bindataFileInfo{name: "scripts/create-lambda-bundle.sh", size: 791, mode: os.FileMode(0755), modTime: time.Unix(1618002872, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc6, 0x84, 0xb9, 0x6d, 0xfb, 0x33, 0xdc, 0xa7, 0xa8, 0x67, 0x8f, 0x17, 0xde, 0x9, 0x83, 0x14, 0x89, 0x73, 0xea, 0x81, 0x18, 0xa4, 0x2b, 0x9d, 0x33, 0xe3, 0xfe, 0xc3, 0xd9, 0x64, 0x43, 0x3c}} return a, nil } @@ -301,7 +301,7 @@ func scriptsKindctlSh() (*asset, error) { return a, nil } -var _scriptsPullDepsSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x95\x5b\x6f\xda\x48\x14\xc7\xdf\xfd\x29\xbc\x6e\xdf\x56\xd8\x73\xbf\xac\x94\x07\x36\x8d\x4a\xb4\x4d\x90\x42\xd2\x57\x34\x33\x67\x06\xac\x80\x49\xb1\x21\x51\xbb\x7c\xf7\xca\x5c\x82\x01\x93\xa4\x4f\x96\x3d\xe7\x9c\xff\x7f\x7e\x73\xe6\xf8\xd3\x5f\x99\xcd\x8b\xcc\x9a\x72\x1c\x45\xa5\xaf\xe2\x8e\x8f\xa2\x4f\xf1\x9d\xff\xb1\xc8\xe7\x1e\xe2\xa5\x9f\x97\xf9\xac\x28\xa3\xde\xd5\xb7\x9b\xe1\xf7\xab\xbb\xc1\x75\xff\xf6\x22\xa1\x29\x4f\x51\xb2\xf9\x38\xe8\x75\x09\x17\xc3\xc1\xc3\xcd\x45\x42\x43\x08\x88\x72\x06\x3c\x58\xc3\x9c\xa4\xde\x02\x07\xcb\xb5\xe1\x1a\xac\x24\x41\x19\x6e\xad\xc7\x18\x4b\x83\x2c\xe5\xdc\x22\x47\xb4\xa2\x5e\x2b\xb0\x9a\xdb\x24\x8a\xae\xbb\x37\xc3\xee\xc3\x7d\xef\xea\xf6\xfe\xfa\xb2\x7b\xdf\xbf\xdb\xab\xa2\x94\xa7\x24\x69\x89\x68\x5a\xa8\xeb\x33\x66\x40\x06\x01\xa0\xa4\x41\xc4\x23\xcb\x04\x35\xc4\x78\xd0\x02\x63\x45\x85\xb7\x24\x30\x04\x32\x58\xaf\x1c\xc7\x92\x09\x64\x18\x55\x5c\x10\x5c\x5b\xf8\xef\xe1\xdf\xab\xef\xdd\x6f\x4d\x61\xbc\xde\xef\x6e\xa1\xa9\x27\x91\x0d\x81\x08\x46\x0c\x51\x4a\x38\x04\xda\x5b\x6f\x18\x27\x21\x58\x85\x03\xa5\xd4\x68\x2e\x3c\x11\xd6\x7a\xa4\x88\x08\x20\x9d\x90\x5a\x7a\xca\xa8\xe7\xc6\x6c\xf5\x2e\xef\x1b\x7a\x4b\x9c\x12\x54\x6f\x75\xb7\x32\xe8\x75\x39\x26\x1b\x41\xcf\x38\xa6\x0e\x40\x70\x0f\x5a\x21\x60\x9a\x12\xae\x9d\x93\xde\x18\x41\x1d\xc3\x3c\x68\xc9\xb1\x00\x4b\x6a\x1f\xc1\x28\x27\x19\x35\xc2\x33\x4c\x0d\x72\x60\x02\xd1\x1a\x10\x05\x90\x5a\x13\x25\x5c\xa0\x84\x60\x45\x6c\xd0\x42\x33\x82\x98\x52\xcc\x5a\x04\x80\x10\x95\x2e\x30\xc6\x35\x01\x08\x86\x7b\x8e\x03\x56\xb4\x36\x7c\x7d\xfb\xa5\xe1\x16\xa5\x2a\xc5\xc9\xe6\xeb\xa0\xd7\x1d\xee\xd9\x28\xec\x28\x58\x26\xb5\x55\x88\x03\x16\xd8\x4a\x47\x9c\x34\x14\x2b\x2d\x00\x1b\x8e\x98\xe5\x8a\x7a\x1b\x9c\xf3\x2a\x38\x60\x9a\x53\xe5\x84\x62\xa0\x85\x75\x49\x14\x7d\xed\xf7\x07\x17\x93\xbc\x58\xbc\x44\x5f\xfb\xdd\xbb\xcb\xde\x85\x99\x82\x60\x51\xe4\xdd\x78\x16\x27\x5f\x66\xcf\xc5\x64\x66\x20\x2f\x46\xf1\xd8\x4f\xa6\xb1\xa9\x76\x3d\x1b\x7f\xfe\xd5\x6c\xda\x55\x12\x3d\x8f\xea\x06\xff\x11\x8f\xab\xea\xa9\xfc\x27\xcb\x46\xbe\x4a\xeb\xa4\xb4\x1c\x67\xf5\xb3\xb3\x3c\x4a\xe9\x7c\xfe\x55\xeb\x6f\x9e\xb5\xf8\x2a\xad\xcc\x3c\x1d\xfd\xdc\xaa\x6f\xc3\xf7\xed\xb0\x8a\xff\xac\x4e\x12\xff\x1f\x97\x63\x43\xb8\x28\x17\xd3\xb8\xe3\xa2\xca\xcc\xe3\xce\xcb\xcf\xf0\x87\x75\x22\xf7\x14\x9f\xac\xad\xf7\x14\xa7\x6d\xa8\xcc\x73\xd9\xc9\xcd\xb4\x63\x16\xd5\xd8\x17\x55\xee\x4c\x35\x9b\x1f\xb2\x3b\x7b\x0f\x1b\x20\x3b\xfd\x33\x95\x5e\x09\xe7\xd5\x78\x61\x53\x37\x9b\x66\x8f\x0b\xeb\xe7\x85\xaf\x7c\xd9\x29\xf3\x51\x99\xb5\x26\x66\x73\x3f\xf1\xa6\xf4\x65\x06\x5b\xb7\xd9\xf2\x2d\x2b\xed\x55\x86\x6f\xa5\x0c\xb7\x9c\x86\xaf\x9c\x5e\x0f\xf3\xad\xc1\xb2\x6a\xdf\xe9\xc9\x01\xba\xf1\x74\x06\xf1\xdf\x2f\xed\xe1\x6d\x67\x51\x83\x59\x9a\xc9\x21\xfd\xa3\x11\xd4\xd6\xbc\x7b\xb4\x79\x51\x56\xf3\xc5\xd4\x17\x95\xc9\xb6\xd5\x5a\x40\x9e\x16\xdd\x05\xbf\xdf\xe6\xa7\x83\x6f\x15\xbf\x97\x7c\xbe\xb7\xdf\x95\x3d\x43\xc9\x55\x2d\x94\x1a\x83\xb3\x85\x12\x4c\xd2\x47\x55\xa6\xf9\x2c\x3b\x8d\x6e\xb6\xa4\x9b\xe4\xbe\xa8\x3e\x06\xe2\x70\x20\x6f\x40\x7c\xac\xcc\x16\x09\xc7\xe4\x00\xc9\x72\xcb\xe4\x83\x5e\xdc\x53\x23\x3a\xdb\x44\xaf\xff\xe2\x3b\x46\xad\x57\xfe\x31\x2f\xe0\x88\x5e\x63\x8a\x1f\x5e\xea\x75\xec\x0e\x61\xfd\x92\xd6\x57\x76\x47\x12\x26\xd9\x51\xf2\x3a\xe6\xd4\xf0\x9e\xda\xd1\x9f\x61\xb5\x56\x38\x7f\x77\xea\xd5\xe8\x77\x00\x00\x00\xff\xff\xb3\xbe\xf2\x9d\x9b\x08\x00\x00") +var _scriptsPullDepsSh = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x95\x4b\x6f\xe3\x36\x14\x85\xf7\xfa\x15\xac\x66\x76\x85\x24\xbe\x45\x16\xc8\xc2\xcd\x04\xe3\xa0\x93\x18\x88\x93\x6e\x0d\x92\x97\xb4\x84\xd8\xf2\x8c\x25\x3b\x41\x52\xff\xf7\x42\x7e\xc4\x2f\x25\x6d\x56\x82\xc5\x73\xef\xb9\xfa\x78\x48\x7f\xf9\x2d\xb3\x65\x95\x59\x53\x17\x51\x54\xfb\x06\x25\x3e\x8a\xbe\xa0\x3b\xff\x6b\x51\xce\x3d\xa0\xa5\x9f\xd7\xe5\xac\xaa\xa3\xfe\xd5\x8f\x9b\xd1\xdf\x57\x77\xc3\xeb\xc1\xed\x45\xcc\x52\x91\xe2\x78\xf3\x72\xd8\xef\x51\x21\x47\xc3\x87\x9b\x8b\x98\x85\x10\x30\x13\x1c\x44\xb0\x86\xbb\x9c\x79\x0b\x02\xac\xd0\x46\x68\xb0\x39\x0d\xca\x08\x6b\x3d\x21\x24\x37\xd8\x32\x21\x2c\x76\x54\x2b\xe6\xb5\x02\xab\x85\x8d\xa3\xe8\xba\x77\x33\xea\x3d\xdc\xf7\xaf\x6e\xef\xaf\x2f\x7b\xf7\x83\xbb\xbd\x2b\x4e\x45\x4a\xe3\x0e\xc5\xe1\x08\x6d\x7f\xce\x0d\xe4\x41\x02\xa8\xdc\x60\xea\xb1\xe5\x92\x19\x6a\x3c\x68\x49\x88\x62\xd2\x5b\x1a\x38\x86\x3c\x58\xaf\x9c\x20\x39\x97\xd8\x70\xa6\x84\xa4\xa4\x1d\xe1\xaf\x87\x3f\xaf\x2e\xef\x7f\xec\x8d\x97\x24\xa5\xb8\xb5\xde\xad\x0c\xfb\x3d\x41\xe8\xc6\xd0\x73\x41\x98\x03\x90\xc2\x83\x56\x18\xb8\x66\x54\x68\xe7\x72\x6f\x8c\x64\x8e\x13\x11\x74\x2e\x88\x04\x4b\xbd\xe1\x22\x18\xe5\x72\xce\x8c\xf4\x9c\x30\x83\x1d\x98\x40\xb5\x06\xcc\x00\x72\xad\xa9\x92\x2e\x30\x4a\x89\xa2\x36\x68\xa9\x39\xc5\x5c\x29\x6e\x2d\x06\xc0\x98\xe5\x2e\x70\x2e\x34\x05\x08\x46\x78\x41\x02\x51\xac\x1d\xf8\xfa\xf6\xdb\xc1\xb4\x38\x55\x29\x89\x37\x6f\x87\xfd\xde\xe8\x8d\x4d\xae\x88\x63\x60\x79\xae\xad\xc2\x02\x88\x24\x36\x77\xd4\xe5\x86\x11\xa5\x25\x10\x23\x30\xb7\x42\x31\x6f\x83\x73\x5e\x05\x07\x5c\x0b\xa6\x9c\x54\x1c\xb4\xb4\x2e\x8e\xa2\xef\x83\xc1\xf0\x62\x52\x56\x8b\xe7\xe8\xfb\xa0\x77\x77\xd9\xbf\x30\x53\x90\x3c\x8a\xbc\x2b\x66\x28\xfe\x36\x7b\xaa\x26\x33\x03\x65\x35\x46\x85\x9f\x4c\x91\x69\x76\x19\x42\x5f\x5f\x0f\x43\xb4\x8a\xa3\xa7\x71\x1b\xb8\x5f\xa8\x68\x9a\x9f\xf5\x1f\x59\x36\xf6\x4d\xda\x16\xa5\x75\x91\xb5\xcf\x64\x79\x52\x92\x7c\x7d\x6d\xfd\x37\xcf\xd6\x7c\x95\x36\x66\x9e\x8e\x5f\xb6\xee\x5b\xf9\x3e\x0e\x2b\xf4\xb9\x3e\x31\xfa\x07\xd5\x85\xa1\x42\xd6\x8b\x29\x4a\x5c\xd4\x98\x39\x4a\x9e\x5f\xc2\x27\xfb\x44\xee\x27\x3a\x5b\x5b\x7f\x13\x4a\xbb\x50\x99\xa7\x3a\x29\xcd\x34\x31\x8b\xa6\xf0\x55\x53\x3a\xd3\xcc\xe6\xc7\xec\xde\x3d\x17\x07\x20\x93\xc1\x3b\x9d\xde\x08\x97\x4d\xb1\xb0\xa9\x9b\x4d\xb3\xc7\x85\xf5\xf3\xca\x37\xbe\x4e\xea\x72\x5c\x67\x9d\x85\xd9\xdc\x4f\xbc\xa9\x7d\x9d\xc1\x76\xda\x6c\xf9\xd1\x28\xdd\x5d\x46\x1f\x95\x8c\xb6\x9c\x46\x6f\x9c\xde\x36\xf3\xa3\x83\xbe\xea\xfe\xd2\xb3\x0d\x74\xc5\x74\x06\xe8\xf7\xe7\x6e\x79\xd7\x5e\xb4\x60\x5c\x33\x39\xa6\x7f\x72\x25\x74\x84\x17\x26\xe9\xa3\xaa\xd3\x72\x96\x9d\xab\x0f\x61\xbb\x49\xe9\xab\xe6\xbf\x93\x7c\x7e\xd5\xac\xd0\xff\x6f\xb3\xe5\x20\x08\x3d\x0a\xf2\xf2\x25\x7c\xa2\x49\x9b\xe2\xbd\x3a\xdb\xa8\xd7\xff\x17\x3b\x46\x9d\x61\x7e\x2c\x2b\x38\xa1\x77\x70\x3f\x1d\xc7\x75\xad\xdd\x21\x6c\x7f\xa4\x6d\x18\x77\x24\x61\x92\x9d\x14\xaf\x35\xe7\x03\xef\xa9\x9d\xdc\x79\xab\xb5\xc3\xfb\xa9\x68\x57\xa3\x7f\x03\x00\x00\xff\xff\xda\x48\x64\x3b\x05\x07\x00\x00") func scriptsPullDepsShBytes() ([]byte, error) { return bindataRead( @@ -316,8 +316,8 @@ func scriptsPullDepsSh() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "scripts/pull-deps.sh", size: 2203, mode: os.FileMode(0755), modTime: time.Unix(1611616180, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x82, 0x78, 0xdc, 0x27, 0x89, 0x9f, 0x3, 0x59, 0x10, 0xa2, 0xed, 0x2, 0x99, 0x9b, 0xa0, 0x37, 0x1a, 0xe0, 0x48, 0xde, 0x41, 0x99, 0xbf, 0x73, 0xf6, 0x7f, 0x94, 0xd8, 0xf8, 0xc, 0xad, 0xc0}} + info := bindataFileInfo{name: "scripts/pull-deps.sh", size: 1797, mode: os.FileMode(0755), modTime: time.Unix(1618002872, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc, 0x3a, 0x72, 0x9, 0x16, 0xea, 0x9e, 0xdc, 0xd4, 0x72, 0x53, 0xa8, 0x31, 0x29, 0x9b, 0xcc, 0x8a, 0xeb, 0x65, 0x26, 0x1b, 0x28, 0xbd, 0x4e, 0x3d, 0x14, 0x43, 0x3d, 0x5a, 0xc1, 0x48, 0x72}} return a, nil } diff --git a/pkg/store/leaderelection/leaderelection.go b/pkg/store/leaderelection/leaderelection.go index d88cf94..e26f266 100644 --- a/pkg/store/leaderelection/leaderelection.go +++ b/pkg/store/leaderelection/leaderelection.go @@ -65,7 +65,8 @@ import ( ) const ( - JitterFactor = 1.2 + JitterFactor = 1.2 + releaseTimeout = 10 * time.Second ) // NewLeaderElector creates a LeaderElector from a LeaderElectionConfig @@ -240,7 +241,8 @@ func (le *LeaderElector) acquire(ctx context.Context) bool { return succeeded } -// renew loops calling tryAcquireOrRenew and returns immediately when tryAcquireOrRenew fails or ctx signals done. +// renew loops calling tryAcquireOrRenew and returns immediately when tryAcquireOrRenew fails or +// ctx signals done. func (le *LeaderElector) renew(ctx context.Context) { ctx, cancel := context.WithCancel(ctx) defer cancel() @@ -264,7 +266,14 @@ func (le *LeaderElector) renew(ctx context.Context) { // if we hold the lease, give it up if le.config.ReleaseOnCancel { - le.release(ctx) + // Use the background context, not the one that was passed in originally. If + // the latter was cancelled, then we can't actually do the release. + releaseCtx, releaseCancel := context.WithTimeout( + context.Background(), + releaseTimeout, + ) + defer releaseCancel() + le.release(releaseCtx) } } @@ -332,7 +341,10 @@ func (le *LeaderElector) tryAcquireOrRenew(ctx context.Context) bool { le.observedTime.Add(le.config.LeaseDuration).After(now.Time) && oldLeaderElectionRecord.RenewTime.Time.After(thresholdTime) && !le.IsLeader() { - log.Infof("Lock is held by %v and has not yet expired", oldLeaderElectionRecord.HolderIdentity) + log.Infof( + "Lock is held by %v and has not yet expired", + oldLeaderElectionRecord.HolderIdentity, + ) return false } diff --git a/pkg/store/lock.go b/pkg/store/lock.go index 1d2c543..28e8fd7 100644 --- a/pkg/store/lock.go +++ b/pkg/store/lock.go @@ -19,6 +19,10 @@ import ( "k8s.io/client-go/tools/leaderelection/resourcelock" ) +const ( + kubeLockerReleaseTimeout = 10 * time.Second +) + // Locker is an interface for structs that can acquire and release locks. type Locker interface { // Acquire acquires the lock with the provided name. @@ -78,6 +82,7 @@ type KubeLocker struct { namespace string objLock sync.Mutex lockCancellations map[string]context.CancelFunc + lockCompletions map[string]chan struct{} coordinationClient coordv1.CoordinationV1Interface } @@ -102,6 +107,7 @@ func NewKubeLocker( id: id, namespace: namespace, lockCancellations: map[string]context.CancelFunc{}, + lockCompletions: map[string]chan struct{}{}, coordinationClient: coordinationClient, }, nil } @@ -117,6 +123,7 @@ func (k *KubeLocker) Acquire(ctx context.Context, name string) error { // Create a separate context for the lock itself lockCtx, lockCancel := context.WithCancel(context.Background()) k.lockCancellations[name] = lockCancel + k.lockCompletions[name] = make(chan struct{}, 1) k.objLock.Unlock() leaseName := fmt.Sprintf("kubeapply-lock-%s", name) @@ -147,7 +154,10 @@ func (k *KubeLocker) Acquire(ctx context.Context, name string) error { acquired <- struct{}{} }, OnStoppedLeading: func() { + k.objLock.Lock() + defer k.objLock.Unlock() log.Warn("Lock lost") + k.lockCompletions[name] <- struct{}{} }, }, }, @@ -172,6 +182,32 @@ func (k *KubeLocker) Acquire(ctx context.Context, name string) error { // Release releases the lock with the argument name. func (k *KubeLocker) Release(name string) error { + // Do this in a separate function to prevent deadlock on k.objLock. + if err := k.releaseHelper(name); err != nil { + return err + } + + log.Infof("Waiting for lock to be released") + releaseCtx, releaseCancel := context.WithTimeout( + context.Background(), + kubeLockerReleaseTimeout, + ) + defer releaseCancel() + + k.objLock.Lock() + completionChan := k.lockCompletions[name] + k.objLock.Unlock() + + select { + case <-completionChan: + delete(k.lockCompletions, name) + return nil + case <-releaseCtx.Done(): + return releaseCtx.Err() + } +} + +func (k *KubeLocker) releaseHelper(name string) error { k.objLock.Lock() defer k.objLock.Unlock() @@ -184,6 +220,5 @@ func (k *KubeLocker) Release(name string) error { cancel() delete(k.lockCancellations, name) - return nil } diff --git a/pkg/validation/format.go b/pkg/validation/format.go new file mode 100644 index 0000000..b13adcb --- /dev/null +++ b/pkg/validation/format.go @@ -0,0 +1,131 @@ +package validation + +import ( + "bytes" + "fmt" + "path/filepath" + "strings" + + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" +) + +func ResultTable( + result ResourceResult, + clusterName string, + baseDir string, + verbose bool, +) string { + buf := &bytes.Buffer{} + + table := tablewriter.NewWriter(buf) + table.SetHeader( + []string{ + "Property", + "Value", + }, + ) + table.SetAutoWrapText(false) + table.SetColumnAlignment( + []int{ + tablewriter.ALIGN_RIGHT, + tablewriter.ALIGN_LEFT, + }, + ) + table.SetBorders( + tablewriter.Border{ + Left: false, + Top: true, + Right: false, + Bottom: true, + }, + ) + + if clusterName != "" { + table.Append( + []string{ + "cluster", + clusterName, + }, + ) + } + + var displayPath string + + relPath, err := filepath.Rel(baseDir, result.Resource.Path) + if err != nil { + displayPath = result.Resource.Path + } else { + displayPath = relPath + } + + table.Append( + []string{ + "path", + displayPath, + }, + ) + table.Append( + []string{ + "resource", + result.Resource.PrettyName(), + }, + ) + + errorPrinter := color.New(color.FgHiRed).SprintfFunc() + warnPrinter := color.New(color.FgHiYellow).SprintfFunc() + standardPrinter := fmt.Sprintf + + for _, checkResult := range result.CheckResults { + if !verbose && (checkResult.Status != StatusError && + checkResult.Status != StatusInvalid && + checkResult.Status != StatusWarning) { + continue + } + + reasonLines := []string{checkResult.Message} + for r, reason := range checkResult.Reasons { + reasonLines = append(reasonLines, fmt.Sprintf("(%d) %s", r+1, reason)) + } + + table.Append( + []string{ + "checkType", + string(checkResult.CheckType), + }, + ) + table.Append( + []string{ + "checkName", + checkResult.CheckName, + }, + ) + + var printer func(f string, a ...interface{}) string + switch checkResult.Status { + case StatusError, StatusInvalid: + printer = errorPrinter + case StatusWarning: + printer = warnPrinter + default: + printer = standardPrinter + } + + table.Append( + []string{ + "checkStatus", + printer(string(checkResult.Status)), + }, + ) + + table.Append( + []string{ + "checkMessage", + strings.Join(reasonLines, "\n"), + }, + ) + } + + table.Render() + return string(bytes.TrimRight(buf.Bytes(), "\n")) +} diff --git a/pkg/validation/policy.go b/pkg/validation/policy.go index 096636e..b0bdd02 100644 --- a/pkg/validation/policy.go +++ b/pkg/validation/policy.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "path/filepath" "reflect" + "strings" "github.com/ghodss/yaml" "github.com/open-policy-agent/opa/rego" @@ -14,6 +15,7 @@ import ( const ( defaultPackage = "com.segment.kubeapply" defaultResult = "deny" + warnPrefix = "warn:" ) // Policy wraps a policy module and a prepared query. @@ -155,12 +157,53 @@ func (p *PolicyChecker) Check(ctx context.Context, resource Resource) CheckResul result.Status = StatusValid result.Message = "Policy returned 0 deny reasons" } else { - result.Status = StatusInvalid - result.Message = fmt.Sprintf( - "Policy returned %d deny reason(s): %+v", - len(value), - value, - ) + invalidReasons := []string{} + warnReasons := []string{} + + for _, subValue := range value { + subValueStr := fmt.Sprintf("%v", subValue) + + if strings.HasPrefix( + strings.ToLower(subValueStr), + warnPrefix, + ) { + // Treat this as a warning + warnReasons = append( + warnReasons, + subValueStr, + ) + } else { + // Treat this as a denial + invalidReasons = append( + invalidReasons, + subValueStr, + ) + } + } + + if len(invalidReasons) == 0 { + result.Status = StatusWarning + result.Message = fmt.Sprintf( + "Policy returned %d warn reason(s)", + len(warnReasons), + ) + result.Reasons = warnReasons + } else if len(warnReasons) == 0 { + result.Status = StatusInvalid + result.Message = fmt.Sprintf( + "Policy returned %d deny reason(s)", + len(invalidReasons), + ) + result.Reasons = invalidReasons + } else { + result.Status = StatusInvalid + result.Message = fmt.Sprintf( + "Policy returned %d deny reason(s) and %d warn reason(s)", + len(invalidReasons), + len(warnReasons), + ) + result.Reasons = append(invalidReasons, warnReasons...) + } } default: result.Status = StatusError diff --git a/pkg/validation/policy_test.go b/pkg/validation/policy_test.go index 5d9f4bf..aef4dcb 100644 --- a/pkg/validation/policy_test.go +++ b/pkg/validation/policy_test.go @@ -10,16 +10,25 @@ import ( const ( denyPolicyStr = ` -package example +package com.segment.kubeapply deny[msg] { input.apiVersion == "badVersion" - input.extraKey == "extraBadValue" msg = "Cannot have bad api version" +} + +deny[msg] { + input.extraKey == "extraBadValue" + msg = "Cannot have bad extra key" +} + +deny[msg] { + input.extraKey2 == "warnValue" + msg = "WARN: Cannot have warn value" }` allowPolicyStr = ` -package example +package com.segment.kubeapply default allow = true @@ -54,11 +63,8 @@ func TestPolicyChecker(t *testing.T) { policyModule: PolicyModule{ Name: "testDenyPolicy", Contents: denyPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", Result: "deny", - ExtraFields: map[string]interface{}{ - "extraKey": "extraBadValue", - }, }, resource: MakeResource("test/path", []byte(goodVersionResourceStr), 0), expected: CheckResult{ @@ -72,7 +78,7 @@ func TestPolicyChecker(t *testing.T) { policyModule: PolicyModule{ Name: "testDenyPolicy", Contents: denyPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", Result: "deny", ExtraFields: map[string]interface{}{ "extraKey": "goodValue", @@ -82,15 +88,39 @@ func TestPolicyChecker(t *testing.T) { expected: CheckResult{ CheckType: CheckTypeOPA, CheckName: "testDenyPolicy", - Status: StatusValid, - Message: "Policy returned 0 deny reasons", + Status: StatusInvalid, + Message: "Policy returned 1 deny reason(s)", + Reasons: []string{ + "Cannot have bad api version", + }, }, }, { policyModule: PolicyModule{ Name: "testDenyPolicy", Contents: denyPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", + Result: "deny", + ExtraFields: map[string]interface{}{ + "extraKey2": "warnValue", + }, + }, + resource: MakeResource("test/path", []byte(goodVersionResourceStr), 0), + expected: CheckResult{ + CheckType: CheckTypeOPA, + CheckName: "testDenyPolicy", + Status: StatusWarning, + Message: "Policy returned 1 warn reason(s)", + Reasons: []string{ + "WARN: Cannot have warn value", + }, + }, + }, + { + policyModule: PolicyModule{ + Name: "testDenyPolicy", + Contents: denyPolicyStr, + Package: "com.segment.kubeapply", Result: "deny", ExtraFields: map[string]interface{}{ "extraKey": "extraBadValue", @@ -101,14 +131,42 @@ func TestPolicyChecker(t *testing.T) { CheckType: CheckTypeOPA, CheckName: "testDenyPolicy", Status: StatusInvalid, - Message: "Policy returned 1 deny reason(s): [Cannot have bad api version]", + Message: "Policy returned 2 deny reason(s)", + Reasons: []string{ + "Cannot have bad extra key", + "Cannot have bad api version", + }, + }, + }, + { + policyModule: PolicyModule{ + Name: "testDenyPolicy", + Contents: denyPolicyStr, + Package: "com.segment.kubeapply", + Result: "deny", + ExtraFields: map[string]interface{}{ + "extraKey": "extraBadValue", + "extraKey2": "warnValue", + }, + }, + resource: MakeResource("test/path", []byte(badVersionResourceStr), 0), + expected: CheckResult{ + CheckType: CheckTypeOPA, + CheckName: "testDenyPolicy", + Status: StatusInvalid, + Message: "Policy returned 2 deny reason(s) and 1 warn reason(s)", + Reasons: []string{ + "Cannot have bad extra key", + "Cannot have bad api version", + "WARN: Cannot have warn value", + }, }, }, { policyModule: PolicyModule{ Name: "testAllowPolicy", Contents: allowPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", Result: "allow", }, resource: MakeResource("test/path", []byte(goodVersionResourceStr), 0), @@ -123,7 +181,7 @@ func TestPolicyChecker(t *testing.T) { policyModule: PolicyModule{ Name: "testAllowPolicy", Contents: allowPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", Result: "allow", }, resource: MakeResource("test/path", []byte(badVersionResourceStr), 0), @@ -138,7 +196,7 @@ func TestPolicyChecker(t *testing.T) { policyModule: PolicyModule{ Name: "testAllowPolicy", Contents: allowPolicyStr, - Package: "example", + Package: "com.segment.kubeapply", Result: "allow", }, resource: MakeResource("test/path", []byte(""), 0), diff --git a/pkg/validation/result.go b/pkg/validation/result.go index 296ce08..4dcee9e 100644 --- a/pkg/validation/result.go +++ b/pkg/validation/result.go @@ -6,6 +6,7 @@ type Status string const ( StatusValid Status = "valid" StatusInvalid Status = "invalid" + StatusWarning Status = "warning" StatusError Status = "error" StatusSkipped Status = "skipped" StatusEmpty Status = "empty" @@ -20,7 +21,7 @@ const ( ) // Result stores the results of validating a single resource in a single file, for all checks. -type Result struct { +type ResourceResult struct { Resource Resource CheckResults []CheckResult } @@ -31,4 +32,45 @@ type CheckResult struct { CheckName string Status Status Message string + Reasons []string +} + +// HasIssues returns whether a ResourceResult has at least one check result with an +// error or warning. +func (r ResourceResult) HasIssues() bool { + for _, checkResult := range r.CheckResults { + if checkResult.Status == StatusError || checkResult.Status == StatusInvalid || + checkResult.Status == StatusWarning { + return true + } + } + + return false +} + +// CountsByStatus returns the number of check results for each status type. +func CountsByStatus(results []ResourceResult) map[Status]int { + counts := map[Status]int{} + + for _, result := range results { + for _, checkResult := range result.CheckResults { + counts[checkResult.Status]++ + } + } + + return counts +} + +// ResultsWithIssues filters the argument resource results to just those with potential +// issues. +func ResultsWithIssues(results []ResourceResult) []ResourceResult { + filteredResults := []ResourceResult{} + + for _, result := range results { + if result.HasIssues() { + filteredResults = append(filteredResults, result) + } + } + + return filteredResults } diff --git a/pkg/validation/validation.go b/pkg/validation/validation.go index cddb5ff..96a082e 100644 --- a/pkg/validation/validation.go +++ b/pkg/validation/validation.go @@ -37,7 +37,7 @@ func NewKubeValidator(config KubeValidatorConfig) *KubeValidator { func (k *KubeValidator) RunChecks( ctx context.Context, path string, -) ([]Result, error) { +) ([]ResourceResult, error) { resources := []Resource{} index := 0 @@ -87,12 +87,12 @@ func (k *KubeValidator) RunChecks( } defer close(resourcesChan) - resultsChan := make(chan Result, len(resources)) + resultsChan := make(chan ResourceResult, len(resources)) for i := 0; i < k.config.NumWorkers; i++ { go func() { for resource := range resourcesChan { - result := Result{ + result := ResourceResult{ Resource: resource, } for _, checker := range k.config.Checkers { @@ -107,7 +107,7 @@ func (k *KubeValidator) RunChecks( }() } - results := []Result{} + results := []ResourceResult{} for i := 0; i < len(resources); i++ { results = append(results, <-resultsChan) } diff --git a/pkg/validation/validation_test.go b/pkg/validation/validation_test.go index 8a1944f..303300a 100644 --- a/pkg/validation/validation_test.go +++ b/pkg/validation/validation_test.go @@ -23,13 +23,13 @@ func TestKubeValidator(t *testing.T) { type testCase struct { path string - expected []Result + expected []ResourceResult } testCases := []testCase{ { path: "testdata/configs", - expected: []Result{ + expected: []ResourceResult{ { Resource: Resource{ Path: "testdata/configs/deployment.yaml", diff --git a/pkg/version/version.go b/pkg/version/version.go index 7a744c3..a8419b9 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,4 +1,4 @@ package version // Version stores the current kubeapply version. -const Version = "0.0.30" +const Version = "0.0.31"