diff --git a/.gitignore b/.gitignore index ea7d50c0..1b2f292e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ .tools .gopath json-kyverno -.terraform/ -.terraform.lock.hcl -tf.plan -tf.plan.json diff --git a/README.md b/README.md index fbf89c4e..d2cc9e98 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,9 @@ make build ## invoke json-kyverno ```console -./json-kyverno --payload ./tf.plan.json --pre-process "planned_values.root_module.resources" --policy ./policy.yaml +# with json payload +./json-kyverno --payload ./testdata/tf-plan/tf.plan.json --pre-process "planned_values.root_module.resources" --policy ./testdata/tf-plan/policy.yaml + +# with yaml payload +./json-kyverno --payload ./testdata/payload-yaml/payload.yaml --pre-process "planned_values.root_module.resources" --policy ./testdata/payload-yaml/policy.yaml ``` diff --git a/go.mod b/go.mod index f87de541..c05be03a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/kyverno/kyverno v1.5.0-rc1.0.20230927190803-27858f634e28 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 + gopkg.in/yaml.v3 v3.0.1 k8s.io/apimachinery v0.28.2 sigs.k8s.io/kubectl-validate v0.0.0-20230927155409-3b3ca3ad91d0 ) @@ -282,7 +283,6 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect k8s.io/api v0.28.2 // indirect k8s.io/apiextensions-apiserver v0.28.1 // indirect diff --git a/pkg/commands/root.go b/pkg/commands/root.go index e71d543e..3bc254bd 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -4,10 +4,10 @@ import ( "errors" "fmt" + "github.com/eddycharly/json-kyverno/pkg/engine/template" jsonengine "github.com/eddycharly/json-kyverno/pkg/json-engine" "github.com/eddycharly/json-kyverno/pkg/payload" "github.com/eddycharly/json-kyverno/pkg/policy" - "github.com/eddycharly/json-kyverno/pkg/template" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/pluralize" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -26,7 +26,7 @@ func (c *command) Run(cmd *cobra.Command, _ []string) error { if err != nil { return err } - fmt.Fprintln(out, "Loading plan ...") + fmt.Fprintln(out, "Loading payload ...") payload, err := payload.Load(c.payload) if err != nil { return err @@ -79,7 +79,7 @@ func NewRootCommand() *cobra.Command { RunE: command.Run, SilenceUsage: true, } - cmd.Flags().StringVar(&command.payload, "payload", "", "Path to json payload") + cmd.Flags().StringVar(&command.payload, "payload", "", "Path to payload (json or yaml file)") cmd.Flags().StringSliceVar(&command.preprocessors, "pre-process", nil, "JmesPath expression used to pre process payload") cmd.Flags().StringSliceVar(&command.policies, "policy", nil, "Path to json-kyverno policies") return cmd diff --git a/pkg/commands/root_test.go b/pkg/commands/root_test.go index 0a4ad1eb..bf797808 100644 --- a/pkg/commands/root_test.go +++ b/pkg/commands/root_test.go @@ -6,16 +6,31 @@ import ( "github.com/stretchr/testify/assert" ) -func TestCommand(t *testing.T) { +func Test_TfPlan(t *testing.T) { cmd := NewRootCommand() assert.NotNil(t, cmd) cmd.SetArgs([]string{ "--payload", - "../../tf.plan.json", + "../../testdata/tf-plan/tf.plan.json", "--pre-process", "planned_values.root_module.resources", "--policy", - "../../policy.yaml", + "../../testdata/tf-plan/policy.yaml", + }) + err := cmd.Execute() + assert.NoError(t, err) +} + +func Test_PayloadYaml(t *testing.T) { + cmd := NewRootCommand() + assert.NotNil(t, cmd) + cmd.SetArgs([]string{ + "--payload", + "../../testdata/payload-yaml/payload.yaml", + "--pre-process", + "planned_values.root_module.resources", + "--policy", + "../../testdata/payload-yaml/policy.yaml", }) err := cmd.Execute() assert.NoError(t, err) diff --git a/pkg/match/match.go b/pkg/engine/match/match.go similarity index 100% rename from pkg/match/match.go rename to pkg/engine/match/match.go diff --git a/pkg/match/options.go b/pkg/engine/match/options.go similarity index 92% rename from pkg/match/options.go rename to pkg/engine/match/options.go index 4ab18d42..98914162 100644 --- a/pkg/match/options.go +++ b/pkg/engine/match/options.go @@ -1,7 +1,7 @@ package match import ( - "github.com/eddycharly/json-kyverno/pkg/template" + "github.com/eddycharly/json-kyverno/pkg/engine/template" ) type matchOptions struct { diff --git a/pkg/template/template.go b/pkg/engine/template/template.go similarity index 100% rename from pkg/template/template.go rename to pkg/engine/template/template.go diff --git a/pkg/json-engine/engine.go b/pkg/json-engine/engine.go index 088e347c..356e3d8e 100644 --- a/pkg/json-engine/engine.go +++ b/pkg/json-engine/engine.go @@ -7,8 +7,8 @@ import ( "github.com/eddycharly/json-kyverno/pkg/engine" "github.com/eddycharly/json-kyverno/pkg/engine/blocks/loop" "github.com/eddycharly/json-kyverno/pkg/engine/builder" - "github.com/eddycharly/json-kyverno/pkg/match" - "github.com/eddycharly/json-kyverno/pkg/template" + "github.com/eddycharly/json-kyverno/pkg/engine/match" + "github.com/eddycharly/json-kyverno/pkg/engine/template" ) type JsonEngineRequest struct { diff --git a/pkg/payload/load.go b/pkg/payload/load.go index 85160433..9821fe7b 100644 --- a/pkg/payload/load.go +++ b/pkg/payload/load.go @@ -2,8 +2,13 @@ package payload import ( "encoding/json" + "fmt" "os" "path/filepath" + + "github.com/eddycharly/json-kyverno/pkg/utils/file" + yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml" + "gopkg.in/yaml.v3" ) func Load(path string) (interface{}, error) { @@ -12,8 +17,31 @@ func Load(path string) (interface{}, error) { return nil, err } var payload interface{} - if err := json.Unmarshal(content, &payload); err != nil { - return nil, err + switch { + case file.IsJson(path): + if err := json.Unmarshal(content, &payload); err != nil { + return nil, err + } + case file.IsYaml(path): + documents, err := yamlutils.SplitDocuments(content) + if err != nil { + return nil, err + } + var objects []interface{} + for _, document := range documents { + var object map[string]interface{} + if err := yaml.Unmarshal(document, &object); err != nil { + return nil, err + } + objects = append(objects, object) + } + if len(objects) == 1 { + payload = objects[0] + } else { + payload = objects + } + default: + return nil, fmt.Errorf("unrecognized payload format, must be yaml or json (%s)", path) } return payload, nil } diff --git a/pkg/policy/load.go b/pkg/policy/load.go index 3b2f0bf0..19254e6c 100644 --- a/pkg/policy/load.go +++ b/pkg/policy/load.go @@ -8,6 +8,7 @@ import ( "github.com/eddycharly/json-kyverno/pkg/apis/v1alpha1" "github.com/eddycharly/json-kyverno/pkg/data" + fileinfo "github.com/eddycharly/json-kyverno/pkg/utils/file-info" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/resource/convert" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/resource/loader" yamlutils "github.com/kyverno/kyverno/pkg/utils/yaml" @@ -20,14 +21,6 @@ var ( policy_v1alpha1 = gv_v1alpha1.WithKind("Policy") ) -func IsYaml(file fs.FileInfo) bool { - if file.IsDir() { - return false - } - ext := filepath.Ext(file.Name()) - return ext == ".yml" || ext == ".yaml" -} - func Load(path ...string) ([]*v1alpha1.Policy, error) { var policies []*v1alpha1.Policy for _, path := range path { @@ -46,7 +39,7 @@ func load(path string) ([]*v1alpha1.Policy, error) { if err != nil { return err } - if IsYaml(info) { + if fileinfo.IsYaml(info) { files = append(files, file) } return nil diff --git a/pkg/utils/file-info/ext.go b/pkg/utils/file-info/ext.go new file mode 100644 index 00000000..fb0beb98 --- /dev/null +++ b/pkg/utils/file-info/ext.go @@ -0,0 +1,28 @@ +package fileinfo + +import ( + "io/fs" + + "github.com/eddycharly/json-kyverno/pkg/utils/file" +) + +func IsYaml(info fs.FileInfo) bool { + if info.IsDir() { + return false + } + return file.IsYaml(info.Name()) +} + +func IsJson(info fs.FileInfo) bool { + if info.IsDir() { + return false + } + return file.IsJson(info.Name()) +} + +func IsYamlOrJson(info fs.FileInfo) bool { + if info.IsDir() { + return false + } + return file.IsYamlOrJson(info.Name()) +} diff --git a/pkg/utils/file/ext.go b/pkg/utils/file/ext.go new file mode 100644 index 00000000..9347506a --- /dev/null +++ b/pkg/utils/file/ext.go @@ -0,0 +1,19 @@ +package file + +import ( + "path/filepath" +) + +func IsYaml(path string) bool { + ext := filepath.Ext(path) + return ext == ".yml" || ext == ".yaml" +} + +func IsJson(path string) bool { + ext := filepath.Ext(path) + return ext == ".json" +} + +func IsYamlOrJson(path string) bool { + return IsYaml(path) || IsJson(path) +} diff --git a/s3.tf b/s3.tf deleted file mode 100644 index 87029852..00000000 --- a/s3.tf +++ /dev/null @@ -1,18 +0,0 @@ -provider "aws" { - region = "eu-west-1" - skip_credentials_validation = true - skip_requesting_account_id = true - skip_metadata_api_check = true - access_key = "mock_access_key" - secret_key = "mock_secret_key" -} - -resource "aws_s3_bucket" "example" { - bucket = "my-tf-test-bucket" - - tags = { - Name = "My bucket" - Environment = "Dev" - Team = "Kyverno" - } -} diff --git a/testdata/payload-yaml/payload.yaml b/testdata/payload-yaml/payload.yaml new file mode 100644 index 00000000..d6233532 --- /dev/null +++ b/testdata/payload-yaml/payload.yaml @@ -0,0 +1,130 @@ +format_version: '1.1' +terraform_version: 1.4.6 +planned_values: + root_module: + resources: + - address: aws_s3_bucket.example + mode: managed + type: aws_s3_bucket + name: example + provider_name: registry.terraform.io/hashicorp/aws + schema_version: 0 + values: + bucket: my-tf-test-bucket + force_destroy: false + tags: + Environment: Dev + Name: My bucket + Team: Kyverno + tags_all: + Environment: Dev + Name: My bucket + Team: Kyverno + timeouts: + sensitive_values: + cors_rule: [] + grant: [] + lifecycle_rule: [] + logging: [] + object_lock_configuration: [] + replication_configuration: [] + server_side_encryption_configuration: [] + tags: {} + tags_all: {} + versioning: [] + website: [] +resource_changes: +- address: aws_s3_bucket.example + mode: managed + type: aws_s3_bucket + name: example + provider_name: registry.terraform.io/hashicorp/aws + change: + actions: + - create + before: + after: + bucket: my-tf-test-bucket + force_destroy: false + tags: + Environment: Dev + Name: My bucket + Team: Kyverno + tags_all: + Environment: Dev + Name: My bucket + Team: Kyverno + timeouts: + after_unknown: + acceleration_status: true + acl: true + arn: true + bucket_domain_name: true + bucket_prefix: true + bucket_regional_domain_name: true + cors_rule: true + grant: true + hosted_zone_id: true + id: true + lifecycle_rule: true + logging: true + object_lock_configuration: true + object_lock_enabled: true + policy: true + region: true + replication_configuration: true + request_payer: true + server_side_encryption_configuration: true + tags: {} + tags_all: {} + versioning: true + website: true + website_domain: true + website_endpoint: true + before_sensitive: false + after_sensitive: + cors_rule: [] + grant: [] + lifecycle_rule: [] + logging: [] + object_lock_configuration: [] + replication_configuration: [] + server_side_encryption_configuration: [] + tags: {} + tags_all: {} + versioning: [] + website: [] +configuration: + provider_config: + aws: + name: aws + full_name: registry.terraform.io/hashicorp/aws + expressions: + access_key: + constant_value: mock_access_key + region: + constant_value: eu-west-1 + secret_key: + constant_value: mock_secret_key + skip_credentials_validation: + constant_value: true + skip_metadata_api_check: + constant_value: true + skip_requesting_account_id: + constant_value: true + root_module: + resources: + - address: aws_s3_bucket.example + mode: managed + type: aws_s3_bucket + name: example + provider_config_key: aws + expressions: + bucket: + constant_value: my-tf-test-bucket + tags: + constant_value: + Environment: Dev + Name: My bucket + Team: Kyverno + schema_version: 0 diff --git a/policy.yaml b/testdata/payload-yaml/policy.yaml similarity index 94% rename from policy.yaml rename to testdata/payload-yaml/policy.yaml index 4725e287..a4833e29 100644 --- a/policy.yaml +++ b/testdata/payload-yaml/policy.yaml @@ -13,7 +13,7 @@ spec: - name: tags variable: value: - team: Kyverno + Team: Kyverno validate: message: Bucket `{{ resource.name }}` ({{ resource.address }}) does not have the required tags {{ to_string($tags) }} pattern: diff --git a/testdata/tf-plan/policy.yaml b/testdata/tf-plan/policy.yaml new file mode 100644 index 00000000..a4833e29 --- /dev/null +++ b/testdata/tf-plan/policy.yaml @@ -0,0 +1,21 @@ +apiVersion: json.kyverno.io/v1alpha1 +kind: Policy +metadata: + name: required-s3-tags +spec: + rules: + - name: require-team-tag + match: + any: + - resource: + type: aws_s3_bucket + context: + - name: tags + variable: + value: + Team: Kyverno + validate: + message: Bucket `{{ resource.name }}` ({{ resource.address }}) does not have the required tags {{ to_string($tags) }} + pattern: + values: + tags: '{{ $tags }}' diff --git a/testdata/tf-plan/tf.plan.json b/testdata/tf-plan/tf.plan.json new file mode 100644 index 00000000..cc9b8cfb --- /dev/null +++ b/testdata/tf-plan/tf.plan.json @@ -0,0 +1,169 @@ +{ + "format_version": "1.1", + "terraform_version": "1.4.6", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "aws_s3_bucket.example", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 0, + "values": { + "bucket": "my-tf-test-bucket", + "force_destroy": false, + "tags": { + "Environment": "Dev", + "Name": "My bucket", + "Team": "Kyverno" + }, + "tags_all": { + "Environment": "Dev", + "Name": "My bucket", + "Team": "Kyverno" + }, + "timeouts": null + }, + "sensitive_values": { + "cors_rule": [], + "grant": [], + "lifecycle_rule": [], + "logging": [], + "object_lock_configuration": [], + "replication_configuration": [], + "server_side_encryption_configuration": [], + "tags": {}, + "tags_all": {}, + "versioning": [], + "website": [] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "aws_s3_bucket.example", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "bucket": "my-tf-test-bucket", + "force_destroy": false, + "tags": { + "Environment": "Dev", + "Name": "My bucket", + "Team": "Kyverno" + }, + "tags_all": { + "Environment": "Dev", + "Name": "My bucket", + "Team": "Kyverno" + }, + "timeouts": null + }, + "after_unknown": { + "acceleration_status": true, + "acl": true, + "arn": true, + "bucket_domain_name": true, + "bucket_prefix": true, + "bucket_regional_domain_name": true, + "cors_rule": true, + "grant": true, + "hosted_zone_id": true, + "id": true, + "lifecycle_rule": true, + "logging": true, + "object_lock_configuration": true, + "object_lock_enabled": true, + "policy": true, + "region": true, + "replication_configuration": true, + "request_payer": true, + "server_side_encryption_configuration": true, + "tags": {}, + "tags_all": {}, + "versioning": true, + "website": true, + "website_domain": true, + "website_endpoint": true + }, + "before_sensitive": false, + "after_sensitive": { + "cors_rule": [], + "grant": [], + "lifecycle_rule": [], + "logging": [], + "object_lock_configuration": [], + "replication_configuration": [], + "server_side_encryption_configuration": [], + "tags": {}, + "tags_all": {}, + "versioning": [], + "website": [] + } + } + } + ], + "configuration": { + "provider_config": { + "aws": { + "name": "aws", + "full_name": "registry.terraform.io/hashicorp/aws", + "expressions": { + "access_key": { + "constant_value": "mock_access_key" + }, + "region": { + "constant_value": "eu-west-1" + }, + "secret_key": { + "constant_value": "mock_secret_key" + }, + "skip_credentials_validation": { + "constant_value": true + }, + "skip_metadata_api_check": { + "constant_value": true + }, + "skip_requesting_account_id": { + "constant_value": true + } + } + } + }, + "root_module": { + "resources": [ + { + "address": "aws_s3_bucket.example", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "example", + "provider_config_key": "aws", + "expressions": { + "bucket": { + "constant_value": "my-tf-test-bucket" + }, + "tags": { + "constant_value": { + "Environment": "Dev", + "Name": "My bucket", + "Team": "Kyverno" + } + } + }, + "schema_version": 0 + } + ] + } + } +} \ No newline at end of file