From ce02d62d61dfa2cbf0912710285ec8d599588d3a Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Thu, 2 Feb 2023 17:48:18 -0800 Subject: [PATCH 1/6] Add multi-engine support Signed-off-by: Max Smythe --- README.md | 4 - constraint/Makefile | 5 +- ...tes.gatekeeper.sh_constrainttemplates.yaml | 60 + constraint/deploy/crds.yaml | 51 + .../templates/v1/constrainttemplate_types.go | 20 + .../v1/constrainttemplate_types_test.go | 491 +++++++- .../pkg/apis/templates/v1/conversion.go | 41 + .../pkg/apis/templates/v1/helpers_test.go | 11 + .../templates/v1/zz_generated.conversion.go | 79 +- .../templates/v1/zz_generated.deepcopy.go | 26 + .../v1alpha1/constrainttemplate_types.go | 19 + .../v1alpha1/constrainttemplate_types_test.go | 496 +++++++- .../pkg/apis/templates/v1alpha1/conversion.go | 41 + .../apis/templates/v1alpha1/helpers_test.go | 11 + .../v1alpha1/zz_generated.conversion.go | 79 +- .../v1alpha1/zz_generated.deepcopy.go | 26 + .../v1beta1/constrainttemplate_types.go | 19 + .../v1beta1/constrainttemplate_types_test.go | 495 +++++++- .../pkg/apis/templates/v1beta1/conversion.go | 41 + .../apis/templates/v1beta1/helpers_test.go | 11 + .../v1beta1/zz_generated.conversion.go | 79 +- .../v1beta1/zz_generated.deepcopy.go | 26 + constraint/pkg/client/client.go | 256 +++- .../client/client_addtemplate_bench_test.go | 14 +- constraint/pkg/client/client_internal_test.go | 1040 +++++++++++++++++ constraint/pkg/client/client_opts.go | 6 +- constraint/pkg/client/client_opts_test.go | 26 + constraint/pkg/client/client_test.go | 148 ++- constraint/pkg/client/clienttest/client.go | 4 +- constraint/pkg/client/clienttest/cts/opts.go | 30 +- constraint/pkg/client/clienttest/templates.go | 91 +- constraint/pkg/client/drivers/dummy/dummy.go | 217 ++++ .../pkg/client/drivers/dummy/schema/schema.go | 49 + constraint/pkg/client/drivers/interface.go | 4 + .../pkg/client/drivers/k8scel/driver.go | 198 ++++ .../client/drivers/{local => rego}/args.go | 2 +- .../client/drivers/{local => rego}/builtin.go | 2 +- .../drivers/{local => rego}/compilers.go | 33 +- .../client/drivers/{local => rego}/driver.go | 20 +- .../drivers/{local => rego}/driver_test.go | 2 +- .../{local => rego}/driver_unit_test.go | 10 +- .../pkg/client/drivers/{local => rego}/new.go | 2 +- .../client/drivers/{local => rego}/rego.go | 2 +- .../pkg/client/drivers/rego/schema/schema.go | 70 ++ .../drivers/{local => rego}/storages.go | 2 +- .../pkg/client/drivers/remote/httpclient.go | 289 ----- .../client/drivers/remote/httpclient_test.go | 106 -- .../pkg/client/drivers/remote/remote.go | 222 ---- .../pkg/client/drivers/remote/remote_test.go | 133 --- constraint/pkg/client/e2e_test.go | 8 +- constraint/pkg/client/errors.go | 1 + constraint/pkg/client/errors/errors.go | 1 + constraint/pkg/client/new_client.go | 6 +- constraint/pkg/client/new_client_test.go | 4 +- constraint/pkg/client/template_client.go | 17 + .../templates/constrainttemplate_types.go | 63 + .../core/templates/zz_generated.deepcopy.go | 28 +- constraint/pkg/schema/yaml_constant.go | 114 +- 58 files changed, 4351 insertions(+), 1000 deletions(-) create mode 100644 constraint/pkg/client/client_internal_test.go create mode 100644 constraint/pkg/client/client_opts_test.go create mode 100644 constraint/pkg/client/drivers/dummy/dummy.go create mode 100644 constraint/pkg/client/drivers/dummy/schema/schema.go create mode 100644 constraint/pkg/client/drivers/k8scel/driver.go rename constraint/pkg/client/drivers/{local => rego}/args.go (99%) rename constraint/pkg/client/drivers/{local => rego}/builtin.go (98%) rename constraint/pkg/client/drivers/{local => rego}/compilers.go (88%) rename constraint/pkg/client/drivers/{local => rego}/driver.go (96%) rename constraint/pkg/client/drivers/{local => rego}/driver_test.go (99%) rename constraint/pkg/client/drivers/{local => rego}/driver_unit_test.go (99%) rename constraint/pkg/client/drivers/{local => rego}/new.go (97%) rename constraint/pkg/client/drivers/{local => rego}/rego.go (99%) create mode 100644 constraint/pkg/client/drivers/rego/schema/schema.go rename constraint/pkg/client/drivers/{local => rego}/storages.go (99%) delete mode 100644 constraint/pkg/client/drivers/remote/httpclient.go delete mode 100644 constraint/pkg/client/drivers/remote/httpclient_test.go delete mode 100644 constraint/pkg/client/drivers/remote/remote.go delete mode 100644 constraint/pkg/client/drivers/remote/remote_test.go diff --git a/README.md b/README.md index f7a20b33c..659b8fcf3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,3 @@ # Open Policy Agent Frameworks Open Policy Agent is a general-purpose policy system designed to policy-enable other projects and services. The OPA Frameworks repository defines opinionated APIs for policy that are less flexible than the OPA API but are well-suited to particular classes of use cases. For example, Role Based Acces Control (RBAC), Attribute Based Access Control, Access Control Lists (ACLs), and IAM can all be implemented on top of the OPA API and its policy language, and could each be defined as an OPA Framework. One analogy from the web development world that seems to help people is that Frameworks are to OPA as Rails is to Ruby. - -## Prerequisites - -To clone this repository, you need [git-lfs](https://git-lfs.github.com/) installed. diff --git a/constraint/Makefile b/constraint/Makefile index d6e19b28f..3bb509579 100644 --- a/constraint/Makefile +++ b/constraint/Makefile @@ -73,7 +73,8 @@ generate: generate-defaults # TODO: Once https://github.com/kubernetes/kubernetes/issues/101567 is fixed, update # conversion-gen and get us back to running `make generate` in our CI pipeline conversion-gen \ - --input-dirs "./pkg/apis/templates/...,./pkg/apis/externaldata/..." \ + --input-dirs "./pkg/apis/templates/v1,./pkg/apis/templates/v1beta1,./pkg/apis/templates/v1alpha1,./pkg/apis/externaldata/v1alpha1,./pkg/apis/externaldata/v1beta1" \ + --output-base=./ \ --go-header-file=./hack/boilerplate.go.txt \ --output-file-base=zz_generated.conversion \ --extra-dirs=k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 @@ -125,4 +126,4 @@ ENVTEST ?= $(LOCALBIN)/setup-envtest .PHONY: envtest envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) - test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.0.0-20230118154835-9241bceb3098 + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) GCO_ENABLED=0 go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.0.0-20230118154835-9241bceb3098 diff --git a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml index 9f7708390..018165dfe 100644 --- a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml +++ b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml @@ -65,6 +65,26 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -168,6 +188,26 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -271,6 +311,26 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string diff --git a/constraint/deploy/crds.yaml b/constraint/deploy/crds.yaml index 9ae0cb7e2..c583ad4f8 100644 --- a/constraint/deploy/crds.yaml +++ b/constraint/deploy/crds.yaml @@ -59,6 +59,23 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -154,6 +171,23 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -249,6 +283,23 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + required: + - engine + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string diff --git a/constraint/pkg/apis/templates/v1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1/constrainttemplate_types.go index 3d71846de..c289c0a39 100644 --- a/constraint/pkg/apis/templates/v1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1/constrainttemplate_types.go @@ -16,6 +16,7 @@ limitations under the License. package v1 import ( + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -48,6 +49,7 @@ type Validation struct { // +kubebuilder:validation:Schemaless // +kubebuilder:validation:Type=object // +kubebuilder:pruning:PreserveUnknownFields + // +k8s:conversion-gen=false OpenAPIV3Schema *apiextensionsv1.JSONSchemaProps `json:"openAPIV3Schema,omitempty"` // +kubebuilder:default=false LegacySchema *bool `json:"legacySchema,omitempty"` // *bool allows for "unset" state which we need to apply appropriate defaults @@ -57,6 +59,24 @@ type Target struct { Target string `json:"target,omitempty"` Rego string `json:"rego,omitempty"` Libs []string `json:"libs,omitempty"` + // The source code options for the constraint template. "Rego" can only + // be specified in one place (either here or in the "rego" field) + // +listType=map + // +listMapKey=engine + // +kubebuilder:validation:Required + Code []Code `json:"code,omitempty"` +} + +type Code struct { + // The engine used to evaluate the code. Example: "Rego". Required. + // +kubebuilder:validation:Required + Engine string `json:"engine"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // The source code for the template. Required. + Source *templates.Anything `json:"source,omitempty"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/apis/templates/v1/constrainttemplate_types_test.go b/constraint/pkg/apis/templates/v1/constrainttemplate_types_test.go index 81eff88b8..c654ebd7e 100644 --- a/constraint/pkg/apis/templates/v1/constrainttemplate_types_test.go +++ b/constraint/pkg/apis/templates/v1/constrainttemplate_types_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -91,11 +92,7 @@ func TestStorageConstraintTemplate(t *testing.T) { } func TestTypeConversion(t *testing.T) { - scheme := runtime.NewScheme() - if err := AddToScheme(scheme); err != nil { - t.Fatalf("Could not add to scheme: %v", err) - } - versioned := &ConstraintTemplate{ + regoOnly := &ConstraintTemplate{ TypeMeta: metav1.TypeMeta{ Kind: "ConstraintTemplate", APIVersion: "templates.gatekeeper.sh/v1", @@ -142,23 +139,483 @@ func TestTypeConversion(t *testing.T) { }, }, } - versionedCopy := versioned.DeepCopy() - // Kind and API Version do not survive the conversion process - versionedCopy.Kind = "" - versionedCopy.APIVersion = "" - unversioned := &templates.ConstraintTemplate{} - if err := scheme.Convert(versioned, unversioned, nil); err != nil { - t.Fatalf("Conversion error: %v", err) + regoOnlyExpectedResult := regoOnly.DeepCopy() + regoOnlyExpectedResult.Spec.Targets[0].Code = append(regoOnlyExpectedResult.Spec.Targets[0].Code, + Code{ + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: regoOnlyExpectedResult.Spec.Targets[0].Rego, + Libs: regoOnlyExpectedResult.Spec.Targets[0].Libs, + }).ToUnstructured(), + }, + }, + ) + + tests := []struct { + name string + input *ConstraintTemplate + expected *ConstraintTemplate + }{ + { + name: "Rego Only", + input: regoOnly, + expected: regoOnlyExpectedResult, + }, + { + name: "Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Non Rego", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Dedicated Field", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Rego Clobber", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "this rego should be clobbered"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, } - recast := &ConstraintTemplate{} - if err := scheme.Convert(unversioned, recast, nil); err != nil { - t.Fatalf("Recast conversion error: %v", err) + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("Could not add to scheme: %v", err) } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected := test.expected + // if expected is nil, this should be a lossless round-trip + if expected == nil { + expected = test.input.DeepCopy() + } + + // Kind and API Version do not survive the conversion process + expected.Kind = "" + expected.APIVersion = "" + + unversioned := &templates.ConstraintTemplate{} + if err := scheme.Convert(test.input, unversioned, nil); err != nil { + t.Fatalf("Conversion error: %v", err) + } + + recast := &ConstraintTemplate{} + if err := scheme.Convert(unversioned, recast, nil); err != nil { + t.Fatalf("Recast conversion error: %v", err) + } - if !reflect.DeepEqual(versionedCopy, recast) { - t.Fatalf("Unexpected template difference. Diff: %v", cmp.Diff(versionedCopy, recast)) + if !reflect.DeepEqual(expected, recast) { + t.Fatalf("Unexpected template difference. Diff: %v", cmp.Diff(expected, recast)) + } + }) } } diff --git a/constraint/pkg/apis/templates/v1/conversion.go b/constraint/pkg/apis/templates/v1/conversion.go index 7081c15e9..0c448123b 100644 --- a/constraint/pkg/apis/templates/v1/conversion.go +++ b/constraint/pkg/apis/templates/v1/conversion.go @@ -16,6 +16,9 @@ limitations under the License. package v1 import ( + "unsafe" + + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" coreTemplates "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -61,3 +64,41 @@ func Convert_v1_Validation_To_templates_Validation(in *Validation, out *coreTemp return nil } + +func Convert_v1_Target_To_templates_Target(in *Target, out *coreTemplates.Target, s conversion.Scope) error { // nolint:revive // Required exact function name. + out.Target = in.Target + out.Rego = in.Rego + out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + + out.Code = make([]coreTemplates.Code, len(in.Code)) + for i := range in.Code { + if err := Convert_v1_Code_To_templates_Code(&(in.Code[i]), &(out.Code[i]), s); err != nil { + return err + } + } + + if in.Rego == "" { + return nil + } + + regoSource := ®oSchema.Source{} + regoSource.Rego = in.Rego + regoSource.Libs = append(regoSource.Libs, in.Libs...) + + injected := false + for i := range out.Code { + if out.Code[i].Engine == regoSchema.Name { + out.Code[i].Source.Value = regoSource.ToUnstructured() + injected = true + break + } + } + if !injected { + out.Code = append(out.Code, coreTemplates.Code{ + Engine: regoSchema.Name, + Source: &coreTemplates.Anything{Value: regoSource.ToUnstructured()}, + }) + } + + return nil +} diff --git a/constraint/pkg/apis/templates/v1/helpers_test.go b/constraint/pkg/apis/templates/v1/helpers_test.go index 50ef91099..b9393057f 100644 --- a/constraint/pkg/apis/templates/v1/helpers_test.go +++ b/constraint/pkg/apis/templates/v1/helpers_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -89,6 +90,16 @@ func TestToVersionless(t *testing.T) { { Target: "sometarget", Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []templates.Code{ + { + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + }).ToUnstructured(), + }, + }, + }, }, }, }, diff --git a/constraint/pkg/apis/templates/v1/zz_generated.conversion.go b/constraint/pkg/apis/templates/v1/zz_generated.conversion.go index 4bacd6c3c..453cd658c 100644 --- a/constraint/pkg/apis/templates/v1/zz_generated.conversion.go +++ b/constraint/pkg/apis/templates/v1/zz_generated.conversion.go @@ -65,6 +65,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Code)(nil), (*templates.Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_Code_To_templates_Code(a.(*Code), b.(*templates.Code), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*templates.Code)(nil), (*Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_templates_Code_To_v1_Code(a.(*templates.Code), b.(*Code), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ConstraintTemplate)(nil), (*templates.ConstraintTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_ConstraintTemplate_To_templates_ConstraintTemplate(a.(*ConstraintTemplate), b.(*templates.ConstraintTemplate), scope) }); err != nil { @@ -125,11 +135,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*templates.Target)(nil), (*Target)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_templates_Target_To_v1_Target(a.(*templates.Target), b.(*Target), scope) }); err != nil { @@ -140,6 +145,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*Validation)(nil), (*templates.Validation)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1_Validation_To_templates_Validation(a.(*Validation), b.(*templates.Validation), scope) }); err != nil { @@ -238,6 +248,28 @@ func Convert_templates_CRDSpec_To_v1_CRDSpec(in *templates.CRDSpec, out *CRDSpec return autoConvert_templates_CRDSpec_To_v1_CRDSpec(in, out, s) } +func autoConvert_v1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_v1_Code_To_templates_Code is an autogenerated conversion function. +func Convert_v1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + return autoConvert_v1_Code_To_templates_Code(in, out, s) +} + +func autoConvert_templates_Code_To_v1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_templates_Code_To_v1_Code is an autogenerated conversion function. +func Convert_templates_Code_To_v1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + return autoConvert_templates_Code_To_v1_Code(in, out, s) +} + func autoConvert_v1_ConstraintTemplate_To_templates_ConstraintTemplate(in *ConstraintTemplate, out *templates.ConstraintTemplate, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1_ConstraintTemplateSpec_To_templates_ConstraintTemplateSpec(&in.Spec, &out.Spec, s); err != nil { @@ -316,7 +348,17 @@ func autoConvert_v1_ConstraintTemplateSpec_To_templates_ConstraintTemplateSpec(i if err := Convert_v1_CRD_To_templates_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]templates.Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]templates.Target, len(*in)) + for i := range *in { + if err := Convert_v1_Target_To_templates_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -329,7 +371,17 @@ func autoConvert_templates_ConstraintTemplateSpec_To_v1_ConstraintTemplateSpec(i if err := Convert_templates_CRD_To_v1_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + if err := Convert_templates_Target_To_v1_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -410,18 +462,15 @@ func autoConvert_v1_Target_To_templates_Target(in *Target, out *templates.Target out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]templates.Code)(unsafe.Pointer(&in.Code)) return nil } -// Convert_v1_Target_To_templates_Target is an autogenerated conversion function. -func Convert_v1_Target_To_templates_Target(in *Target, out *templates.Target, s conversion.Scope) error { - return autoConvert_v1_Target_To_templates_Target(in, out, s) -} - func autoConvert_templates_Target_To_v1_Target(in *templates.Target, out *Target, s conversion.Scope) error { out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]Code)(unsafe.Pointer(&in.Code)) return nil } @@ -430,6 +479,12 @@ func Convert_templates_Target_To_v1_Target(in *templates.Target, out *Target, s return autoConvert_templates_Target_To_v1_Target(in, out, s) } +func autoConvert_v1_Validation_To_templates_Validation(in *Validation, out *templates.Validation, s conversion.Scope) error { + // INFO: in.OpenAPIV3Schema opted out of conversion generation + out.LegacySchema = (*bool)(unsafe.Pointer(in.LegacySchema)) + return nil +} + func autoConvert_templates_Validation_To_v1_Validation(in *templates.Validation, out *Validation, s conversion.Scope) error { if in.OpenAPIV3Schema != nil { in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema diff --git a/constraint/pkg/apis/templates/v1/zz_generated.deepcopy.go b/constraint/pkg/apis/templates/v1/zz_generated.deepcopy.go index 00df042db..797b58ef4 100644 --- a/constraint/pkg/apis/templates/v1/zz_generated.deepcopy.go +++ b/constraint/pkg/apis/templates/v1/zz_generated.deepcopy.go @@ -81,6 +81,25 @@ func (in *CRDSpec) DeepCopy() *CRDSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Code) DeepCopyInto(out *Code) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Code. +func (in *Code) DeepCopy() *Code { + if in == nil { + return nil + } + out := new(Code) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConstraintTemplate) DeepCopyInto(out *ConstraintTemplate) { *out = *in @@ -228,6 +247,13 @@ func (in *Target) DeepCopyInto(out *Target) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Code != nil { + in, out := &in.Code, &out.Code + *out = make([]Code, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. diff --git a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go index 061926cf5..3da73c909 100644 --- a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go @@ -16,6 +16,7 @@ limitations under the License. package v1alpha1 import ( + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -48,6 +49,7 @@ type Validation struct { // +kubebuilder:validation:Schemaless // +kubebuilder:validation:Type=object // +kubebuilder:pruning:PreserveUnknownFields + // +k8s:conversion-gen=false OpenAPIV3Schema *apiextensionsv1.JSONSchemaProps `json:"openAPIV3Schema,omitempty"` // +kubebuilder:default=true LegacySchema *bool `json:"legacySchema,omitempty"` // *bool allows for "unset" state which we need to apply appropriate defaults @@ -57,6 +59,23 @@ type Target struct { Target string `json:"target,omitempty"` Rego string `json:"rego,omitempty"` Libs []string `json:"libs,omitempty"` + // The source code options for the constraint template. "Rego" can only + // be specified in one place (either here or in the "rego" field) + // +listType=map + // +listMapKey=engine + Code []Code `json:"code,omitempty"` +} + +type Code struct { + // The engine used to evaluate the code. Example: "Rego". Required. + // +kubebuilder:validation:Required + Engine string `json:"engine"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // The source code for the template. Required. + Source *templates.Anything `json:"source,omitempty"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types_test.go b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types_test.go index 7bcc4492e..11b7a2d34 100644 --- a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types_test.go +++ b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -91,15 +92,10 @@ func TestStorageConstraintTemplate(t *testing.T) { } func TestTypeConversion(t *testing.T) { - scheme := runtime.NewScheme() - if err := AddToScheme(scheme); err != nil { - t.Fatalf("Could not add to scheme: %v", err) - } - - versioned := &ConstraintTemplate{ + regoOnly := &ConstraintTemplate{ TypeMeta: metav1.TypeMeta{ Kind: "ConstraintTemplate", - APIVersion: "templates.gatekeeper.sh/v1alpha1", + APIVersion: "templates.gatekeeper.sh/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "MustHaveMoreCats", @@ -113,6 +109,7 @@ func TestTypeConversion(t *testing.T) { }, Validation: &Validation{ OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "message": { Type: "string", @@ -142,18 +139,483 @@ func TestTypeConversion(t *testing.T) { }, }, } - versionedCopy := versioned.DeepCopy() - // Kind and API Version do not survive the conversion process - versionedCopy.Kind = "" - versionedCopy.APIVersion = "" - unversioned := &templates.ConstraintTemplate{} - if err := scheme.Convert(versioned, unversioned, nil); err != nil { - t.Fatalf("Conversion error: %v", err) + regoOnlyExpectedResult := regoOnly.DeepCopy() + regoOnlyExpectedResult.Spec.Targets[0].Code = append(regoOnlyExpectedResult.Spec.Targets[0].Code, + Code{ + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: regoOnlyExpectedResult.Spec.Targets[0].Rego, + Libs: regoOnlyExpectedResult.Spec.Targets[0].Libs, + }).ToUnstructured(), + }, + }, + ) + + tests := []struct { + name string + input *ConstraintTemplate + expected *ConstraintTemplate + }{ + { + name: "Rego Only", + input: regoOnly, + expected: regoOnlyExpectedResult, + }, + { + name: "Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Non Rego", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Dedicated Field", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Rego Clobber", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "this rego should be clobbered"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("Could not add to scheme: %v", err) } - recast := &ConstraintTemplate{} - if err := scheme.Convert(unversioned, recast, nil); err != nil { - t.Fatalf("Conversion error: %v", err) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected := test.expected + // if expected is nil, this should be a lossless round-trip + if expected == nil { + expected = test.input.DeepCopy() + } + + // Kind and API Version do not survive the conversion process + expected.Kind = "" + expected.APIVersion = "" + + unversioned := &templates.ConstraintTemplate{} + if err := scheme.Convert(test.input, unversioned, nil); err != nil { + t.Fatalf("Conversion error: %v", err) + } + + recast := &ConstraintTemplate{} + if err := scheme.Convert(unversioned, recast, nil); err != nil { + t.Fatalf("Recast conversion error: %v", err) + } + + if !reflect.DeepEqual(expected, recast) { + t.Fatalf("Unexpected template difference. Diff: %v", cmp.Diff(expected, recast)) + } + }) } } diff --git a/constraint/pkg/apis/templates/v1alpha1/conversion.go b/constraint/pkg/apis/templates/v1alpha1/conversion.go index 68cf02014..bdfaef975 100644 --- a/constraint/pkg/apis/templates/v1alpha1/conversion.go +++ b/constraint/pkg/apis/templates/v1alpha1/conversion.go @@ -16,6 +16,9 @@ limitations under the License. package v1alpha1 import ( + "unsafe" + + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" coreTemplates "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -61,3 +64,41 @@ func Convert_v1alpha1_Validation_To_templates_Validation(in *Validation, out *co return nil } + +func Convert_v1alpha1_Target_To_templates_Target(in *Target, out *coreTemplates.Target, s conversion.Scope) error { // nolint:revive // Required exact function name. + out.Target = in.Target + out.Rego = in.Rego + out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + + out.Code = make([]coreTemplates.Code, len(in.Code)) + for i := range in.Code { + if err := Convert_v1alpha1_Code_To_templates_Code(&(in.Code[i]), &(out.Code[i]), s); err != nil { + return err + } + } + + if in.Rego == "" { + return nil + } + + regoSource := ®oSchema.Source{} + regoSource.Rego = in.Rego + regoSource.Libs = append(regoSource.Libs, in.Libs...) + + injected := false + for i := range out.Code { + if out.Code[i].Engine == regoSchema.Name { + out.Code[i].Source.Value = regoSource.ToUnstructured() + injected = true + break + } + } + if !injected { + out.Code = append(out.Code, coreTemplates.Code{ + Engine: regoSchema.Name, + Source: &coreTemplates.Anything{Value: regoSource.ToUnstructured()}, + }) + } + + return nil +} diff --git a/constraint/pkg/apis/templates/v1alpha1/helpers_test.go b/constraint/pkg/apis/templates/v1alpha1/helpers_test.go index b21227dec..e9378ae38 100644 --- a/constraint/pkg/apis/templates/v1alpha1/helpers_test.go +++ b/constraint/pkg/apis/templates/v1alpha1/helpers_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -89,6 +90,16 @@ func TestToVersionless(t *testing.T) { { Target: "sometarget", Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []templates.Code{ + { + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + }).ToUnstructured(), + }, + }, + }, }, }, }, diff --git a/constraint/pkg/apis/templates/v1alpha1/zz_generated.conversion.go b/constraint/pkg/apis/templates/v1alpha1/zz_generated.conversion.go index ef3792ae5..38fe6f4cd 100644 --- a/constraint/pkg/apis/templates/v1alpha1/zz_generated.conversion.go +++ b/constraint/pkg/apis/templates/v1alpha1/zz_generated.conversion.go @@ -65,6 +65,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Code)(nil), (*templates.Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Code_To_templates_Code(a.(*Code), b.(*templates.Code), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*templates.Code)(nil), (*Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_templates_Code_To_v1alpha1_Code(a.(*templates.Code), b.(*Code), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ConstraintTemplate)(nil), (*templates.ConstraintTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ConstraintTemplate_To_templates_ConstraintTemplate(a.(*ConstraintTemplate), b.(*templates.ConstraintTemplate), scope) }); err != nil { @@ -125,11 +135,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1alpha1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*templates.Target)(nil), (*Target)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_templates_Target_To_v1alpha1_Target(a.(*templates.Target), b.(*Target), scope) }); err != nil { @@ -140,6 +145,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*Validation)(nil), (*templates.Validation)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_Validation_To_templates_Validation(a.(*Validation), b.(*templates.Validation), scope) }); err != nil { @@ -238,6 +248,28 @@ func Convert_templates_CRDSpec_To_v1alpha1_CRDSpec(in *templates.CRDSpec, out *C return autoConvert_templates_CRDSpec_To_v1alpha1_CRDSpec(in, out, s) } +func autoConvert_v1alpha1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_v1alpha1_Code_To_templates_Code is an autogenerated conversion function. +func Convert_v1alpha1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + return autoConvert_v1alpha1_Code_To_templates_Code(in, out, s) +} + +func autoConvert_templates_Code_To_v1alpha1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_templates_Code_To_v1alpha1_Code is an autogenerated conversion function. +func Convert_templates_Code_To_v1alpha1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + return autoConvert_templates_Code_To_v1alpha1_Code(in, out, s) +} + func autoConvert_v1alpha1_ConstraintTemplate_To_templates_ConstraintTemplate(in *ConstraintTemplate, out *templates.ConstraintTemplate, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_ConstraintTemplateSpec_To_templates_ConstraintTemplateSpec(&in.Spec, &out.Spec, s); err != nil { @@ -316,7 +348,17 @@ func autoConvert_v1alpha1_ConstraintTemplateSpec_To_templates_ConstraintTemplate if err := Convert_v1alpha1_CRD_To_templates_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]templates.Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]templates.Target, len(*in)) + for i := range *in { + if err := Convert_v1alpha1_Target_To_templates_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -329,7 +371,17 @@ func autoConvert_templates_ConstraintTemplateSpec_To_v1alpha1_ConstraintTemplate if err := Convert_templates_CRD_To_v1alpha1_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + if err := Convert_templates_Target_To_v1alpha1_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -410,18 +462,15 @@ func autoConvert_v1alpha1_Target_To_templates_Target(in *Target, out *templates. out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]templates.Code)(unsafe.Pointer(&in.Code)) return nil } -// Convert_v1alpha1_Target_To_templates_Target is an autogenerated conversion function. -func Convert_v1alpha1_Target_To_templates_Target(in *Target, out *templates.Target, s conversion.Scope) error { - return autoConvert_v1alpha1_Target_To_templates_Target(in, out, s) -} - func autoConvert_templates_Target_To_v1alpha1_Target(in *templates.Target, out *Target, s conversion.Scope) error { out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]Code)(unsafe.Pointer(&in.Code)) return nil } @@ -430,6 +479,12 @@ func Convert_templates_Target_To_v1alpha1_Target(in *templates.Target, out *Targ return autoConvert_templates_Target_To_v1alpha1_Target(in, out, s) } +func autoConvert_v1alpha1_Validation_To_templates_Validation(in *Validation, out *templates.Validation, s conversion.Scope) error { + // INFO: in.OpenAPIV3Schema opted out of conversion generation + out.LegacySchema = (*bool)(unsafe.Pointer(in.LegacySchema)) + return nil +} + func autoConvert_templates_Validation_To_v1alpha1_Validation(in *templates.Validation, out *Validation, s conversion.Scope) error { if in.OpenAPIV3Schema != nil { in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema diff --git a/constraint/pkg/apis/templates/v1alpha1/zz_generated.deepcopy.go b/constraint/pkg/apis/templates/v1alpha1/zz_generated.deepcopy.go index 1ce321d28..6720c01f7 100644 --- a/constraint/pkg/apis/templates/v1alpha1/zz_generated.deepcopy.go +++ b/constraint/pkg/apis/templates/v1alpha1/zz_generated.deepcopy.go @@ -81,6 +81,25 @@ func (in *CRDSpec) DeepCopy() *CRDSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Code) DeepCopyInto(out *Code) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Code. +func (in *Code) DeepCopy() *Code { + if in == nil { + return nil + } + out := new(Code) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConstraintTemplate) DeepCopyInto(out *ConstraintTemplate) { *out = *in @@ -228,6 +247,13 @@ func (in *Target) DeepCopyInto(out *Target) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Code != nil { + in, out := &in.Code, &out.Code + *out = make([]Code, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. diff --git a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go index 51167178b..819ab1e04 100644 --- a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go @@ -16,6 +16,7 @@ limitations under the License. package v1beta1 import ( + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -48,6 +49,7 @@ type Validation struct { // +kubebuilder:validation:Schemaless // +kubebuilder:validation:Type=object // +kubebuilder:pruning:PreserveUnknownFields + // +k8s:conversion-gen=false OpenAPIV3Schema *apiextensionsv1.JSONSchemaProps `json:"openAPIV3Schema,omitempty"` // +kubebuilder:default=true LegacySchema *bool `json:"legacySchema,omitempty"` // *bool allows for "unset" state which we need to apply appropriate defaults @@ -57,6 +59,23 @@ type Target struct { Target string `json:"target,omitempty"` Rego string `json:"rego,omitempty"` Libs []string `json:"libs,omitempty"` + // The source code options for the constraint template. "Rego" can only + // be specified in one place (either here or in the "rego" field) + // +listType=map + // +listMapKey=engine + Code []Code `json:"code,omitempty"` +} + +type Code struct { + // The engine used to evaluate the code. Example: "Rego". Required. + // +kubebuilder:validation:Required + Engine string `json:"engine"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // The source code for the template. Required. + Source *templates.Anything `json:"source,omitempty"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types_test.go b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types_test.go index ce74ace4d..f8682f4cb 100644 --- a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types_test.go +++ b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -91,14 +92,10 @@ func TestStorageConstraintTemplate(t *testing.T) { } func TestTypeConversion(t *testing.T) { - scheme := runtime.NewScheme() - if err := AddToScheme(scheme); err != nil { - t.Fatalf("Could not add to scheme: %v", err) - } - versioned := &ConstraintTemplate{ + regoOnly := &ConstraintTemplate{ TypeMeta: metav1.TypeMeta{ Kind: "ConstraintTemplate", - APIVersion: "templates.gatekeeper.sh/v1beta1", + APIVersion: "templates.gatekeeper.sh/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "MustHaveMoreCats", @@ -112,6 +109,7 @@ func TestTypeConversion(t *testing.T) { }, Validation: &Validation{ OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", Properties: map[string]apiextensionsv1.JSONSchemaProps{ "message": { Type: "string", @@ -141,18 +139,483 @@ func TestTypeConversion(t *testing.T) { }, }, } - versionedCopy := versioned.DeepCopy() - // Kind and API Version do not survive the conversion process - versionedCopy.Kind = "" - versionedCopy.APIVersion = "" - unversioned := &templates.ConstraintTemplate{} - if err := scheme.Convert(versioned, unversioned, nil); err != nil { - t.Fatalf("Conversion error: %v", err) + regoOnlyExpectedResult := regoOnly.DeepCopy() + regoOnlyExpectedResult.Spec.Targets[0].Code = append(regoOnlyExpectedResult.Spec.Targets[0].Code, + Code{ + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: regoOnlyExpectedResult.Spec.Targets[0].Rego, + Libs: regoOnlyExpectedResult.Spec.Targets[0].Libs, + }).ToUnstructured(), + }, + }, + ) + + tests := []struct { + name string + input *ConstraintTemplate + expected *ConstraintTemplate + }{ + { + name: "Rego Only", + input: regoOnly, + expected: regoOnlyExpectedResult, + }, + { + name: "Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Non Rego", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Code", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Mixed, Rego in Dedicated Field", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Rego Clobber", + input: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "this rego should be clobbered"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + expected: &ConstraintTemplate{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConstraintTemplate", + APIVersion: "templates.gatekeeper.sh/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "MustHaveMoreCats", + }, + Spec: ConstraintTemplateSpec{ + CRD: CRD{ + Spec: CRDSpec{ + Names: Names{ + Kind: "MustHaveMoreCats", + ShortNames: []string{"mhmc"}, + }, + Validation: &Validation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "message": { + Type: "string", + }, + "labels": { + Type: "array", + Items: &apiextensionsv1.JSONSchemaPropsOrArray{ + Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "key": {Type: "string"}, + "allowedRegex": {Type: "string"}, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Targets: []Target{ + { + Target: "sometarget", + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []Code{ + { + Engine: "k8sadmission", + Source: &templates.Anything{ + Value: map[string]interface{}{"my-k8s-code": `validate-super-strict`}, + }, + }, + { + Engine: "Rego", + Source: &templates.Anything{ + Value: map[string]interface{}{"rego": `package hello ; violation[{"msg": "msg"}] { true }`}, + }, + }, + }, + }, + }, + }, + }, + }, + } + + scheme := runtime.NewScheme() + if err := AddToScheme(scheme); err != nil { + t.Fatalf("Could not add to scheme: %v", err) } - recast := &ConstraintTemplate{} - if err := scheme.Convert(unversioned, recast, nil); err != nil { - t.Fatalf("Recast conversion error: %v", err) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + expected := test.expected + // if expected is nil, this should be a lossless round-trip + if expected == nil { + expected = test.input.DeepCopy() + } + + // Kind and API Version do not survive the conversion process + expected.Kind = "" + expected.APIVersion = "" + + unversioned := &templates.ConstraintTemplate{} + if err := scheme.Convert(test.input, unversioned, nil); err != nil { + t.Fatalf("Conversion error: %v", err) + } + + recast := &ConstraintTemplate{} + if err := scheme.Convert(unversioned, recast, nil); err != nil { + t.Fatalf("Recast conversion error: %v", err) + } + + if !reflect.DeepEqual(expected, recast) { + t.Fatalf("Unexpected template difference. Diff: %v", cmp.Diff(expected, recast)) + } + }) } } diff --git a/constraint/pkg/apis/templates/v1beta1/conversion.go b/constraint/pkg/apis/templates/v1beta1/conversion.go index 9bb69b0c7..915782b83 100644 --- a/constraint/pkg/apis/templates/v1beta1/conversion.go +++ b/constraint/pkg/apis/templates/v1beta1/conversion.go @@ -16,6 +16,9 @@ limitations under the License. package v1beta1 import ( + "unsafe" + + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" coreTemplates "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -61,3 +64,41 @@ func Convert_v1beta1_Validation_To_templates_Validation(in *Validation, out *cor return nil } + +func Convert_v1beta1_Target_To_templates_Target(in *Target, out *coreTemplates.Target, s conversion.Scope) error { // nolint:revive // Required exact function name. + out.Target = in.Target + out.Rego = in.Rego + out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + + out.Code = make([]coreTemplates.Code, len(in.Code)) + for i := range in.Code { + if err := Convert_v1beta1_Code_To_templates_Code(&(in.Code[i]), &(out.Code[i]), s); err != nil { + return err + } + } + + if in.Rego == "" { + return nil + } + + regoSource := ®oSchema.Source{} + regoSource.Rego = in.Rego + regoSource.Libs = append(regoSource.Libs, in.Libs...) + + injected := false + for i := range out.Code { + if out.Code[i].Engine == regoSchema.Name { + out.Code[i].Source.Value = regoSource.ToUnstructured() + injected = true + break + } + } + if !injected { + out.Code = append(out.Code, coreTemplates.Code{ + Engine: regoSchema.Name, + Source: &coreTemplates.Anything{Value: regoSource.ToUnstructured()}, + }) + } + + return nil +} diff --git a/constraint/pkg/apis/templates/v1beta1/helpers_test.go b/constraint/pkg/apis/templates/v1beta1/helpers_test.go index aa29d7542..3099ca8f5 100644 --- a/constraint/pkg/apis/templates/v1beta1/helpers_test.go +++ b/constraint/pkg/apis/templates/v1beta1/helpers_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/schema" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" @@ -89,6 +90,16 @@ func TestToVersionless(t *testing.T) { { Target: "sometarget", Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + Code: []templates.Code{ + { + Engine: regoSchema.Name, + Source: &templates.Anything{ + Value: (®oSchema.Source{ + Rego: `package hello ; violation[{"msg": "msg"}] { true }`, + }).ToUnstructured(), + }, + }, + }, }, }, }, diff --git a/constraint/pkg/apis/templates/v1beta1/zz_generated.conversion.go b/constraint/pkg/apis/templates/v1beta1/zz_generated.conversion.go index 2a46ec4b1..b99250527 100644 --- a/constraint/pkg/apis/templates/v1beta1/zz_generated.conversion.go +++ b/constraint/pkg/apis/templates/v1beta1/zz_generated.conversion.go @@ -65,6 +65,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*Code)(nil), (*templates.Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Code_To_templates_Code(a.(*Code), b.(*templates.Code), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*templates.Code)(nil), (*Code)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_templates_Code_To_v1beta1_Code(a.(*templates.Code), b.(*Code), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ConstraintTemplate)(nil), (*templates.ConstraintTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_ConstraintTemplate_To_templates_ConstraintTemplate(a.(*ConstraintTemplate), b.(*templates.ConstraintTemplate), scope) }); err != nil { @@ -125,11 +135,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*templates.Target)(nil), (*Target)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_templates_Target_To_v1beta1_Target(a.(*templates.Target), b.(*Target), scope) }); err != nil { @@ -140,6 +145,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*Target)(nil), (*templates.Target)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Target_To_templates_Target(a.(*Target), b.(*templates.Target), scope) + }); err != nil { + return err + } if err := s.AddConversionFunc((*Validation)(nil), (*templates.Validation)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_Validation_To_templates_Validation(a.(*Validation), b.(*templates.Validation), scope) }); err != nil { @@ -238,6 +248,28 @@ func Convert_templates_CRDSpec_To_v1beta1_CRDSpec(in *templates.CRDSpec, out *CR return autoConvert_templates_CRDSpec_To_v1beta1_CRDSpec(in, out, s) } +func autoConvert_v1beta1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_v1beta1_Code_To_templates_Code is an autogenerated conversion function. +func Convert_v1beta1_Code_To_templates_Code(in *Code, out *templates.Code, s conversion.Scope) error { + return autoConvert_v1beta1_Code_To_templates_Code(in, out, s) +} + +func autoConvert_templates_Code_To_v1beta1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + out.Engine = in.Engine + out.Source = (*templates.Anything)(unsafe.Pointer(in.Source)) + return nil +} + +// Convert_templates_Code_To_v1beta1_Code is an autogenerated conversion function. +func Convert_templates_Code_To_v1beta1_Code(in *templates.Code, out *Code, s conversion.Scope) error { + return autoConvert_templates_Code_To_v1beta1_Code(in, out, s) +} + func autoConvert_v1beta1_ConstraintTemplate_To_templates_ConstraintTemplate(in *ConstraintTemplate, out *templates.ConstraintTemplate, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1beta1_ConstraintTemplateSpec_To_templates_ConstraintTemplateSpec(&in.Spec, &out.Spec, s); err != nil { @@ -316,7 +348,17 @@ func autoConvert_v1beta1_ConstraintTemplateSpec_To_templates_ConstraintTemplateS if err := Convert_v1beta1_CRD_To_templates_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]templates.Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]templates.Target, len(*in)) + for i := range *in { + if err := Convert_v1beta1_Target_To_templates_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -329,7 +371,17 @@ func autoConvert_templates_ConstraintTemplateSpec_To_v1beta1_ConstraintTemplateS if err := Convert_templates_CRD_To_v1beta1_CRD(&in.CRD, &out.CRD, s); err != nil { return err } - out.Targets = *(*[]Target)(unsafe.Pointer(&in.Targets)) + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]Target, len(*in)) + for i := range *in { + if err := Convert_templates_Target_To_v1beta1_Target(&(*in)[i], &(*out)[i], s); err != nil { + return err + } + } + } else { + out.Targets = nil + } return nil } @@ -410,18 +462,15 @@ func autoConvert_v1beta1_Target_To_templates_Target(in *Target, out *templates.T out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]templates.Code)(unsafe.Pointer(&in.Code)) return nil } -// Convert_v1beta1_Target_To_templates_Target is an autogenerated conversion function. -func Convert_v1beta1_Target_To_templates_Target(in *Target, out *templates.Target, s conversion.Scope) error { - return autoConvert_v1beta1_Target_To_templates_Target(in, out, s) -} - func autoConvert_templates_Target_To_v1beta1_Target(in *templates.Target, out *Target, s conversion.Scope) error { out.Target = in.Target out.Rego = in.Rego out.Libs = *(*[]string)(unsafe.Pointer(&in.Libs)) + out.Code = *(*[]Code)(unsafe.Pointer(&in.Code)) return nil } @@ -430,6 +479,12 @@ func Convert_templates_Target_To_v1beta1_Target(in *templates.Target, out *Targe return autoConvert_templates_Target_To_v1beta1_Target(in, out, s) } +func autoConvert_v1beta1_Validation_To_templates_Validation(in *Validation, out *templates.Validation, s conversion.Scope) error { + // INFO: in.OpenAPIV3Schema opted out of conversion generation + out.LegacySchema = (*bool)(unsafe.Pointer(in.LegacySchema)) + return nil +} + func autoConvert_templates_Validation_To_v1beta1_Validation(in *templates.Validation, out *Validation, s conversion.Scope) error { if in.OpenAPIV3Schema != nil { in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema diff --git a/constraint/pkg/apis/templates/v1beta1/zz_generated.deepcopy.go b/constraint/pkg/apis/templates/v1beta1/zz_generated.deepcopy.go index 593de6419..27a564e25 100644 --- a/constraint/pkg/apis/templates/v1beta1/zz_generated.deepcopy.go +++ b/constraint/pkg/apis/templates/v1beta1/zz_generated.deepcopy.go @@ -81,6 +81,25 @@ func (in *CRDSpec) DeepCopy() *CRDSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Code) DeepCopyInto(out *Code) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Code. +func (in *Code) DeepCopy() *Code { + if in == nil { + return nil + } + out := new(Code) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConstraintTemplate) DeepCopyInto(out *ConstraintTemplate) { *out = *in @@ -228,6 +247,13 @@ func (in *Target) DeepCopyInto(out *Target) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Code != nil { + in, out := &in.Code, &out.Code + *out = make([]Code, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. diff --git a/constraint/pkg/client/client.go b/constraint/pkg/client/client.go index e0b9b3adf..97aba2f7c 100644 --- a/constraint/pkg/client/client.go +++ b/constraint/pkg/client/client.go @@ -11,6 +11,7 @@ import ( apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" "github.com/open-policy-agent/frameworks/constraint/pkg/client/crds" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" + regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" @@ -31,9 +32,17 @@ const statusField = "status" // allowed to continue running. Thus, this problem can only safely be handled // by the caller. type Client struct { - // driver contains the Rego runtime environments to run queries against. - // Does not require mutex locking as Driver is threadsafe. - driver drivers.Driver + // driver priority specifies the preference for which driver should + // be preferred if a template specifies multiple kinds of source + // code. It is determined by the order with which drivers are + // added to the client. + driverPriority map[string]int + + // drivers contains the drivers for policy engines understood + // by the constraint framework client. + // Does not require mutex locking as Driver is threadsafe + // and the map should be created during bootstrapping. + drivers map[string]drivers.Driver // targets are the targets supported by this Client. // Assumed to be constant after initialization. targets map[string]handler.TargetHandler @@ -45,6 +54,26 @@ type Client struct { templates map[string]*templateClient } +// driverForTemplate returns the driver to be used for a template according +// to the driver priority in the client. An empty string means the constraint +// template does not contain a language the client has a driver for. +func (c *Client) driverForTemplate(template *templates.ConstraintTemplate) string { + if len(template.Spec.Targets) == 0 { + return "" + } + language := "" + for _, v := range template.Spec.Targets[0].Code { + priority, ok := c.driverPriority[v.Engine] + if !ok { + continue + } + if priority < c.driverPriority[language] || c.driverPriority[language] == 0 { + language = v.Engine + } + } + return language +} + // CreateCRD creates a CRD from template. func (c *Client) CreateCRD(ctx context.Context, templ *templates.ConstraintTemplate) (*apiextensions.CustomResourceDefinition, error) { if templ == nil { @@ -93,7 +122,9 @@ func (c *Client) AddTemplate(ctx context.Context, templ *templates.ConstraintTem } } - if cachedCpy != nil && cachedCpy.SemanticEqual(templ) { + // if there is more than one active driver for the template, there is some cleanup to do + // from a botched driver swap. + if cachedCpy != nil && cachedCpy.SemanticEqual(templ) && len(cached.activeDrivers) == 1 { resp.Handled[targetName] = true return resp, nil } @@ -135,29 +166,72 @@ func (c *Client) AddTemplate(ctx context.Context, templ *templates.ConstraintTem return resp, err } - if err := c.driver.AddTemplate(ctx, templ); err != nil { + newDriverN := c.driverForTemplate(templ) + + driver, ok := c.drivers[newDriverN] + if !ok { + return resp, fmt.Errorf("%w: available drivers: %v, wanted %q", clienterrors.ErrNoDriver, c.driverPriority, c.driverForTemplate(templ)) + } + + // TODO: because different targets may have different code sets, + // the driver should be told which targets to load code for. + // this is moot right now, since templates only have one target + if err := driver.AddTemplate(ctx, templ); err != nil { return resp, err } templateName := templ.GetName() - template := c.templates[templateName] + cacheEntry := c.templates[templateName] // We don't want to use the usual "if found/ok" idiom here - if the value // stored for templateName is nil, we need to update it to be non-nil to avoid // a panic. - if template == nil { - template = &templateClient{ - constraints: make(map[string]*constraintClient), + if cacheEntry == nil { + cacheEntry = newTemplateClient() + c.templates[templateName] = cacheEntry + } + + cacheEntry.activeDrivers[newDriverN] = true + + // For drivers that require a local cache of constraints, we ensure that + // cache is current if the active driver has changed. + if cachedCpy != nil { + oldDriverN := c.driverForTemplate(cachedCpy) + if oldDriverN != newDriverN { + cacheEntry.needsConstraintReplay = true } + } - c.templates[templateName] = template + if cacheEntry.needsConstraintReplay { + for _, constraintEntry := range cacheEntry.constraints { + cstr := constraintEntry.getConstraint() + if err := driver.AddConstraint(ctx, cstr); err != nil { + return resp, fmt.Errorf("%w: while replaying constraints", err) + } + } + cacheEntry.needsConstraintReplay = false } - // This state mutation needs to happen last so that the semantic equal check - // at the beginning does not incorrectly return true when updating did not - // succeed previously. - template.Update(templ, crd, target) + // This state mutation needs to happen after the new driver is fully ready + // to enforce the template + cacheEntry.Update(templ, crd, target) + + // Remove old drivers last so that templates can be enforced + // despite a botched update + for oldDriverN := range cacheEntry.activeDrivers { + if oldDriverN == newDriverN { + continue + } + oldDriver, ok := c.drivers[oldDriverN] + if !ok { + return resp, fmt.Errorf("%w: while changing drivers", clienterrors.ErrNoDriver) + } + if err := oldDriver.RemoveTemplate(ctx, cachedCpy); err != nil { + return resp, fmt.Errorf("%w: while changing drivers", err) + } + delete(cacheEntry.activeDrivers, oldDriverN) + } resp.Handled[targetName] = true return resp, nil @@ -189,22 +263,33 @@ func (c *Client) RemoveTemplate(ctx context.Context, templ *templates.Constraint c.mtx.Lock() defer c.mtx.Unlock() - err := c.driver.RemoveTemplate(ctx, templ) - if err != nil { - return resp, err - } - name := templ.GetName() - template, found := c.templates[name] - + cached, found := c.templates[name] if !found { return resp, nil } + template := cached.getTemplate() + + // remove the template from all active drivers + // to ensure cleanup in case of a botched update + for driverN := range cached.activeDrivers { + driver, ok := c.drivers[driverN] + if !ok { + return resp, clienterrors.ErrNoDriver + } + + err := driver.RemoveTemplate(ctx, template) + if err != nil { + return resp, err + } + delete(cached.activeDrivers, driverN) + } + delete(c.templates, name) - for _, target := range template.targets { + for _, target := range cached.targets { resp.Handled[target.GetName()] = true } @@ -253,25 +338,32 @@ func (c *Client) AddConstraint(ctx context.Context, constraint *unstructured.Uns } kind := constraint.GetKind() - template := c.getTemplateForKind(kind) - if template == nil { + cached := c.getTemplateForKind(kind) + if cached == nil { templateName := strings.ToLower(kind) return resp, templateNotFound(templateName) } - changed, err := template.AddConstraint(constraint) + template := cached.getTemplate() + + driver, ok := c.drivers[c.driverForTemplate(template)] + if !ok { + return resp, clienterrors.ErrNoDriver + } + + changed, err := cached.AddConstraint(constraint) if err != nil { return resp, err } if changed { - err = c.driver.AddConstraint(ctx, constraint) + err = driver.AddConstraint(ctx, constraint) if err != nil { return resp, err } } - for _, target := range template.targets { + for _, target := range cached.targets { resp.Handled[target.GetName()] = true } @@ -291,25 +383,34 @@ func (c *Client) RemoveConstraint(ctx context.Context, constraint *unstructured. return resp, err } - err = c.driver.RemoveConstraint(ctx, constraint) - if err != nil { - return nil, err - } - kind := constraint.GetKind() - template := c.getTemplateForKind(kind) - if template == nil { + cached := c.getTemplateForKind(kind) + if cached == nil { // The Template has been deleted, so nothing to do and no reason to return // error. return resp, nil } - for _, target := range template.targets { + // Remove the constraint from all active drivers + // in case we are in the middle of a botched update + for driverN := range cached.activeDrivers { + driver, ok := c.drivers[driverN] + if !ok { + return resp, clienterrors.ErrNoDriver + } + + err = driver.RemoveConstraint(ctx, constraint) + if err != nil { + return nil, err + } + } + + for _, target := range cached.targets { resp.Handled[target.GetName()] = true } - template.RemoveConstraint(constraint.GetName()) + cached.RemoveConstraint(constraint.GetName()) return resp, nil } @@ -431,14 +532,20 @@ func (c *Client) AddData(ctx context.Context, data interface{}) (*types.Response continue } - err = c.driver.AddData(ctx, name, key, processedDataCpy) - if err != nil { - errMap[name] = err + // To avoid maintaining duplicate caches, only Rego should get its own + // storage. We should work to remove the need for this special case + // by building a global storage object. Right now Rego needs its own + // cache to cache constraints. + if _, ok := c.drivers[regoSchema.Name]; ok { + err = c.drivers[regoSchema.Name].AddData(ctx, name, key, processedDataCpy) + if err != nil { + errMap[name] = err - if cache != nil { - cache.Remove(key) + if cache != nil { + cache.Remove(key) + } + continue } - continue } resp.Handled[name] = true @@ -468,10 +575,16 @@ func (c *Client) RemoveData(ctx context.Context, data interface{}) (*types.Respo continue } - err = c.driver.RemoveData(ctx, target, relPath) - if err != nil { - errMap[target] = err - continue + // To avoid maintaining duplicate caches, only Rego should get its own + // storage. We should work to remove the need for this special case + // by building a global storage object. Right now Rego needs its own + // cache to cache constraints. + if _, ok := c.drivers[regoSchema.Name]; ok { + err = c.drivers[regoSchema.Name].RemoveData(ctx, target, relPath) + if err != nil { + errMap[target] = err + continue + } } resp.Handled[target] = true @@ -574,14 +687,41 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr var results []*types.Result var tracesBuilder strings.Builder - results, trace, err := c.driver.Query(ctx, target, constraints, review, opts...) - if err != nil { - return nil, err + driverToConstraints := map[string][]*unstructured.Unstructured{} + + for _, constraint := range constraints { + template, ok := c.templates[strings.ToLower(constraint.GetObjectKind().GroupVersionKind().Kind)] + if !ok { + return nil, fmt.Errorf("%w: while loading driver for constraint %s", ErrMissingConstraintTemplate, constraint.GetName()) + } + driver := c.driverForTemplate(template.template) + if driver == "" { + return nil, fmt.Errorf("%w: while loading driver for constraint %s", clienterrors.ErrNoDriver, constraint.GetName()) + } + driverToConstraints[driver] = append(driverToConstraints[driver], constraint) + } + + for driverName, driver := range c.drivers { + if len(driverToConstraints[driverName]) == 0 { + continue + } + driverResults, trace, err := driver.Query(ctx, target, driverToConstraints[driverName], review, opts...) + if err != nil { + return nil, err + } + results = append(results, driverResults...) + + if trace != nil { + tracesBuilder.WriteString(fmt.Sprintf("DRIVER %s:\n\n", driverName)) + tracesBuilder.WriteString(*trace) + tracesBuilder.WriteString("\n\n") + } } - if trace != nil { - tracesBuilder.WriteString(*trace) - tracesBuilder.WriteString("\n\n") + traceStr := tracesBuilder.String() + var trace *string + if len(traceStr) != 0 { + trace = &traceStr } return &types.Response{ @@ -593,7 +733,17 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr // Dump dumps the state of OPA to aid in debugging. func (c *Client) Dump(ctx context.Context) (string, error) { - return c.driver.Dump(ctx) + var dumpBuilder strings.Builder + for driverName, driver := range c.drivers { + dump, err := driver.Dump(ctx) + if err != nil { + return "", err + } + dumpBuilder.WriteString(fmt.Sprintf("DRIVER: %s:\n\n", driverName)) + dumpBuilder.WriteString(dump) + dumpBuilder.WriteString("\n\n") + } + return dumpBuilder.String(), nil } // knownTargets returns a sorted list of known target names. diff --git a/constraint/pkg/client/client_addtemplate_bench_test.go b/constraint/pkg/client/client_addtemplate_bench_test.go index c56f42298..fce757b2b 100644 --- a/constraint/pkg/client/client_addtemplate_bench_test.go +++ b/constraint/pkg/client/client_addtemplate_bench_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" ) @@ -241,8 +242,17 @@ func makeConstraintTemplate(i int, module string, libs ...string) *templates.Con ct.Spec.CRD.Spec.Names.Kind = kind ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: module, - Libs: libs, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: module, + Libs: libs, + }).ToUnstructured(), + }, + }, + }, }} return ct diff --git a/constraint/pkg/client/client_internal_test.go b/constraint/pkg/client/client_internal_test.go new file mode 100644 index 000000000..18f048331 --- /dev/null +++ b/constraint/pkg/client/client_internal_test.go @@ -0,0 +1,1040 @@ +package client + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest/cts" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy/schema" + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/utils/pointer" +) + +func TestMultiDriverAddTemplate(t *testing.T) { + templateA := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + templateB := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + templateC := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverC", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + // sometimes we don't care which template we use + anyTemplate := templateA.DeepCopy() + + constraint1 := cts.MakeConstraint(t, "Fakes", "constraint1") + constraint2 := cts.MakeConstraint(t, "Fakes", "constraint2") + constraint3 := cts.MakeConstraint(t, "Fakes", "constraint3") + constraints := map[string]*unstructured.Unstructured{ + "constraint1": constraint1.DeepCopy(), + "constraint2": constraint2.DeepCopy(), + } + constraintsPlus := map[string]*unstructured.Unstructured{ + "constraint1": constraint1.DeepCopy(), + "constraint2": constraint2.DeepCopy(), + "constraint3": constraint3.DeepCopy(), + } + + cleanSlate := func() (*dummy.Driver, *dummy.Driver, *dummy.Driver, *Client) { + driverA := dummy.New("driverA") + driverB := dummy.New("driverB") + driverC := dummy.New("driverC") + + client, err := NewClient( + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(driverA), + Driver(driverB), + Driver(driverC), + ) + if err != nil { + t.Fatal(err) + } + + return driverA, driverB, driverC, client + } + + bootstrapTwoConstraints := func(t *testing.T, client *Client) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if _, err := client.AddTemplate(ctx, templateA.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint1.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint2.DeepCopy()); err != nil { + t.Fatal(err) + } + } + + driverA, driverB, driverC, client := cleanSlate() + t.Run("Bootstrap State Correct", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + bootstrapTwoConstraints(t, client) + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Successful Switch", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Error On Destination AddTemplate", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + driverB.SetErrOnAddTemplate(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("expected error, got nothing") + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Recover From Error On Destination AddTemplate", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddTemplate(false) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Error On Destination AddConstraint", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddConstraint(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("expected err; got nil") + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Recover From Error On Destination AddConstraint", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddConstraint(false) + + template := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + if _, err := client.AddTemplate(ctx, template); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Error States Across Multiple Drivers Get Cleaned Up", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddConstraint(true) + driverC.SetErrOnAddConstraint(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("wanted err; got nil") + } + + if _, err := client.AddTemplate(ctx, templateC.DeepCopy()); err == nil { + t.Fatal("wanted err; got nil") + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(client.templates[anyTemplate.Name].activeDrivers) != 3 { + t.Errorf("Wanted 3 active drivers, got %d", len(client.templates[anyTemplate.Name].activeDrivers)) + } + if len(driverB.GetTemplateCode()) != 1 { + t.Errorf("Wanted 1 template in driver B; got %d", len(driverB.GetTemplateCode())) + } + if len(driverC.GetTemplateCode()) != 1 { + t.Errorf("Wanted 1 template in driver C; got %d", len(driverC.GetTemplateCode())) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + + driverB.SetErrOnAddConstraint(false) + driverC.SetErrOnAddConstraint(false) + + // now that no errors are being raised, migration should happen successfully. + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverA.GetTemplateCode()) != 0 { + t.Errorf("Wanted 0 templates in driver A; got %d", len(driverA.GetTemplateCode())) + } + if len(driverC.GetTemplateCode()) != 0 { + t.Errorf("Wanted 0 templates in driver C; got %d", len(driverC.GetTemplateCode())) + } + + resp, err = client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Adding a Constraint After Failed Migration Goes to Old Driver", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddConstraint(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("Wanted err; got nil") + } + + if _, err := client.AddConstraint(ctx, constraint3.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraintsPlus) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraintsPlus) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("But Will Migrate Successfully Once Error Clears", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverB.SetErrOnAddConstraint(false) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraintsPlus) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + + if _, err := client.RemoveConstraint(ctx, constraint3); err != nil { + t.Fatal(err) + } + }) + + t.Run("No Zombie State On Re-Migration Post Failure", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverA.SetErrOnRemoveTemplate(true) + + if _, err := client.AddConstraint(ctx, constraint3.DeepCopy()); err != nil { + t.Fatal(err) + } + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("Wanted err; got nil") + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraintsPlus) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != len(constraintsPlus) { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraintsPlus) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + + driverA.SetErrOnRemoveTemplate(false) + + if _, err := client.RemoveConstraint(ctx, constraint3.DeepCopy()); err != nil { + t.Fatal(err) + } + + // driverB and driverA should have the constraint removed + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != len(constraints) { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err = client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + + // since we only add missing constraints on migration, if driverA had stale state when it's re-activated, + // we'd expect to see zombie constraints. + if _, err := client.AddTemplate(ctx, templateA.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err = client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Adding a Constraint After Half-Completed Migration Goes to New Driver", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + bootstrapTwoConstraints(t, client) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverA.SetErrOnRemoveTemplate(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("Wanted err; got nil") + } + + if _, err := client.AddConstraint(ctx, constraint3.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != len(constraints) { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraintsPlus) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("And That Half-Completed Migrations Recover", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + driverA.SetErrOnRemoveTemplate(false) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraintsPlus)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraintsPlus) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraintsPlus)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Multi-Driver Template", func(t *testing.T) { + driverA, driverB, driverC, client = cleanSlate() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + multiDriverTemplate := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverC", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + + if _, err := client.AddTemplate(ctx, multiDriverTemplate.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint1.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint2.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverA.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Multi-Driver Template, No driverA", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + multiDriverTemplate := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverC", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + + if _, err := client.AddTemplate(ctx, multiDriverTemplate.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint1.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint2.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverB.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverB.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverB.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) + + t.Run("Multi-Driver Template, Reverse Order", func(t *testing.T) { + driverA := dummy.New("driverA") + driverB := dummy.New("driverB") + driverC := dummy.New("driverC") + + client, err := NewClient( + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(driverC), + Driver(driverB), + Driver(driverA), + ) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + multiDriverTemplate := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverC", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + + if _, err := client.AddTemplate(ctx, multiDriverTemplate.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint1.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint2.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if !reflect.DeepEqual(driverC.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverC.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + + resp, err := client.Review(ctx, handlertest.NewReview("", "foo", "bar")) + if err != nil { + t.Fatal(err) + } + if len(resp.Results()) != len(constraints) { + t.Errorf("Unexpected results: %v; wanted %d results", resp.Results(), len(constraints)) + } + for _, result := range resp.Results() { + if !strings.HasPrefix(result.Msg, fmt.Sprintf("rejected by driver %s:", driverC.Name())) { + t.Errorf("Unexpected rejection message: %v", result.Msg) + } + } + }) +} + +func TestMultiDriverRemoveTemplate(t *testing.T) { + templateA := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + templateB := cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )) + + // sometimes we don't care which template we use + anyTemplate := templateA.DeepCopy() + + constraint1 := cts.MakeConstraint(t, "Fakes", "constraint1") + constraint2 := cts.MakeConstraint(t, "Fakes", "constraint2") + constraints := map[string]*unstructured.Unstructured{ + "constraint1": constraint1.DeepCopy(), + "constraint2": constraint2.DeepCopy(), + } + + cleanSlate := func() (*dummy.Driver, *dummy.Driver, *dummy.Driver, *Client) { + driverA := dummy.New("driverA") + driverB := dummy.New("driverB") + driverC := dummy.New("driverC") + + client, err := NewClient( + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(driverA), + Driver(driverB), + Driver(driverC), + ) + if err != nil { + t.Fatal(err) + } + + return driverA, driverB, driverC, client + } + + bootstrapTwoConstraints := func(t *testing.T, client *Client) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if _, err := client.AddTemplate(ctx, templateA.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint1.DeepCopy()); err != nil { + t.Fatal(err) + } + if _, err := client.AddConstraint(ctx, constraint2.DeepCopy()); err != nil { + t.Fatal(err) + } + } + + driverA, driverB, driverC, client := cleanSlate() + t.Run("Remove Partial Migration", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + bootstrapTwoConstraints(t, client) + + driverA.SetErrOnRemoveTemplate(true) + + if _, err := client.AddTemplate(ctx, templateB.DeepCopy()); err == nil { + t.Fatal("error expected; got nil") + } + + // test desired intermediate state. + if !reflect.DeepEqual(driverA.GetConstraintsForTemplate(anyTemplate), constraints) { + t.Errorf("Missing constraints: %v", cmp.Diff(driverA.GetConstraintsForTemplate(anyTemplate), constraints)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != len(constraints) { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + + driverA.SetErrOnRemoveTemplate(false) + + if _, err := client.RemoveTemplate(ctx, templateA.DeepCopy()); err != nil { + t.Fatal(err) + } + + // test desired state. + if len(driverA.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver A has unexpected state: %v", driverA.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverB.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver B has unexpected state: %v", driverB.GetConstraintsForTemplate(anyTemplate)) + } + if len(driverC.GetConstraintsForTemplate(anyTemplate)) != 0 { + t.Errorf("Driver C has unexpected state: %v", driverC.GetConstraintsForTemplate(anyTemplate)) + } + }) +} + +// test remove template with multiple active drivers + +func TestDriverForTemplate(t *testing.T) { + tests := []struct { + name string + options []Opt + template *templates.ConstraintTemplate + expected string + }{ + { + name: "One Driver", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverA")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverA", + }, + { + name: "One Driver, Mismatch", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverA")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverNoExist", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "", + }, + { + name: "Multi Driver", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverA")), + Driver(dummy.New("driverB")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverA", + }, + { + name: "Multi Driver, Second", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverB")), + Driver(dummy.New("driverA")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverA", + }, + { + name: "One Driver, Multi-Template", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverA")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverA", + }, + { + name: "One Driver, Multi-Template Second", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverB")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverB", + }, + { + name: "Two Driver, Multi-Template", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverA")), + Driver(dummy.New("driverB")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverA", + }, + { + name: "Two Driver, Multi-Template, Second", + options: []Opt{ + Targets(&handlertest.Handler{Name: pointer.String("h1")}), + Driver(dummy.New("driverB")), + Driver(dummy.New("driverA")), + }, + template: cts.New(cts.OptTargets( + cts.TargetCustomEngines( + "h1", + cts.Code("driverA", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + cts.Code("driverB", (&schema.Source{RejectWith: "MUCH REJECTING"}).ToUnstructured()), + ), + )), + expected: "driverB", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + client, err := NewClient(test.options...) + if err != nil { + t.Fatal(err) + } + result := client.driverForTemplate(test.template) + if result != test.expected { + t.Errorf("got %v; wanted %v", result, test.expected) + } + }) + } +} diff --git a/constraint/pkg/client/client_opts.go b/constraint/pkg/client/client_opts.go index e1a16d534..31c853953 100644 --- a/constraint/pkg/client/client_opts.go +++ b/constraint/pkg/client/client_opts.go @@ -54,7 +54,11 @@ func validateTargetNames(ts []handler.TargetHandler) []string { // Driver defines the Rego execution environment. func Driver(d drivers.Driver) Opt { return func(client *Client) error { - client.driver = d + if d.Name() == "" { + return ErrNoDriverName + } + client.drivers[d.Name()] = d + client.driverPriority[d.Name()] = len(client.drivers) return nil } } diff --git a/constraint/pkg/client/client_opts_test.go b/constraint/pkg/client/client_opts_test.go new file mode 100644 index 000000000..f5d6d54f9 --- /dev/null +++ b/constraint/pkg/client/client_opts_test.go @@ -0,0 +1,26 @@ +package client + +import ( + "reflect" + "testing" + + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy" + "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" + "k8s.io/utils/pointer" +) + +func TestAddingDrivers(t *testing.T) { + c, err := NewClient(Targets(&handlertest.Handler{Name: pointer.String("foo")}), Driver(dummy.New("driver1")), Driver(dummy.New("driver2"))) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(c.driverPriority, map[string]int{"driver1": 1, "driver2": 2}) { + t.Errorf("driver priority wrong, got %v", c.driverPriority) + } + if _, ok := c.drivers["driver1"]; !ok { + t.Errorf("driver1 missing from driverset") + } + if _, ok := c.drivers["driver2"]; !ok { + t.Errorf("driver2 missing from driverset") + } +} diff --git a/constraint/pkg/client/client_test.go b/constraint/pkg/client/client_test.go index c910ae02d..206d91b21 100644 --- a/constraint/pkg/client/client_test.go +++ b/constraint/pkg/client/client_test.go @@ -12,7 +12,8 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/client" "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest" "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest/cts" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/local" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" @@ -53,7 +54,7 @@ func TestBackend_NewClient_InvalidTargetName(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -135,7 +136,7 @@ func TestClient_AddData(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -240,7 +241,7 @@ func TestClient_RemoveData(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -366,6 +367,13 @@ func TestClient_AddTemplate(t *testing.T) { wantHandled: nil, wantError: clienterrors.ErrInvalidConstraintTemplate, }, + { + name: "No Engine", + targets: []handler.TargetHandler{&handlertest.Handler{}}, + template: cts.New(cts.OptTargets(cts.TargetNoEngine(handlertest.TargetName))), + wantHandled: nil, + wantError: clienterrors.ErrNoDriver, + }, { name: "Missing Rule", targets: []handler.TargetHandler{&handlertest.Handler{}}, @@ -390,7 +398,7 @@ r = 5 for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -500,7 +508,7 @@ func TestClient_RemoveTemplate(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -562,7 +570,7 @@ func TestClient_RemoveTemplate_ByNameOnly(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -627,7 +635,7 @@ func TestClient_GetTemplate(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -694,7 +702,7 @@ func TestClient_GetTemplate_ByNameOnly(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -734,7 +742,7 @@ func TestClient_GetTemplate_ByNameOnly(t *testing.T) { func TestClient_RemoveTemplate_CascadingDelete(t *testing.T) { h := &handlertest.Handler{} - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -956,7 +964,7 @@ func TestClient_AddConstraint(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -1081,7 +1089,7 @@ func TestClient_RemoveConstraint(t *testing.T) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } @@ -1173,7 +1181,7 @@ violation[{"msg": "msg"}] { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - d, err := local.New(local.Externs(tc.allowedFields...)) + d, err := rego.New(rego.Externs(tc.allowedFields...)) if err != nil { t.Fatal(err) } @@ -1245,12 +1253,23 @@ func TestClient_CreateCRD(t *testing.T) { }, }, }, - Targets: []templates.Target{{ - Target: handlertest.TargetName, - Rego: `package foo - -violation[msg] {msg := "always"}`, - }}, + Targets: []templates.Target{ + { + Target: handlertest.TargetName, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: `package foo + + violation[msg] {msg := "always"}`, + }).ToUnstructured(), + }, + }, + }, + }, + }, }, }, want: nil, @@ -1311,17 +1330,38 @@ violation[msg] {msg := "always"}`, }, }, }, - Targets: []templates.Target{{ - Target: handlertest.TargetName, - Rego: `package foo - -violation[msg] {msg := "always"}`, - }, { - Target: "handler.2", - Rego: `package foo - -violation[msg] {msg := "always"}`, - }}, + Targets: []templates.Target{ + { + Target: handlertest.TargetName, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: `package foo + + violation[msg] {msg := "always"}`, + }).ToUnstructured(), + }, + }, + }, + }, + { + Target: "handler.2", + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: `package foo + + violation[msg] {msg := "always"}`, + }).ToUnstructured(), + }, + }, + }, + }, + }, }, }, want: nil, @@ -1340,12 +1380,23 @@ violation[msg] {msg := "always"}`, }, }, }, - Targets: []templates.Target{{ - Target: "", - Rego: `package foo - -violation[msg] {msg := "always"}`, - }}, + Targets: []templates.Target{ + { + Target: "", + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: `package foo + + violation[msg] {msg := "always"}`, + }).ToUnstructured(), + }, + }, + }, + }, + }, }, }, want: nil, @@ -1364,12 +1415,23 @@ violation[msg] {msg := "always"}`, }, }, }, - Targets: []templates.Target{{ - Target: handlertest.TargetName, - Rego: `package foo - -violation[msg] {msg := "always"}`, - }}, + Targets: []templates.Target{ + { + Target: handlertest.TargetName, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: `package foo + + violation[msg] {msg := "always"}`, + }).ToUnstructured(), + }, + }, + }, + }, + }, }, }, want: &apiextensions.CustomResourceDefinition{ @@ -1421,7 +1483,7 @@ violation[msg] {msg := "always"}`, for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ctx := context.Background() - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } diff --git a/constraint/pkg/client/clienttest/client.go b/constraint/pkg/client/clienttest/client.go index 6332020d2..1dfdea8ef 100644 --- a/constraint/pkg/client/clienttest/client.go +++ b/constraint/pkg/client/clienttest/client.go @@ -4,12 +4,12 @@ import ( "testing" "github.com/open-policy-agent/frameworks/constraint/pkg/client" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/local" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" ) func defaults() []client.Opt { - d, err := local.New() + d, err := rego.New() if err != nil { panic(err) } diff --git a/constraint/pkg/client/clienttest/cts/opts.go b/constraint/pkg/client/clienttest/cts/opts.go index 481bdcb8f..fe0ba7128 100644 --- a/constraint/pkg/client/clienttest/cts/opts.go +++ b/constraint/pkg/client/clienttest/cts/opts.go @@ -1,6 +1,7 @@ package cts import ( + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" ) @@ -66,7 +67,34 @@ func OptCRDSchema(pm PropMap) Opt { } func Target(name string, rego string, libs ...string) templates.Target { - return templates.Target{Target: name, Rego: rego, Libs: libs} + return templates.Target{ + Target: name, + Code: []templates.Code{ + Code(schema.Name, (&schema.Source{Rego: rego, Libs: libs}).ToUnstructured()), + }, + } +} + +func Code(engine string, source interface{}) templates.Code { + return templates.Code{ + Engine: engine, + Source: &templates.Anything{ + Value: source, + }, + } +} + +func TargetCustomEngines(name string, codes ...templates.Code) templates.Target { + target := templates.Target{Target: name} + target.Code = append(target.Code, codes...) + return target +} + +func TargetNoEngine(name string) templates.Target { + return templates.Target{ + Target: name, + Code: []templates.Code{}, + } } func OptTargets(targets ...templates.Target) Opt { diff --git a/constraint/pkg/client/clienttest/templates.go b/constraint/pkg/client/clienttest/templates.go index a6ef26d0d..ce2c3b014 100644 --- a/constraint/pkg/client/clienttest/templates.go +++ b/constraint/pkg/client/clienttest/templates.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" @@ -44,7 +45,16 @@ func TemplateAllow() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: ModuleAllow, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: ModuleAllow, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -74,7 +84,16 @@ func TemplateDeny() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: ModuleDeny, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: ModuleDeny, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -104,7 +123,16 @@ func TemplateDenyPrint() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleDenyPrint, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleDenyPrint, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -145,8 +173,17 @@ func TemplateDenyImport() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleImportDenyRego, - Libs: []string{moduleImportDenyLib}, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleImportDenyRego, + Libs: []string{moduleImportDenyLib}, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -179,7 +216,16 @@ func TemplateCheckData() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleCheckData, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleCheckData, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -230,7 +276,16 @@ func TemplateRuntimeError() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleRuntimeError, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleRuntimeError, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -260,7 +315,16 @@ func TemplateForbidDuplicates() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleForbidDuplicates, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleForbidDuplicates, + }).ToUnstructured(), + }, + }, + }, }} return ct @@ -291,7 +355,16 @@ func TemplateFuture() *templates.ConstraintTemplate { ct.Spec.Targets = []templates.Target{{ Target: handlertest.TargetName, - Rego: moduleFuture, + Code: []templates.Code{ + { + Engine: schema.Name, + Source: &templates.Anything{ + Value: (&schema.Source{ + Rego: moduleFuture, + }).ToUnstructured(), + }, + }, + }, }} return ct diff --git a/constraint/pkg/client/drivers/dummy/dummy.go b/constraint/pkg/client/drivers/dummy/dummy.go new file mode 100644 index 000000000..8b845333f --- /dev/null +++ b/constraint/pkg/client/drivers/dummy/dummy.go @@ -0,0 +1,217 @@ +package dummy + +import ( + "context" + "errors" + "fmt" + "strings" + "sync" + + apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy/schema" + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + "github.com/open-policy-agent/frameworks/constraint/pkg/types" + "github.com/open-policy-agent/opa/storage" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var ErrTesting = errors.New("test error") + +func New(name string) *Driver { + return &Driver{ + name: name, + code: make(map[string]string), + constraints: make(map[string]map[string]*unstructured.Unstructured), + } +} + +var _ drivers.Driver = &Driver{} + +// Driver is a threadsafe Rego environment for compiling Rego in ConstraintTemplates, +// registering Constraints, and executing queries. +type Driver struct { + name string + + errOnTemplateAdd bool + errOnTemplateRemove bool + errOnConstraintAdd bool + errOnConstraintRemove bool + + // mtx guards access to the storage and target maps. + mtx sync.RWMutex + + // code maps the template name to the rejection statement to return. + code map[string]string + + // constraints caches constraints for testing purposes. + // stored as map[lowercase(kind)][name]. + constraints map[string]map[string]*unstructured.Unstructured +} + +func (d *Driver) SetErrOnAddTemplate(raiseErr bool) { + d.mtx.Lock() + defer d.mtx.Unlock() + + d.errOnTemplateAdd = raiseErr +} + +func (d *Driver) SetErrOnRemoveTemplate(raiseErr bool) { + d.mtx.Lock() + defer d.mtx.Unlock() + + d.errOnTemplateRemove = raiseErr +} + +func (d *Driver) SetErrOnAddConstraint(raiseErr bool) { + d.mtx.Lock() + defer d.mtx.Unlock() + + d.errOnConstraintAdd = raiseErr +} + +func (d *Driver) SetErrOnRemoveConstraint(raiseErr bool) { + d.mtx.Lock() + defer d.mtx.Unlock() + + d.errOnConstraintRemove = raiseErr +} + +func (d *Driver) GetConstraintsForTemplate(template *templates.ConstraintTemplate) map[string]*unstructured.Unstructured { + d.mtx.RLock() + defer d.mtx.RUnlock() + + ret := make(map[string]*unstructured.Unstructured) + for k, v := range d.constraints[template.Name] { + ret[k] = v.DeepCopy() + } + return ret +} + +func (d *Driver) GetTemplateCode() map[string]string { + d.mtx.RLock() + defer d.mtx.RUnlock() + + ret := make(map[string]string) + for k, v := range d.code { + ret[k] = v + } + return ret +} + +// Name returns the name of the driver. +func (d *Driver) Name() string { + return d.name +} + +// AddTemplate adds templ to Driver. Normalizes modules into usable forms for +// use in queries. +func (d *Driver) AddTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { + if len(ct.Spec.Targets) != 1 { + return errors.New("wrong number of targets defined, only 1 target allowed") + } + var dummyCode templates.Code + found := false + for _, code := range ct.Spec.Targets[0].Code { + if code.Engine != d.name { + continue + } + dummyCode = code + found = true + break + } + if !found { + return errors.New("SimplePolicy code not defined") + } + + source, err := schema.GetSource(dummyCode) + if err != nil { + return err + } + + d.mtx.Lock() + defer d.mtx.Unlock() + if d.errOnTemplateAdd { + return fmt.Errorf("%w: test error for add template", ErrTesting) + } + d.code[ct.GetName()] = source.RejectWith + return nil +} + +// RemoveTemplate removes all Compilers and Constraints for templ. +// Returns nil if templ does not exist. +func (d *Driver) RemoveTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { + d.mtx.Lock() + defer d.mtx.Unlock() + if d.errOnTemplateRemove { + return fmt.Errorf("%w: test error for remove template", ErrTesting) + } + delete(d.code, ct.GetName()) + delete(d.constraints, ct.GetName()) + return nil +} + +// AddConstraint adds Constraint to storage. Used to validate state in tests. +func (d *Driver) AddConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { + d.mtx.Lock() + defer d.mtx.Unlock() + if d.errOnConstraintAdd { + return fmt.Errorf("%w: test error for add constraint", ErrTesting) + } + kind := strings.ToLower(constraint.GroupVersionKind().Kind) + if _, ok := d.constraints[kind]; !ok { + d.constraints[kind] = make(map[string]*unstructured.Unstructured) + } + + d.constraints[kind][constraint.GetName()] = constraint.DeepCopy() + + return nil +} + +// RemoveConstraint removes Constraint from Rego storage. Used to validate state in tests. +func (d *Driver) RemoveConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { + d.mtx.Lock() + defer d.mtx.Unlock() + if d.errOnConstraintRemove { + return fmt.Errorf("%w: test error for remove constraint", ErrTesting) + } + kind := strings.ToLower(constraint.GroupVersionKind().Kind) + + // if missing, consider the constraint removed + if _, ok := d.constraints[kind]; !ok { + return nil + } + + delete(d.constraints[kind], constraint.GetName()) + + return nil +} + +// AddData should not be a thing that drivers handle. +func (d *Driver) AddData(ctx context.Context, target string, path storage.Path, data interface{}) error { + return nil +} + +// RemoveData should not be a thing that drivers handle. +func (d *Driver) RemoveData(ctx context.Context, target string, path storage.Path) error { + return nil +} + +func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) ([]*types.Result, *string, error) { + results := []*types.Result{} + for i := range constraints { + constraint := constraints[i] + result := &types.Result{ + Msg: fmt.Sprintf("rejected by driver %s: %s", d.name, d.code[strings.ToLower(constraint.GetObjectKind().GroupVersionKind().Kind)]), + Constraint: constraint, + // TODO: the engine should not determine the enforcement action -- that does not work with CEL KEP + EnforcementAction: apiconstraints.EnforcementActionDeny, + } + results = append(results, result) + } + return results, nil, nil +} + +func (d *Driver) Dump(ctx context.Context) (string, error) { + return "", nil +} diff --git a/constraint/pkg/client/drivers/dummy/schema/schema.go b/constraint/pkg/client/drivers/dummy/schema/schema.go new file mode 100644 index 000000000..828dc305d --- /dev/null +++ b/constraint/pkg/client/drivers/dummy/schema/schema.go @@ -0,0 +1,49 @@ +package schema + +import ( + "errors" + "fmt" + + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var ( + ErrBadType = errors.New("Could not recognize the type") + ErrMissingField = errors.New("Rego source missing required field") +) + +type Source struct { + // RejectWith is the rejection message + RejectWith string `json:"rejectWith,omitempty"` +} + +func (in *Source) ToUnstructured() map[string]interface{} { + if in == nil { + return nil + } + + out := map[string]interface{}{} + out["rejectWith"] = in.RejectWith + + return out +} + +func GetSource(code templates.Code) (*Source, error) { + rawCode := code.Source + v, ok := rawCode.Value.(map[string]interface{}) + if !ok { + return nil, ErrBadType + } + source := &Source{} + rejectWith, found, err := unstructured.NestedString(v, "rejectWith") + if err != nil { + return nil, fmt.Errorf("%w: while extracting source", err) + } + if !found { + return nil, fmt.Errorf("%w: rejectWith", ErrMissingField) + } + source.RejectWith = rejectWith + + return source, nil +} diff --git a/constraint/pkg/client/drivers/interface.go b/constraint/pkg/client/drivers/interface.go index 1a1754d1f..38902853c 100644 --- a/constraint/pkg/client/drivers/interface.go +++ b/constraint/pkg/client/drivers/interface.go @@ -11,6 +11,10 @@ import ( // A Driver implements Rego query execution of Templates and Constraints. type Driver interface { + // Name returns the name of the driver, used to uniquely identify a driver + // and in errors returned to the user. + Name() string + // AddTemplate compiles a Template's code to be specified by // Constraints and referenced in Query. Replaces the existing Template if it // already exists. diff --git a/constraint/pkg/client/drivers/k8scel/driver.go b/constraint/pkg/client/drivers/k8scel/driver.go new file mode 100644 index 000000000..97f0ba690 --- /dev/null +++ b/constraint/pkg/client/drivers/k8scel/driver.go @@ -0,0 +1,198 @@ +package k8scel + +// import ( +// "context" +// "errors" +// "fmt" +// "strings" +// "sync" + +// apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" +// "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" +// "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" +// "github.com/open-policy-agent/frameworks/constraint/pkg/types" +// "github.com/open-policy-agent/opa/storage" +// admissionv1alpha1 "k8s.io/api/admissionregistration/v1alpha1" +// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +// "k8s.io/apimachinery/pkg/runtime" +// "k8s.io/apimachinery/pkg/runtime/schema" +// "k8s.io/apiserver/pkg/admission" +// "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" +// "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" +// ) + +// Friction log: +// there is no way to re-use the matcher interface here, as it requires an informer... not sure we need to use +// the matchers, as match Criteria should take care of things + +// "Expression" is a bit confusing, since it doesn't tell me whether "true" implies violation or not: "requirement", "mustSatisfy"? +// +// +// From the Validation help text: +// Equality on arrays with list type of 'set' or 'map' ignores element order, i.e. [1, 2] == [2, 1]. +// Concatenation on arrays with x-kubernetes-list-type use the semantics of the list type: +// Is this type metadata available shift-left? Likely not. Can the expectation be built into the operators? +// +// Other friction points are commented with the keyword FRICTION + +// const Name = "K8sValidation" + +// var _ drivers.Driver = &Driver{} + +// type Driver struct { +// mux sync.RWMutex +// compiler *validatingadmissionpolicy.CELValidatorCompiler +// validators map[string]validatingadmissionpolicy.Validator +// } + +// func New() *Driver { +// return &Driver{validators: map[string]validatingadmissionpolicy.Validator{}} +// } + +// func (d *Driver) Name() string { +// return Name +// } + +// func (d *Driver) AddTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { +// if len(ct.Spec.Targets) != 1 { +// return errors.New("wrong number of targets defined, only 1 target allowed") +// } +// var rawCode map[string]interface{} +// for _, code := range ct.Spec.Targets[0].Code { +// if code.Engine != Name { +// continue +// } +// objMap, ok := code.Source.Value.(map[string]interface{}) +// if !ok { +// return errors.New("K8sValidation code malformed") +// } +// rawCode = objMap +// break +// } +// if rawCode == nil { +// return errors.New("K8sValidation code not defined") +// } + +// validatorCode := &admissionv1alpha1.ValidatingAdmissionPolicy{} +// if err := runtime.DefaultUnstructuredConverter.FromUnstructuredWithValidation(rawCode, validatorCode, true); err != nil { +// return err +// } + +// // FRICTION: Note that compilation errors are possible, but we cannot introspect to see whether any +// // occurred +// validator := d.compiler.Compile(validatorCode) + +// d.mux.Lock() +// defer d.mux.Unlock() +// d.validators[ct.GetName()] = validator +// return nil +// } + +// func (d *Driver) RemoveTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { +// d.mux.Lock() +// defer d.mux.Unlock() +// delete(d.validators, ct.GetName()) +// return nil +// } + +// func (d *Driver) AddConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { +// return nil +// } + +// func (d *Driver) RemoveConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { +// return nil +// } + +// func (d *Driver) AddData(ctx context.Context, target string, path storage.Path, data interface{}) error { +// return nil +// } + +// func (d *Driver) RemoveData(ctx context.Context, target string, path storage.Path) error { +// return nil +// } + +// func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) ([]*types.Result, *string, error) { +// d.mux.RLock() +// defer d.mux.RUnlock() + +// typedReview, ok := review.(admission.Attributes) +// if !ok { +// return nil, nil, errors.New("cannot convert review to typed review") +// } + +// results := []*types.Result{} + +// for _, constraint := range constraints { +// // FRICTION/design question: should parameters be created as a "mock" object so that users don't have to type `params.spec.parameters`? How do we prevent visibility into other, +// // non-parameter fields, such as `spec.match`? Does it matter? Note that creating a special "parameters" object means that we'd need to copy the constraint contents to +// // a special "parameters" object for on-server enforcement with a clean value for "params", which is non-ideal. Could we provide the field of the parameters object and limit scoping to that? +// // Then how would we implement custom matchers? Maybe adding variable assignments to the Policy Definition is a better idea? That would at least allow for a convenience handle, even if +// // it doesn't scope visibility. + +// // template name is the lowercase of its kind +// validator := d.validators[strings.ToLower(constraint.GetKind())] +// if validator == nil { +// return nil, nil, fmt.Errorf("unknown constraint template validator: %s", constraint.GetKind()) +// } +// versionedAttr := &generic.VersionedAttributes{ +// Attributes: typedReview, +// VersionedKind: typedReview.GetKind(), +// VersionedOldObject: typedReview.GetOldObject(), +// VersionedObject: typedReview.GetObject(), +// } +// // FRICTION: member variables of `decision` are private, which makes it impossible to consume the results. I got around this by forking the apiserver code +// decisions, err := validator.Validate(versionedAttr, constraint) +// if err != nil { +// return nil, nil, err +// } + +// enforcementAction, found, err := unstructured.NestedString(constraint.Object, "spec", "enforcementAction") +// if err != nil { +// return nil, nil, err +// } +// if !found { +// enforcementAction = apiconstraints.EnforcementActionDeny +// } +// for _, decision := range decisions { +// if decision.Action == validatingadmissionpolicy.ActionDeny { +// results = append(results, &types.Result{ +// Target: target, +// Msg: decision.Message, +// Constraint: constraint, +// EnforcementAction: enforcementAction, +// }) +// } +// } +// } +// return results, nil, nil +// } + +// func (d *Driver) Dump(ctx context.Context) (string, error) { +// return "", nil +// } + +// // FRICTION we should not need to create mocks to use this library offline... this is used for version conversion, which +// // cannot be done reliably offline and is superfluous for audit, as audit scrapes all versions anyway +// // Currently, creating a mock that returns nil and/or errors risks the code breaking on library upgrade. It would be good +// // to have a contract here. +// var _ admission.ObjectInterfaces = &mockObjectInterfaces{} + +// type mockObjectInterfaces struct{} + +// type mockObjectCreater struct{} + +// func (m *mockObjectCreater) New(gvk schema.GroupVersionKind) (runtime.Object, error) { +// return nil, errors.New("OBJECT CREATOR NOT IMPLEMENTED") +// } + +// func (m *mockObjectInterfaces) GetObjectCreater() runtime.ObjectCreater { return &mockObjectCreater{} } + +// func (m *mockObjectInterfaces) GetObjectTyper() runtime.ObjectTyper { return nil } + +// func (m *mockObjectInterfaces) GetObjectDefaulter() runtime.ObjectDefaulter { return nil } + +// func (m *mockObjectInterfaces) GetObjectConvertor() runtime.ObjectConvertor { return nil } + +// func (m *mockObjectInterfaces) GetEquivalentResourceMapper() runtime.EquivalentResourceMapper { +// return nil +// } diff --git a/constraint/pkg/client/drivers/local/args.go b/constraint/pkg/client/drivers/rego/args.go similarity index 99% rename from constraint/pkg/client/drivers/local/args.go rename to constraint/pkg/client/drivers/rego/args.go index 8a4d8794e..9868cc308 100644 --- a/constraint/pkg/client/drivers/local/args.go +++ b/constraint/pkg/client/drivers/rego/args.go @@ -1,4 +1,4 @@ -package local +package rego import ( "fmt" diff --git a/constraint/pkg/client/drivers/local/builtin.go b/constraint/pkg/client/drivers/rego/builtin.go similarity index 98% rename from constraint/pkg/client/drivers/local/builtin.go rename to constraint/pkg/client/drivers/rego/builtin.go index ee27249f3..106ea1fce 100644 --- a/constraint/pkg/client/drivers/local/builtin.go +++ b/constraint/pkg/client/drivers/rego/builtin.go @@ -1,4 +1,4 @@ -package local +package rego import ( "net/http" diff --git a/constraint/pkg/client/drivers/local/compilers.go b/constraint/pkg/client/drivers/rego/compilers.go similarity index 88% rename from constraint/pkg/client/drivers/local/compilers.go rename to constraint/pkg/client/drivers/rego/compilers.go index 908513a9e..0fc072dea 100644 --- a/constraint/pkg/client/drivers/local/compilers.go +++ b/constraint/pkg/client/drivers/rego/compilers.go @@ -1,15 +1,19 @@ -package local +package rego import ( + "errors" "fmt" "sync" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/regorewriter" "github.com/open-policy-agent/opa/ast" ) +var ErrNoRego = errors.New("Could not extract Rego from the constraint template") + // Compilers is a threadsafe store of Compilers for ConstraintTemplates. type Compilers struct { mtx sync.RWMutex @@ -125,8 +129,9 @@ func parseConstraintTemplate(templ *templates.ConstraintTemplate, externs []stri } mods := make(map[string][]*ast.Module) - for _, target := range templ.Spec.Targets { - targetMods, err := parseConstraintTemplateTarget(rr, target) + for i := range templ.Spec.Targets { + target := templ.Spec.Targets[i] + targetMods, err := parseConstraintTemplateTarget(rr, &target) if err != nil { return nil, err } @@ -137,8 +142,24 @@ func parseConstraintTemplate(templ *templates.ConstraintTemplate, externs []stri return mods, nil } -func parseConstraintTemplateTarget(rr *regorewriter.RegoRewriter, targetSpec templates.Target) ([]*ast.Module, error) { - entryPoint, err := parseModule(templatePath, targetSpec.Rego) +func parseConstraintTemplateTarget(rr *regorewriter.RegoRewriter, targetSpec *templates.Target) ([]*ast.Module, error) { + var regoCode templates.Code + found := false + for _, code := range targetSpec.Code { + if code.Engine == schema.Name { + found = true + regoCode = code + break + } + } + if !found { + return nil, ErrNoRego + } + regoSrc, err := schema.GetSource(regoCode) + if err != nil { + return nil, err + } + entryPoint, err := parseModule(templatePath, regoSrc.Rego) if err != nil { return nil, fmt.Errorf("%w: %v", clienterrors.ErrInvalidConstraintTemplate, err) } @@ -160,7 +181,7 @@ func parseConstraintTemplateTarget(rr *regorewriter.RegoRewriter, targetSpec tem } rr.AddEntryPointModule(templatePath, entryPoint) - for idx, libSrc := range targetSpec.Libs { + for idx, libSrc := range regoSrc.Libs { libPath := fmt.Sprintf(`%s["lib_%d"]`, templateLibPrefix, idx) m, err := parseModule(libPath, libSrc) diff --git a/constraint/pkg/client/drivers/local/driver.go b/constraint/pkg/client/drivers/rego/driver.go similarity index 96% rename from constraint/pkg/client/drivers/local/driver.go rename to constraint/pkg/client/drivers/rego/driver.go index 55c5754e3..c1b546e12 100644 --- a/constraint/pkg/client/drivers/local/driver.go +++ b/constraint/pkg/client/drivers/rego/driver.go @@ -1,4 +1,4 @@ -package local +package rego import ( "bytes" @@ -13,6 +13,7 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/externaldata" @@ -73,14 +74,19 @@ type Driver struct { clientCertWatcher *certwatcher.CertWatcher } -// RegoEvaluationMeta has rego specific metadata from evaluation. -type RegoEvaluationMeta struct { +// EvaluationMeta has rego specific metadata from evaluation. +type EvaluationMeta struct { // TemplateRunTime is the number of milliseconds it took to evaluate all constraints for a template. TemplateRunTime float64 `json:"templateRunTime"` // ConstraintCount indicates how many constraints were evaluated for an underlying rego engine eval call. ConstraintCount uint `json:"constraintCount"` } +// Name returns the name of the driver. +func (d *Driver) Name() string { + return schema.Name +} + // AddTemplate adds templ to Driver. Normalizes modules into usable forms for // use in queries. func (d *Driver) AddTemplate(ctx context.Context, templ *templates.ConstraintTemplate) error { @@ -113,9 +119,13 @@ func (d *Driver) RemoveTemplate(ctx context.Context, templ *templates.Constraint d.mtx.Lock() defer d.mtx.Unlock() + if err := d.storage.removeDataEach(ctx, constraintParent); err != nil { + return err + } + d.compilers.removeTemplate(kind) delete(d.targets, kind) - return d.storage.removeDataEach(ctx, constraintParent) + return nil } // AddConstraint adds Constraint to Rego storage. Future calls to Query will @@ -291,7 +301,7 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru } for _, result := range kindResults { - result.EvaluationMeta = RegoEvaluationMeta{ + result.EvaluationMeta = EvaluationMeta{ TemplateRunTime: float64(evalEndTime.Nanoseconds()) / 1000000, ConstraintCount: uint(len(kindResults)), } diff --git a/constraint/pkg/client/drivers/local/driver_test.go b/constraint/pkg/client/drivers/rego/driver_test.go similarity index 99% rename from constraint/pkg/client/drivers/local/driver_test.go rename to constraint/pkg/client/drivers/rego/driver_test.go index b67f908d7..a4f6ba0e5 100644 --- a/constraint/pkg/client/drivers/local/driver_test.go +++ b/constraint/pkg/client/drivers/rego/driver_test.go @@ -1,4 +1,4 @@ -package local +package rego import ( "context" diff --git a/constraint/pkg/client/drivers/local/driver_unit_test.go b/constraint/pkg/client/drivers/rego/driver_unit_test.go similarity index 99% rename from constraint/pkg/client/drivers/local/driver_unit_test.go rename to constraint/pkg/client/drivers/rego/driver_unit_test.go index 2ddfd4357..2b54f5daa 100644 --- a/constraint/pkg/client/drivers/local/driver_unit_test.go +++ b/constraint/pkg/client/drivers/rego/driver_unit_test.go @@ -1,4 +1,4 @@ -package local +package rego import ( "context" @@ -172,7 +172,7 @@ func TestDriver_Query(t *testing.T) { t.Fatalf("got 0 errors on data-less query; want 1") } - stats, ok := res[0].EvaluationMeta.(RegoEvaluationMeta) + stats, ok := res[0].EvaluationMeta.(EvaluationMeta) if !ok { t.Fatalf("could not type convert to RegoEvaluationMeta") } @@ -252,6 +252,9 @@ func TestDriver_ExternalData(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + clientCertFile, err := os.CreateTemp("", "client-cert") if err != nil { t.Fatal(err) @@ -276,7 +279,7 @@ func TestDriver_ExternalData(t *testing.T) { } go func() { - _ = clientCertWatcher.Start(context.Background()) + _ = clientCertWatcher.Start(ctx) }() d, err := New( @@ -299,7 +302,6 @@ func TestDriver_ExternalData(t *testing.T) { } tmpl := cts.New(cts.OptTargets(cts.Target(cts.MockTargetHandler, ExternalData))) - ctx := context.Background() if err := d.AddTemplate(ctx, tmpl); err != nil { t.Fatalf("got AddTemplate() error = %v, want %v", err, nil) diff --git a/constraint/pkg/client/drivers/local/new.go b/constraint/pkg/client/drivers/rego/new.go similarity index 97% rename from constraint/pkg/client/drivers/local/new.go rename to constraint/pkg/client/drivers/rego/new.go index 654e1fe1d..de433764b 100644 --- a/constraint/pkg/client/drivers/local/new.go +++ b/constraint/pkg/client/drivers/rego/new.go @@ -1,4 +1,4 @@ -package local +package rego import ( "github.com/open-policy-agent/opa/rego" diff --git a/constraint/pkg/client/drivers/local/rego.go b/constraint/pkg/client/drivers/rego/rego.go similarity index 99% rename from constraint/pkg/client/drivers/local/rego.go rename to constraint/pkg/client/drivers/rego/rego.go index 00df19ed1..2037bc028 100644 --- a/constraint/pkg/client/drivers/local/rego.go +++ b/constraint/pkg/client/drivers/rego/rego.go @@ -1,4 +1,4 @@ -package local +package rego import "github.com/open-policy-agent/opa/ast" diff --git a/constraint/pkg/client/drivers/rego/schema/schema.go b/constraint/pkg/client/drivers/rego/schema/schema.go new file mode 100644 index 000000000..da1bd4c04 --- /dev/null +++ b/constraint/pkg/client/drivers/rego/schema/schema.go @@ -0,0 +1,70 @@ +package schema + +import ( + "errors" + "fmt" + + "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Name is the name of the driver. +const Name = "Rego" + +var ( + ErrBadType = errors.New("Could not recognize the type") + ErrMissingField = errors.New("Rego source missing required field") +) + +type Source struct { + // Rego holds the main code for the constraint template. The `Violations` rule is the entry point. + Rego string `json:"rego,omitempty"` + // Libs holds supporting code for the main rego library. Modules can be imported from `data.libs`. + Libs []string `json:"libs,omitempty"` +} + +func (in *Source) ToUnstructured() map[string]interface{} { + if in == nil { + return nil + } + + out := map[string]interface{}{} + + out["rego"] = in.Rego + + if in.Libs != nil { + var libs []interface{} + for _, v := range in.Libs { + libs = append(libs, v) + } + out["libs"] = libs + } + + return out +} + +func GetSource(code templates.Code) (*Source, error) { + rawCode := code.Source + v, ok := rawCode.Value.(map[string]interface{}) + if !ok { + return nil, ErrBadType + } + source := &Source{} + rego, found, err := unstructured.NestedString(v, "rego") + if err != nil { + return nil, fmt.Errorf("%w: while extracting Rego source", err) + } + if !found { + return nil, fmt.Errorf("%w: rego", ErrMissingField) + } + source.Rego = rego + + libs, found, err := unstructured.NestedStringSlice(v, "libs") + if err != nil { + return nil, fmt.Errorf("%w: while extracting Rego libs", err) + } + if found { + source.Libs = libs + } + return source, nil +} diff --git a/constraint/pkg/client/drivers/local/storages.go b/constraint/pkg/client/drivers/rego/storages.go similarity index 99% rename from constraint/pkg/client/drivers/local/storages.go rename to constraint/pkg/client/drivers/rego/storages.go index 7b6738ee7..26361a1bb 100644 --- a/constraint/pkg/client/drivers/local/storages.go +++ b/constraint/pkg/client/drivers/rego/storages.go @@ -1,4 +1,4 @@ -package local +package rego import ( "context" diff --git a/constraint/pkg/client/drivers/remote/httpclient.go b/constraint/pkg/client/drivers/remote/httpclient.go deleted file mode 100644 index e2f49fe38..000000000 --- a/constraint/pkg/client/drivers/remote/httpclient.go +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2017 The OPA Authors. All rights reserved. -// Use of this source code is governed by an Apache2 -// license that can be found in the LICENSE file. - -package remote - -import ( - "bytes" - "crypto/tls" - "crypto/x509" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" -) - -// Error contains the standard error fields returned by OPA. -type Error struct { - Status int - Message string -} - -func (err *Error) Error() string { - return fmt.Sprintf("code %v: %v", err.Status, err.Message) -} - -// Undefined represents an undefined response from OPA. -type Undefined struct{} - -func (Undefined) Error() string { - return "undefined" -} - -// IsUndefinedErr returns true if the err represents an undefined result from -// OPA. -func IsUndefinedErr(err error) bool { - _, ok := err.(Undefined) - return ok -} - -// Client defines the OPA client interface. -type client interface { - Policies - Data -} - -// Policies defines the policy management interface in OPA. -type Policies interface { - InsertPolicy(id string, bs []byte) error - DeletePolicy(id string) error - ListPolicies() (*QueryResult, error) -} - -// Data defines the interface for pushing and querying data in OPA. -type Data interface { - Prefix(path string) Data - PatchData(path string, op string, value *interface{}) error - PutData(path string, value interface{}) error - PostData(path string, value interface{}) (json.RawMessage, error) - DeleteData(path string) error - Query(path string, value interface{}) (*QueryResult, error) -} - -// New returns a new client object. -func newHTTPClient(url string, opaCAs *x509.CertPool, auth string) client { - return &httpClient{ - strings.TrimRight(url, "/"), "", opaCAs, auth, - } -} - -type httpClient struct { - url string - prefix string - opaCAs *x509.CertPool - authentication string -} - -func (c *httpClient) Prefix(path string) Data { - cpy := *c - cpy.prefix = joinPaths("/", c.prefix, path) - return &cpy -} - -func (c *httpClient) PatchData(path string, op string, value *interface{}) error { - buf, err := c.makePatch(path, op, value) - if err != nil { - return fmt.Errorf("got PatchData: %w", err) - } - resp, err := c.do("PATCH", slashPath("v1", "data"), buf) - if err != nil { - return fmt.Errorf("got PatchData: %w", err) - } - return c.handleErrors(resp) -} - -func (c *httpClient) PutData(path string, value interface{}) error { - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(value); err != nil { - return fmt.Errorf("got PutData: %w", err) - } - absPath := slashPath("v1", "data", c.prefix, path) - resp, err := c.do("PUT", absPath, &buf) - if err != nil { - return fmt.Errorf("got PutData: %w", err) - } - return c.handleErrors(resp) -} - -func (c *httpClient) PostData(path string, value interface{}) (json.RawMessage, error) { - var buf bytes.Buffer - var input struct { - Input interface{} `json:"input"` - } - input.Input = value - if err := json.NewEncoder(&buf).Encode(input); err != nil { - return nil, err - } - absPath := slashPath("v1", "data", c.prefix, path) - resp, err := c.do("POST", absPath, &buf) - if err != nil { - return nil, err - } - var result struct { - Result json.RawMessage `json:"result"` - Error map[string]interface{} `json:"error"` - } - if resp.StatusCode != 200 { - return nil, c.handleErrors(resp) - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - if result.Result == nil { - return nil, Undefined{} - } - return result.Result, nil -} - -func (c *httpClient) DeleteData(path string) error { - absPath := slashPath("v1", "data", c.prefix, path) - resp, err := c.do("DELETE", absPath, nil) - if err != nil { - return fmt.Errorf("got DeleteData: %w", err) - } - return c.handleErrors(resp) -} - -type QueryResult struct { - Explanation json.RawMessage `json:"explanation,omitempty"` - Result json.RawMessage `json:"result"` - Error map[string]interface{} `json:"error"` -} - -func (c *httpClient) Query(path string, input interface{}) (*QueryResult, error) { - var buf bytes.Buffer - var body struct { - Input interface{} `json:"input,omitempty"` - } - body.Input = input - if err := json.NewEncoder(&buf).Encode(body); err != nil { - return nil, fmt.Errorf("got encoding Query:body: %w", err) - } - absPath := slashPath("v1", "data", c.prefix, path) - method := "GET" - if input != nil { - method = "POST" - } - resp, err := c.do(method, absPath, &buf) - if err != nil { - return nil, fmt.Errorf("got running Query:do: %w", err) - } - result := &QueryResult{} - if resp.StatusCode != 200 { - return nil, fmt.Errorf("got error running Query:handleErrors: %w", c.handleErrors(resp)) - } - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - return nil, fmt.Errorf("got decoding Query: %w", err) - } - return result, nil -} - -func (c *httpClient) InsertPolicy(id string, bs []byte) error { - buf := bytes.NewBuffer(bs) - path := slashPath("v1", "policies", id) - resp, err := c.do("PUT", path, buf) - if err != nil { - return fmt.Errorf("got InsertPolicy: %w", err) - } - return c.handleErrors(resp) -} - -func (c *httpClient) DeletePolicy(id string) error { - path := slashPath("v1", "policies", id) - resp, err := c.do("DELETE", path, nil) - if err != nil { - return fmt.Errorf("got DeletePolicy: %w", err) - } - return c.handleErrors(resp) -} - -func (c *httpClient) ListPolicies() (*QueryResult, error) { - absPath := slashPath("v1", "policies", c.prefix) - resp, err := c.do("GET", absPath, nil) - if err != nil { - return nil, fmt.Errorf("got ListPolicies:do: %w", err) - } - result := &QueryResult{} - if resp.StatusCode != 200 { - return nil, fmt.Errorf("got running ListPolicies:handleErrors: %w", c.handleErrors(resp)) - } - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - return nil, fmt.Errorf("got decoding ListPolicies: %w", err) - } - return result, nil -} - -func (c *httpClient) makePatch(path, op string, value *interface{}) (io.Reader, error) { - patch := []struct { - Path string `json:"path"` - Op string `json:"op"` - Value *interface{} `json:"value,omitempty"` - }{ - { - Path: slashPath(c.prefix, path), - Op: op, - Value: value, - }, - } - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(patch); err != nil { - return nil, err - } - return &buf, nil -} - -func (c *httpClient) handleErrors(resp *http.Response) error { - defer resp.Body.Close() - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - return nil - } - msg, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("got handleErrors: %w", err) - } - return &Error{Message: string(msg), Status: resp.StatusCode} -} - -func (c *httpClient) do(verb, path string, body io.Reader) (*http.Response, error) { - url := c.url + path - req, err := http.NewRequest(verb, url, body) - if err != nil { - return nil, err - } - - if c.authentication != "" { - req.Header.Set("Authorization", "Bearer "+c.authentication) - } - - client := &http.Client{} - if strings.HasPrefix(c.url, "https") && c.opaCAs != nil { - client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ // nolint:gosec // TODO: Determine appropriate minimum TLS version. - RootCAs: c.opaCAs, - }, - } - } - - return client.Do(req) -} - -func slashPath(paths ...string) string { - return makePath("/", paths...) -} - -func makePath(join string, paths ...string) string { - return join + joinPaths(join, paths...) -} - -func joinPaths(join string, paths ...string) string { - var parts []string - for _, path := range paths { - path = strings.Trim(path, join) - if path != "" { - parts = append(parts, path) - } - } - return strings.Join(parts, join) -} diff --git a/constraint/pkg/client/drivers/remote/httpclient_test.go b/constraint/pkg/client/drivers/remote/httpclient_test.go deleted file mode 100644 index 8aad3418b..000000000 --- a/constraint/pkg/client/drivers/remote/httpclient_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2018 The OPA Authors. All rights reserved. -// Use of this source code is governed by an Apache2 -// license that can be found in the LICENSE file. - -package remote - -import ( - "encoding/json" - "io" - "reflect" - "testing" -) - -func TestHTTPClientMakePatch(t *testing.T) { - tests := []struct { - prefix string - path string - op string - value string - want string - }{ - { - prefix: "", - path: "foo", - op: "add", - value: "true", - want: `[{ - "path": "/foo", - "op": "add", - "value": true - }]`, - }, - { - prefix: "", - path: "default/foo", - op: "remove", - value: "", - want: `[{ - "path": "/default/foo", - "op": "remove" - }]`, - }, - { - prefix: "type", - path: "default/foo", - op: "remove", - value: "", - want: `[{ - "path": "/type/default/foo", - "op": "remove" - }]`, - }, - { - prefix: "/type1/subtypeA/", - path: "default/foo", - op: "remove", - value: "", - want: `[{ - "path": "/type1/subtypeA/default/foo", - "op": "remove" - }]`, - }, - } - - for _, tc := range tests { - client := &httpClient{"URL", tc.prefix, nil, ""} - var value *interface{} - - if tc.value != "" { - var x interface{} - if err := json.Unmarshal([]byte(tc.value), &x); err != nil { - panic(err) - } - value = &x - } - - patch := mustMakePatch(client, tc.path, tc.op, value) - - var expected interface{} - if err := json.Unmarshal([]byte(tc.want), &expected); err != nil { - panic(err) - } - - if !reflect.DeepEqual(patch, expected) { - t.Errorf("Expected %v but got: %v", expected, patch) - } - } -} - -func mustMakePatch(client *httpClient, path, op string, value *interface{}) interface{} { - buf, err := client.makePatch(path, op, value) - if err != nil { - panic(err) - } - - return mustUnmarshalJSON(buf) -} - -func mustUnmarshalJSON(r io.Reader) interface{} { - var x interface{} - err := json.NewDecoder(r).Decode(&x) - if err != nil { - panic(err) - } - return x -} diff --git a/constraint/pkg/client/drivers/remote/remote.go b/constraint/pkg/client/drivers/remote/remote.go deleted file mode 100644 index 70d84f8af..000000000 --- a/constraint/pkg/client/drivers/remote/remote.go +++ /dev/null @@ -1,222 +0,0 @@ -package remote - -import ( - "context" - "crypto/x509" - "encoding/json" - "errors" - "fmt" - "net/url" - "strings" - - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" - "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" - "github.com/open-policy-agent/frameworks/constraint/pkg/types" - "github.com/open-policy-agent/opa/storage" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -type Arg func(*inits) - -type inits struct { - url string - opaCAs *x509.CertPool - auth string - traceEnabled bool -} - -func URL(url string) Arg { - return func(i *inits) { - i.url = url - } -} - -func OpaCA(ca *x509.CertPool) Arg { - return func(i *inits) { - i.opaCAs = ca - } -} - -func Auth(auth string) Arg { - return func(i *inits) { - i.auth = auth - } -} - -func Tracing(enabled bool) Arg { - return func(i *inits) { - i.traceEnabled = enabled - } -} - -func New(args ...Arg) (drivers.Driver, error) { - i := &inits{} - for _, arg := range args { - arg(i) - } - if i.url == "" { - return nil, errors.New("missing URL for OPA") - } - return &driver{opa: newHTTPClient(i.url, i.opaCAs, i.auth), traceEnabled: i.traceEnabled}, nil -} - -var _ drivers.Driver = &driver{} - -type driver struct { - opa client - traceEnabled bool -} - -func (d *driver) Init() error { - return nil -} - -// Re-add once there is an implementation for Query. -// func (d *driver) addTrace(path string) string { -// return path + "?explain=full&pretty=true" -// } - -func (d *driver) PutModule(name string, src string) error { - return d.opa.InsertPolicy(name, []byte(src)) -} - -// DeleteModule deletes a rule from OPA and returns true if a rule was found and deleted, false -// if a rule was not found, and any errors. -func (d *driver) DeleteModule(name string) (bool, error) { - err := d.opa.DeletePolicy(name) - if err != nil { - e := &Error{} - if errors.As(err, &e) { - if e.Status == 404 { - return false, nil - } - } - } - return err == nil, err -} - -// AddTemplate implements drivers.Driver. -func (d *driver) AddTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { - panic("not implemented") -} - -// RemoveTemplate implements driver.Driver. -func (d *driver) RemoveTemplate(ctx context.Context, ct *templates.ConstraintTemplate) error { - panic("not implemented") -} - -func (d *driver) AddConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { - panic("not implemented") -} - -func (d *driver) RemoveConstraint(ctx context.Context, constraint *unstructured.Unstructured) error { - panic("not implemented") -} - -func (d *driver) AddData(_ context.Context, target string, path storage.Path, data interface{}) error { - return d.opa.PutData(path.String(), data) -} - -// RemoveData deletes data from OPA and returns true if data was found and deleted, false -// if data was not found, and any errors. -func (d *driver) RemoveData(_ context.Context, target string, path storage.Path) error { - err := d.opa.DeleteData(path.String()) - if err != nil { - e := &Error{} - if errors.As(err, &e) { - if e.Status == 404 { - return nil - } - } - } - - return err -} - -// makeURLPath takes a path of the form data.foo["bar.baz"].yes and converts it to an URI path -// such as /data/foo/bar.baz/yes. -func makeURLPath(path string) (string, error) { - var pieces []string - quoted := false - openBracket := false - builder := &strings.Builder{} - for _, chr := range path { - ch := string(chr) - if !quoted { - if ch == "." { - pieces = append(pieces, builder.String()) - builder.Reset() - continue - } - if ch == "[" { - if !openBracket { - openBracket = true - pieces = append(pieces, builder.String()) - builder.Reset() - continue - } else { - return "", fmt.Errorf("mismatched bracketing: %q", path) - } - } - if ch == "]" { - if openBracket { - openBracket = false - continue - } else { - return "", fmt.Errorf("mismatched bracketing: %q", path) - } - } - } - if ch == `"` { - quoted = !quoted - continue - } - _, _ = fmt.Fprint(builder, ch) - } - pieces = append(pieces, builder.String()) - - return strings.Join(pieces, "/"), nil -} - -func (d *driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) ([]*types.Result, *string, error) { - return nil, nil, nil -} - -func (d *driver) Dump(_ context.Context) (string, error) { - response, err := d.opa.Query("", nil) - if err != nil { - return "", err - } - resp := make(map[string]interface{}) - resp["data"] = response.Result - - polResponse, err := d.opa.ListPolicies() - if err != nil { - return "", err - } - pols := make([]map[string]interface{}, 0) - err = json.Unmarshal(polResponse.Result, &pols) - if err != nil { - return "", err - } - policies := make(map[string]string) - for _, v := range pols { - id, ok := v["id"] - raw, ok2 := v["raw"] - ids, ok3 := id.(string) - raws, ok4 := raw.(string) - if ok && ok2 && ok3 && ok4 { - p, err := url.PathUnescape(ids) - if err != nil { - return "", err - } - policies[p] = raws - } - } - resp["modules"] = policies - b, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/constraint/pkg/client/drivers/remote/remote_test.go b/constraint/pkg/client/drivers/remote/remote_test.go deleted file mode 100644 index 40b7a1bc5..000000000 --- a/constraint/pkg/client/drivers/remote/remote_test.go +++ /dev/null @@ -1,133 +0,0 @@ -package remote - -import ( - "context" - "encoding/json" - "errors" - "testing" -) - -type testClient struct { - queryResponse json.RawMessage -} - -func (c *testClient) InsertPolicy(id string, bs []byte) error { - return errors.New("not implemented") -} - -func (c *testClient) DeletePolicy(id string) error { - return errors.New("not implemented") -} - -func (c *testClient) ListPolicies() (*QueryResult, error) { - return nil, errors.New("not implemented") -} - -func (c *testClient) Prefix(path string) Data { - return c -} - -func (c *testClient) PatchData(path string, op string, value *interface{}) error { - return errors.New("not implemented") -} - -func (c *testClient) PutData(path string, value interface{}) error { - return errors.New("not implemented") -} - -func (c *testClient) PostData(path string, value interface{}) (json.RawMessage, error) { - return nil, errors.New("not implemented") -} - -func (c *testClient) DeleteData(path string) error { - return errors.New("not implemented") -} - -func (c *testClient) Query(path string, value interface{}) (*QueryResult, error) { - return &QueryResult{Result: c.queryResponse}, nil -} - -func newTestClient(resp string) *testClient { - return &testClient{queryResponse: json.RawMessage(resp)} -} - -const response = ` -[ - { - "msg": "totally invalid", - "metadata": {"details": {"not": "good"}}, - "constraint": { - "apiVersion": "constraints.gatekeeper.sh/v1", - "kind": "RequiredLabels", - "metadata": { - "name": "require-a-label" - }, - "spec": { - "parameters": {"hello": "world"} - } - }, - "resource": {"hi": "there"} - }, - { - "msg": "yep" - } -] -` - -func TestQuery(t *testing.T) { - t.Run("Parse Response", func(t *testing.T) { - ctx := context.Background() - - d := driver{opa: newTestClient(response)} - _, _, _ = d.Query(ctx, "random", nil, nil, nil) - }) -} - -func TestMakeURLPath(t *testing.T) { - tc := []struct { - Name string - input string - expected string - errorExpected bool - }{ - { - Name: "Simple Result", - input: "asdf", - expected: "asdf", - }, - { - Name: "Just Dots", - input: "asdf.gfgf.dsdf", - expected: "asdf/gfgf/dsdf", - }, - { - Name: "Dots and Brackets", - input: "asdf[gfgf].dsdf", - expected: "asdf/gfgf/dsdf", - }, - { - Name: "Dots and Brackets And Quotes", - input: `asdf["gfgf"].dsdf`, - expected: "asdf/gfgf/dsdf", - }, - { - Name: "Dots and Brackets And Quotes Containing Dots", - input: `asdf["gf.gf"].dsdf`, - expected: "asdf/gf.gf/dsdf", - }, - } - for _, tt := range tc { - t.Run(tt.Name, func(t *testing.T) { - res, err := makeURLPath(tt.input) - if err != nil && !tt.errorExpected { - t.Errorf("err = %s; want nil", err) - } - if err == nil && tt.errorExpected { - t.Error("err = nil; want non-nil") - } - if res != tt.expected { - t.Errorf("makeURLPath(%s) = %s; want %s", tt.input, res, tt.expected) - } - }) - } -} diff --git a/constraint/pkg/client/e2e_test.go b/constraint/pkg/client/e2e_test.go index 36f397e2d..3d755c961 100644 --- a/constraint/pkg/client/e2e_test.go +++ b/constraint/pkg/client/e2e_test.go @@ -13,7 +13,7 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest" "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest/cts" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/local" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" @@ -542,7 +542,7 @@ func TestClient_Review_Print(t *testing.T) { var printed []string printHook := appendingPrintHook{printed: &printed} - d, err := local.New(local.PrintEnabled(tc.printEnabled), local.PrintHook(printHook)) + d, err := rego.New(rego.PrintEnabled(tc.printEnabled), rego.PrintHook(printHook)) if err != nil { t.Fatal(err) } @@ -719,7 +719,7 @@ func TestE2E_Tracing(t *testing.T) { if trace == nil && tt.tracingEnabled { t.Fatal("got nil trace but tracing enabled for Review") } else if trace != nil && !tt.tracingEnabled { - t.Fatalf("got trace but tracing disabled: %v", *trace) + t.Fatalf("got trace but tracing disabled: <<%v>>", *trace) } _, err = c.AddData(ctx, &obj.Object) @@ -766,7 +766,7 @@ func TestE2E_Review_RegoEvaluationMeta(t *testing.T) { // for each result check that we have the constraintCount == 3 and a positive templateRunTime for _, result := range results { - stats, ok := result.EvaluationMeta.(local.RegoEvaluationMeta) + stats, ok := result.EvaluationMeta.(rego.EvaluationMeta) if !ok { t.Fatalf("could not type convert to RegoEvaluationMeta") } diff --git a/constraint/pkg/client/errors.go b/constraint/pkg/client/errors.go index d5271c91a..d73084d4b 100644 --- a/constraint/pkg/client/errors.go +++ b/constraint/pkg/client/errors.go @@ -6,6 +6,7 @@ import ( var ( ErrCreatingBackend = errors.New("unable to create backend") + ErrNoDriverName = errors.New("driver has no name") ErrCreatingClient = errors.New("unable to create client") ErrMissingConstraint = errors.New("missing Constraint") ErrMissingConstraintTemplate = errors.New("missing ConstraintTemplate") diff --git a/constraint/pkg/client/errors/errors.go b/constraint/pkg/client/errors/errors.go index 294599c37..25a6f211b 100644 --- a/constraint/pkg/client/errors/errors.go +++ b/constraint/pkg/client/errors/errors.go @@ -19,4 +19,5 @@ var ( ErrMissingConstraintTemplate = errors.New("missing ConstraintTemplate") ErrInvalidModule = errors.New("invalid module") ErrChangeTargets = errors.New("ConstraintTemplates with Constraints may not change targets") + ErrNoDriver = errors.New("No language driver is installed that handles this constraint template") ) diff --git a/constraint/pkg/client/new_client.go b/constraint/pkg/client/new_client.go index e19d15a36..64d153e17 100644 --- a/constraint/pkg/client/new_client.go +++ b/constraint/pkg/client/new_client.go @@ -2,12 +2,16 @@ package client import ( "fmt" + + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" ) // NewClient creates a new client. func NewClient(opts ...Opt) (*Client, error) { c := &Client{ - templates: make(map[string]*templateClient), + templates: make(map[string]*templateClient), + drivers: make(map[string]drivers.Driver), + driverPriority: make(map[string]int), } for _, opt := range opts { diff --git a/constraint/pkg/client/new_client_test.go b/constraint/pkg/client/new_client_test.go index d4e97d0d1..41211fe80 100644 --- a/constraint/pkg/client/new_client_test.go +++ b/constraint/pkg/client/new_client_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/open-policy-agent/frameworks/constraint/pkg/client" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/local" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" ) @@ -31,7 +31,7 @@ func TestNewClient(t *testing.T) { t.Run(tc.name, func(t *testing.T) { opts := tc.clientOpts - d, err := local.New() + d, err := rego.New() if err != nil { t.Fatal(err) } diff --git a/constraint/pkg/client/template_client.go b/constraint/pkg/client/template_client.go index b2975bc33..ef5fa7306 100644 --- a/constraint/pkg/client/template_client.go +++ b/constraint/pkg/client/template_client.go @@ -30,6 +30,23 @@ type templateClient struct { // this Template. This is used to validate incoming Constraints before adding // them. crd *apiextensions.CustomResourceDefinition + + // if, for some reason, there was an error adding a pre-cached constraint after + // a driver switch, AddTemplate returns an error. We should preserve that state + // so that we know a constraint replay should be attempted the next time AddTemplate + // is called. + needsConstraintReplay bool + + // activeDrivers keeps track of drivers that are in an ambiguous state due to a failed + // cross-driver update. This allows us to clean up stale state on old drivers. + activeDrivers map[string]bool +} + +func newTemplateClient() *templateClient { + return &templateClient{ + constraints: make(map[string]*constraintClient), + activeDrivers: make(map[string]bool), + } } func (e *templateClient) ValidateConstraint(constraint *unstructured.Unstructured) error { diff --git a/constraint/pkg/core/templates/constrainttemplate_types.go b/constraint/pkg/core/templates/constrainttemplate_types.go index b19b66f46..5e0a7ef01 100644 --- a/constraint/pkg/core/templates/constrainttemplate_types.go +++ b/constraint/pkg/core/templates/constrainttemplate_types.go @@ -16,10 +16,13 @@ limitations under the License. package templates import ( + "bytes" + "encoding/json" "reflect" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! @@ -55,6 +58,21 @@ type Target struct { Target string `json:"target,omitempty"` Rego string `json:"rego,omitempty"` Libs []string `json:"libs,omitempty"` + // The source code options for the constraint template, only one of this + // or "rego" can be specified. + Code []Code `json:"code,omitempty"` +} + +type Code struct { + // +kubebuilder:validation:Required + // The engine used to evaluate the code. Example: "Rego". Required. + Engine string `json:"engine,omitempty"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + // The source code for the template. Required. + Source *Anything `json:"source,omitempty"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. @@ -105,6 +123,51 @@ type ConstraintTemplateList struct { Items []ConstraintTemplate `json:"items"` } +// Anything is a struct wrapper around a field of type `interface{}` +// that plays nicely with controller-gen +// +kubebuilder:object:generate=false +// +kubebuilder:validation:Type="" +type Anything struct { + Value interface{} `json:"-"` +} + +func (in *Anything) GetValue() interface{} { + return runtime.DeepCopyJSONValue(in.Value) +} + +func (in *Anything) UnmarshalJSON(val []byte) error { + if bytes.Equal(val, []byte("null")) { + return nil + } + return json.Unmarshal(val, &in.Value) +} + +// MarshalJSON should be implemented against a value +// per http://stackoverflow.com/questions/21390979/custom-marshaljson-never-gets-called-in-go +// credit to K8s api machinery's RawExtension for finding this. +func (in Anything) MarshalJSON() ([]byte, error) { + if in.Value == nil { + return []byte("null"), nil + } + return json.Marshal(in.Value) +} + +func (in *Anything) DeepCopy() *Anything { + if in == nil { + return nil + } + + return &Anything{Value: runtime.DeepCopyJSONValue(in.Value)} +} + +func (in *Anything) DeepCopyInto(out *Anything) { + *out = *in + + if in.Value != nil { + out.Value = runtime.DeepCopyJSONValue(in.Value) + } +} + // SemanticEqual returns whether there have been changes to a constraint that // the framework should know about. It can ignore metadata as it assumes the // two comparables share the same identity. diff --git a/constraint/pkg/core/templates/zz_generated.deepcopy.go b/constraint/pkg/core/templates/zz_generated.deepcopy.go index 3fca590ed..3e0d52bde 100644 --- a/constraint/pkg/core/templates/zz_generated.deepcopy.go +++ b/constraint/pkg/core/templates/zz_generated.deepcopy.go @@ -21,7 +21,7 @@ limitations under the License. package templates import ( - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -81,6 +81,25 @@ func (in *CRDSpec) DeepCopy() *CRDSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Code) DeepCopyInto(out *Code) { + *out = *in + if in.Source != nil { + in, out := &in.Source, &out.Source + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Code. +func (in *Code) DeepCopy() *Code { + if in == nil { + return nil + } + out := new(Code) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ConstraintTemplate) DeepCopyInto(out *ConstraintTemplate) { *out = *in @@ -228,6 +247,13 @@ func (in *Target) DeepCopyInto(out *Target) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Code != nil { + in, out := &in.Code, &out.Code + *out = make([]Code, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target. diff --git a/constraint/pkg/schema/yaml_constant.go b/constraint/pkg/schema/yaml_constant.go index f93fe25f0..5ffb515af 100644 --- a/constraint/pkg/schema/yaml_constant.go +++ b/constraint/pkg/schema/yaml_constant.go @@ -22,13 +22,18 @@ spec: - name: v1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -64,6 +69,24 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -80,11 +103,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -98,7 +123,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 @@ -117,13 +143,18 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -159,6 +190,24 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -175,11 +224,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -193,7 +244,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 @@ -212,13 +264,18 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -254,6 +311,24 @@ spec: targets: items: properties: + code: + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) + items: + properties: + engine: + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' + type: string + source: + description: The source code for the template. Required. + x-kubernetes-preserve-unknown-fields: true + type: object + type: array + x-kubernetes-list-map-keys: + - engine + x-kubernetes-list-type: map libs: items: type: string @@ -270,11 +345,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -288,7 +365,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 From 47e3d0cc9d7d9b0eaa3026f7bc041f8c43fb887b Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Wed, 22 Feb 2023 18:10:29 -0800 Subject: [PATCH 2/6] Address review comments Signed-off-by: Max Smythe --- .../externaldata.gatekeeper.sh_providers.yaml | 2 +- ...tes.gatekeeper.sh_constrainttemplates.yaml | 5 +- constraint/deploy/crds.yaml | 122 +++++++++++++----- .../templates/v1/constrainttemplate_types.go | 2 +- .../v1alpha1/constrainttemplate_types.go | 2 +- .../v1beta1/constrainttemplate_types.go | 2 +- constraint/pkg/client/client.go | 12 +- constraint/pkg/schema/yaml_constant.go | 12 +- 8 files changed, 115 insertions(+), 44 deletions(-) diff --git a/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml b/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml index fa4c08075..c6e20104e 100644 --- a/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml +++ b/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null name: providers.externaldata.gatekeeper.sh spec: diff --git a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml index 018165dfe..c67dcda43 100644 --- a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml +++ b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null name: constrainttemplates.templates.gatekeeper.sh spec: @@ -80,6 +80,7 @@ spec: x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -203,6 +204,7 @@ spec: x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -326,6 +328,7 @@ spec: x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: diff --git a/constraint/deploy/crds.yaml b/constraint/deploy/crds.yaml index c583ad4f8..03c900b9e 100644 --- a/constraint/deploy/crds.yaml +++ b/constraint/deploy/crds.yaml @@ -2,7 +2,8 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null name: constrainttemplates.templates.gatekeeper.sh spec: group: templates.gatekeeper.sh @@ -17,13 +18,18 @@ spec: - name: v1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -60,17 +66,21 @@ spec: items: properties: code: - description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: "Rego". Required.' + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' type: string source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -92,11 +102,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -110,7 +122,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 @@ -129,13 +142,18 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -172,17 +190,21 @@ spec: items: properties: code: - description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: "Rego". Required.' + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' type: string source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -204,11 +226,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -222,7 +246,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 @@ -241,13 +266,18 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates API + description: ConstraintTemplate is the Schema for the constrainttemplates + API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -284,17 +314,21 @@ spec: items: properties: code: - description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) + description: The source code options for the constraint template. + "Rego" can only be specified in one place (either here or + in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: "Rego". Required.' + description: 'The engine used to evaluate the code. Example: + "Rego". Required.' type: string source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true required: - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -316,11 +350,13 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate + as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught during parsing, compiling, etc. + description: CreateCRDError represents a single error caught + during parsing, compiling, etc. properties: code: type: string @@ -334,7 +370,8 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the status + description: a unique identifier for the pod that wrote the + status type: string observedGeneration: format: int64 @@ -355,7 +392,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 creationTimestamp: null name: providers.externaldata.gatekeeper.sh spec: @@ -368,17 +405,22 @@ spec: scope: Cluster versions: - deprecated: true - deprecationWarning: externaldata.gatekeeper.sh/v1alpha1 is deprecated. Use externaldata.gatekeeper.sh/v1beta1 instead. + deprecationWarning: externaldata.gatekeeper.sh/v1alpha1 is deprecated. Use externaldata.gatekeeper.sh/v1beta1 + instead. name: v1alpha1 schema: openAPIV3Schema: description: Provider is the Schema for the Provider API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -386,13 +428,16 @@ spec: description: Spec defines the Provider specifications. properties: caBundle: - description: CABundle is a base64-encoded string that contains the TLS CA bundle in PEM format. It is used to verify the signature of the provider's certificate. + description: CABundle is a base64-encoded string that contains the + TLS CA bundle in PEM format. It is used to verify the signature + of the provider's certificate. type: string timeout: description: Timeout is the timeout when querying the provider. type: integer url: - description: URL is the url for the provider. URL is prefixed with http:// or https://. + description: URL is the url for the provider. URL is prefixed with + http:// or https://. type: string type: object type: object @@ -404,10 +449,14 @@ spec: description: Provider is the Schema for the providers API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -415,13 +464,16 @@ spec: description: Spec defines the Provider specifications. properties: caBundle: - description: CABundle is a base64-encoded string that contains the TLS CA bundle in PEM format. It is used to verify the signature of the provider's certificate. + description: CABundle is a base64-encoded string that contains the + TLS CA bundle in PEM format. It is used to verify the signature + of the provider's certificate. type: string timeout: description: Timeout is the timeout when querying the provider. type: integer url: - description: URL is the url for the provider. URL is prefixed with http:// or https://. + description: URL is the url for the provider. URL is prefixed with + http:// or https://. type: string type: object type: object diff --git a/constraint/pkg/apis/templates/v1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1/constrainttemplate_types.go index c289c0a39..ca272adbf 100644 --- a/constraint/pkg/apis/templates/v1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1/constrainttemplate_types.go @@ -76,7 +76,7 @@ type Code struct { // +kubebuilder:validation:Schemaless // +kubebuilder:pruning:PreserveUnknownFields // The source code for the template. Required. - Source *templates.Anything `json:"source,omitempty"` + Source *templates.Anything `json:"source"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go index 3da73c909..71f337e26 100644 --- a/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1alpha1/constrainttemplate_types.go @@ -75,7 +75,7 @@ type Code struct { // +kubebuilder:validation:Schemaless // +kubebuilder:pruning:PreserveUnknownFields // The source code for the template. Required. - Source *templates.Anything `json:"source,omitempty"` + Source *templates.Anything `json:"source"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go index 819ab1e04..8eda89973 100644 --- a/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go +++ b/constraint/pkg/apis/templates/v1beta1/constrainttemplate_types.go @@ -75,7 +75,7 @@ type Code struct { // +kubebuilder:validation:Schemaless // +kubebuilder:pruning:PreserveUnknownFields // The source code for the template. Required. - Source *templates.Anything `json:"source,omitempty"` + Source *templates.Anything `json:"source"` } // CreateCRDError represents a single error caught during parsing, compiling, etc. diff --git a/constraint/pkg/client/client.go b/constraint/pkg/client/client.go index 97aba2f7c..cf51484f5 100644 --- a/constraint/pkg/client/client.go +++ b/constraint/pkg/client/client.go @@ -12,6 +12,7 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/client/crds" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" @@ -277,7 +278,7 @@ func (c *Client) RemoveTemplate(ctx context.Context, templ *templates.Constraint for driverN := range cached.activeDrivers { driver, ok := c.drivers[driverN] if !ok { - return resp, clienterrors.ErrNoDriver + return resp, fmt.Errorf("%w: could not clean up %q", clienterrors.ErrNoDriver, driverN) } err := driver.RemoveTemplate(ctx, template) @@ -686,6 +687,7 @@ func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Qu func (c *Client) review(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) (*types.Response, error) { var results []*types.Result var tracesBuilder strings.Builder + var errs *errors.ErrorMap driverToConstraints := map[string][]*unstructured.Unstructured{} @@ -707,7 +709,11 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr } driverResults, trace, err := driver.Query(ctx, target, driverToConstraints[driverName], review, opts...) if err != nil { - return nil, err + if errs == nil { + errs = &clienterrors.ErrorMap{} + } + errs.Add(driverName, err) + continue } results = append(results, driverResults...) @@ -728,7 +734,7 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr Trace: trace, Target: target, Results: results, - }, nil + }, errs } // Dump dumps the state of OPA to aid in debugging. diff --git a/constraint/pkg/schema/yaml_constant.go b/constraint/pkg/schema/yaml_constant.go index 5ffb515af..b67a7deb4 100644 --- a/constraint/pkg/schema/yaml_constant.go +++ b/constraint/pkg/schema/yaml_constant.go @@ -7,7 +7,8 @@ const constraintTemplateCRDYaml = `apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 + controller-gen.kubebuilder.io/version: v0.11.3 + creationTimestamp: null name: constrainttemplates.templates.gatekeeper.sh spec: group: templates.gatekeeper.sh @@ -82,6 +83,9 @@ spec: source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true + required: + - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -203,6 +207,9 @@ spec: source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true + required: + - engine + - source type: object type: array x-kubernetes-list-map-keys: @@ -324,6 +331,9 @@ spec: source: description: The source code for the template. Required. x-kubernetes-preserve-unknown-fields: true + required: + - engine + - source type: object type: array x-kubernetes-list-map-keys: From 8a87bbccc41475d6a22d36287da1510f3b3a7ca7 Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Wed, 22 Feb 2023 18:27:34 -0800 Subject: [PATCH 3/6] Fix never-nil issue with error maps Signed-off-by: Max Smythe --- constraint/pkg/client/client.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/constraint/pkg/client/client.go b/constraint/pkg/client/client.go index cf51484f5..93e511f43 100644 --- a/constraint/pkg/client/client.go +++ b/constraint/pkg/client/client.go @@ -687,7 +687,7 @@ func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Qu func (c *Client) review(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) (*types.Response, error) { var results []*types.Result var tracesBuilder strings.Builder - var errs *errors.ErrorMap + errs := &errors.ErrorMap{} driverToConstraints := map[string][]*unstructured.Unstructured{} @@ -709,9 +709,6 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr } driverResults, trace, err := driver.Query(ctx, target, driverToConstraints[driverName], review, opts...) if err != nil { - if errs == nil { - errs = &clienterrors.ErrorMap{} - } errs.Add(driverName, err) continue } @@ -730,11 +727,20 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr trace = &traceStr } + // golang idiom is nil on no errors, so we should + // only return errs if it is non-empty, otherwise + // we get a non-nil interface (even if errs is nil, since + // the interface would still hold type info). + var errRet error + if len(*errs) > 0 { + errRet = errs + } + return &types.Response{ Trace: trace, Target: target, Results: results, - }, errs + }, errRet } // Dump dumps the state of OPA to aid in debugging. From 8d38e31763f859063db347b17adc64bb02ea76d6 Mon Sep 17 00:00:00 2001 From: Max Smythe Date: Fri, 24 Feb 2023 20:40:11 -0800 Subject: [PATCH 4/6] Address review comments Signed-off-by: Max Smythe --- constraint/pkg/client/client_internal_test.go | 50 +++++++++---------- constraint/pkg/client/client_opts.go | 3 ++ constraint/pkg/client/client_opts_test.go | 15 +++++- .../drivers/{dummy/dummy.go => fake/fake.go} | 10 ++-- .../drivers/{dummy => fake}/schema/schema.go | 0 constraint/pkg/client/errors.go | 1 + 6 files changed, 47 insertions(+), 32 deletions(-) rename constraint/pkg/client/drivers/{dummy/dummy.go => fake/fake.go} (97%) rename constraint/pkg/client/drivers/{dummy => fake}/schema/schema.go (100%) diff --git a/constraint/pkg/client/client_internal_test.go b/constraint/pkg/client/client_internal_test.go index 18f048331..33032a474 100644 --- a/constraint/pkg/client/client_internal_test.go +++ b/constraint/pkg/client/client_internal_test.go @@ -9,8 +9,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/open-policy-agent/frameworks/constraint/pkg/client/clienttest/cts" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy/schema" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/fake" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/fake/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -52,10 +52,10 @@ func TestMultiDriverAddTemplate(t *testing.T) { "constraint3": constraint3.DeepCopy(), } - cleanSlate := func() (*dummy.Driver, *dummy.Driver, *dummy.Driver, *Client) { - driverA := dummy.New("driverA") - driverB := dummy.New("driverB") - driverC := dummy.New("driverC") + cleanSlate := func() (*fake.Driver, *fake.Driver, *fake.Driver, *Client) { + driverA := fake.New("driverA") + driverB := fake.New("driverB") + driverC := fake.New("driverC") client, err := NewClient( Targets(&handlertest.Handler{Name: pointer.String("h1")}), @@ -734,9 +734,9 @@ func TestMultiDriverAddTemplate(t *testing.T) { }) t.Run("Multi-Driver Template, Reverse Order", func(t *testing.T) { - driverA := dummy.New("driverA") - driverB := dummy.New("driverB") - driverC := dummy.New("driverC") + driverA := fake.New("driverA") + driverB := fake.New("driverB") + driverC := fake.New("driverC") client, err := NewClient( Targets(&handlertest.Handler{Name: pointer.String("h1")}), @@ -820,10 +820,10 @@ func TestMultiDriverRemoveTemplate(t *testing.T) { "constraint2": constraint2.DeepCopy(), } - cleanSlate := func() (*dummy.Driver, *dummy.Driver, *dummy.Driver, *Client) { - driverA := dummy.New("driverA") - driverB := dummy.New("driverB") - driverC := dummy.New("driverC") + cleanSlate := func() (*fake.Driver, *fake.Driver, *fake.Driver, *Client) { + driverA := fake.New("driverA") + driverB := fake.New("driverB") + driverC := fake.New("driverC") client, err := NewClient( Targets(&handlertest.Handler{Name: pointer.String("h1")}), @@ -908,7 +908,7 @@ func TestDriverForTemplate(t *testing.T) { name: "One Driver", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverA")), + Driver(fake.New("driverA")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -922,7 +922,7 @@ func TestDriverForTemplate(t *testing.T) { name: "One Driver, Mismatch", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverA")), + Driver(fake.New("driverA")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -936,8 +936,8 @@ func TestDriverForTemplate(t *testing.T) { name: "Multi Driver", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverA")), - Driver(dummy.New("driverB")), + Driver(fake.New("driverA")), + Driver(fake.New("driverB")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -951,8 +951,8 @@ func TestDriverForTemplate(t *testing.T) { name: "Multi Driver, Second", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverB")), - Driver(dummy.New("driverA")), + Driver(fake.New("driverB")), + Driver(fake.New("driverA")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -966,7 +966,7 @@ func TestDriverForTemplate(t *testing.T) { name: "One Driver, Multi-Template", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverA")), + Driver(fake.New("driverA")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -981,7 +981,7 @@ func TestDriverForTemplate(t *testing.T) { name: "One Driver, Multi-Template Second", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverB")), + Driver(fake.New("driverB")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -996,8 +996,8 @@ func TestDriverForTemplate(t *testing.T) { name: "Two Driver, Multi-Template", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverA")), - Driver(dummy.New("driverB")), + Driver(fake.New("driverA")), + Driver(fake.New("driverB")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( @@ -1012,8 +1012,8 @@ func TestDriverForTemplate(t *testing.T) { name: "Two Driver, Multi-Template, Second", options: []Opt{ Targets(&handlertest.Handler{Name: pointer.String("h1")}), - Driver(dummy.New("driverB")), - Driver(dummy.New("driverA")), + Driver(fake.New("driverB")), + Driver(fake.New("driverA")), }, template: cts.New(cts.OptTargets( cts.TargetCustomEngines( diff --git a/constraint/pkg/client/client_opts.go b/constraint/pkg/client/client_opts.go index 31c853953..334d10f60 100644 --- a/constraint/pkg/client/client_opts.go +++ b/constraint/pkg/client/client_opts.go @@ -57,6 +57,9 @@ func Driver(d drivers.Driver) Opt { if d.Name() == "" { return ErrNoDriverName } + if _, ok := client.drivers[d.Name()]; ok { + return fmt.Errorf("%w: %s", ErrDuplicateDriver, d.Name()) + } client.drivers[d.Name()] = d client.driverPriority[d.Name()] = len(client.drivers) return nil diff --git a/constraint/pkg/client/client_opts_test.go b/constraint/pkg/client/client_opts_test.go index f5d6d54f9..fc21dbad2 100644 --- a/constraint/pkg/client/client_opts_test.go +++ b/constraint/pkg/client/client_opts_test.go @@ -1,16 +1,17 @@ package client import ( + "errors" "reflect" "testing" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/fake" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" "k8s.io/utils/pointer" ) func TestAddingDrivers(t *testing.T) { - c, err := NewClient(Targets(&handlertest.Handler{Name: pointer.String("foo")}), Driver(dummy.New("driver1")), Driver(dummy.New("driver2"))) + c, err := NewClient(Targets(&handlertest.Handler{Name: pointer.String("foo")}), Driver(fake.New("driver1")), Driver(fake.New("driver2"))) if err != nil { t.Fatal(err) } @@ -24,3 +25,13 @@ func TestAddingDrivers(t *testing.T) { t.Errorf("driver2 missing from driverset") } } + +func TestNoDuplicates(t *testing.T) { + _, err := NewClient(Targets(&handlertest.Handler{Name: pointer.String("foo")}), Driver(fake.New("driver1")), Driver(fake.New("driver1"))) + if err == nil { + t.Fatal("expected error, got none") + } + if !errors.Is(err, ErrDuplicateDriver) { + t.Errorf("wanted duplicate driver error, got %v", err) + } +} diff --git a/constraint/pkg/client/drivers/dummy/dummy.go b/constraint/pkg/client/drivers/fake/fake.go similarity index 97% rename from constraint/pkg/client/drivers/dummy/dummy.go rename to constraint/pkg/client/drivers/fake/fake.go index 8b845333f..8cdfb479d 100644 --- a/constraint/pkg/client/drivers/dummy/dummy.go +++ b/constraint/pkg/client/drivers/fake/fake.go @@ -1,4 +1,4 @@ -package dummy +package fake import ( "context" @@ -9,7 +9,7 @@ import ( apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" - "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/dummy/schema" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/fake/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/types" "github.com/open-policy-agent/opa/storage" @@ -110,13 +110,13 @@ func (d *Driver) AddTemplate(ctx context.Context, ct *templates.ConstraintTempla if len(ct.Spec.Targets) != 1 { return errors.New("wrong number of targets defined, only 1 target allowed") } - var dummyCode templates.Code + var fakeCode templates.Code found := false for _, code := range ct.Spec.Targets[0].Code { if code.Engine != d.name { continue } - dummyCode = code + fakeCode = code found = true break } @@ -124,7 +124,7 @@ func (d *Driver) AddTemplate(ctx context.Context, ct *templates.ConstraintTempla return errors.New("SimplePolicy code not defined") } - source, err := schema.GetSource(dummyCode) + source, err := schema.GetSource(fakeCode) if err != nil { return err } diff --git a/constraint/pkg/client/drivers/dummy/schema/schema.go b/constraint/pkg/client/drivers/fake/schema/schema.go similarity index 100% rename from constraint/pkg/client/drivers/dummy/schema/schema.go rename to constraint/pkg/client/drivers/fake/schema/schema.go diff --git a/constraint/pkg/client/errors.go b/constraint/pkg/client/errors.go index d73084d4b..8d11f51e9 100644 --- a/constraint/pkg/client/errors.go +++ b/constraint/pkg/client/errors.go @@ -7,6 +7,7 @@ import ( var ( ErrCreatingBackend = errors.New("unable to create backend") ErrNoDriverName = errors.New("driver has no name") + ErrDuplicateDriver = errors.New("duplicate drivers of the same name") ErrCreatingClient = errors.New("unable to create client") ErrMissingConstraint = errors.New("missing Constraint") ErrMissingConstraintTemplate = errors.New("missing ConstraintTemplate") From b11a0fe456effd8f645ddb8fa1bde2a35eda092b Mon Sep 17 00:00:00 2001 From: Alex Pana <8968914+acpana@users.noreply.github.com> Date: Fri, 27 Jan 2023 07:59:43 +0000 Subject: [PATCH 5/6] feat: add instrumentation primitives modify driver interface to take a QueryResponse on Query() func calls for future backwards compat Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> remove unused code Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> remove NamedStats Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> make value interface Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> make value interface{} Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> rename StatsEntry.Key to .Scope Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> reshape stats Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> pull out err from QueryResponse, rt nil Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> add driver specific opt Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> move StatsEntries to Responses Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> add labels, source struct Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> use Source in GetDescription Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> add target label Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> fix description string Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> --- .../externaldata.gatekeeper.sh_providers.yaml | 2 +- ...tes.gatekeeper.sh_constrainttemplates.yaml | 2 +- constraint/deploy/crds.yaml | 119 +++------- constraint/pkg/client/client.go | 64 +++++- constraint/pkg/client/drivers/fake/fake.go | 5 +- constraint/pkg/client/drivers/interface.go | 7 +- constraint/pkg/client/drivers/query_opts.go | 37 ++- constraint/pkg/client/drivers/rego/args.go | 10 + constraint/pkg/client/drivers/rego/driver.go | 111 ++++++--- .../client/drivers/rego/driver_unit_test.go | 211 ++++++++++++++++-- constraint/pkg/client/drivers/types.go | 16 ++ constraint/pkg/client/e2e_test.go | 182 ++++++++++++--- .../pkg/instrumentation/instrumentation.go | 54 +++++ constraint/pkg/types/validation.go | 16 +- 14 files changed, 643 insertions(+), 193 deletions(-) create mode 100644 constraint/pkg/client/drivers/types.go create mode 100644 constraint/pkg/instrumentation/instrumentation.go diff --git a/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml b/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml index c6e20104e..fa4c08075 100644 --- a/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml +++ b/constraint/config/crds/externaldata.gatekeeper.sh_providers.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: providers.externaldata.gatekeeper.sh spec: diff --git a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml index c67dcda43..c93cac629 100644 --- a/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml +++ b/constraint/config/crds/templates.gatekeeper.sh_constrainttemplates.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: constrainttemplates.templates.gatekeeper.sh spec: diff --git a/constraint/deploy/crds.yaml b/constraint/deploy/crds.yaml index 03c900b9e..61bed51f4 100644 --- a/constraint/deploy/crds.yaml +++ b/constraint/deploy/crds.yaml @@ -2,8 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.10.0 name: constrainttemplates.templates.gatekeeper.sh spec: group: templates.gatekeeper.sh @@ -18,18 +17,13 @@ spec: - name: v1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates - API + description: ConstraintTemplate is the Schema for the constrainttemplates API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -66,14 +60,11 @@ spec: items: properties: code: - description: The source code options for the constraint template. - "Rego" can only be specified in one place (either here or - in the "rego" field) + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: - "Rego". Required.' + description: 'The engine used to evaluate the code. Example: "Rego". Required.' type: string source: description: The source code for the template. Required. @@ -102,13 +93,11 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate - as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught - during parsing, compiling, etc. + description: CreateCRDError represents a single error caught during parsing, compiling, etc. properties: code: type: string @@ -122,8 +111,7 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the - status + description: a unique identifier for the pod that wrote the status type: string observedGeneration: format: int64 @@ -142,18 +130,13 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates - API + description: ConstraintTemplate is the Schema for the constrainttemplates API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -190,14 +173,11 @@ spec: items: properties: code: - description: The source code options for the constraint template. - "Rego" can only be specified in one place (either here or - in the "rego" field) + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: - "Rego". Required.' + description: 'The engine used to evaluate the code. Example: "Rego". Required.' type: string source: description: The source code for the template. Required. @@ -226,13 +206,11 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate - as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught - during parsing, compiling, etc. + description: CreateCRDError represents a single error caught during parsing, compiling, etc. properties: code: type: string @@ -246,8 +224,7 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the - status + description: a unique identifier for the pod that wrote the status type: string observedGeneration: format: int64 @@ -266,18 +243,13 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: ConstraintTemplate is the Schema for the constrainttemplates - API + description: ConstraintTemplate is the Schema for the constrainttemplates API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -314,14 +286,11 @@ spec: items: properties: code: - description: The source code options for the constraint template. - "Rego" can only be specified in one place (either here or - in the "rego" field) + description: The source code options for the constraint template. "Rego" can only be specified in one place (either here or in the "rego" field) items: properties: engine: - description: 'The engine used to evaluate the code. Example: - "Rego". Required.' + description: 'The engine used to evaluate the code. Example: "Rego". Required.' type: string source: description: The source code for the template. Required. @@ -350,13 +319,11 @@ spec: properties: byPod: items: - description: ByPodStatus defines the observed state of ConstraintTemplate - as seen by an individual controller + description: ByPodStatus defines the observed state of ConstraintTemplate as seen by an individual controller properties: errors: items: - description: CreateCRDError represents a single error caught - during parsing, compiling, etc. + description: CreateCRDError represents a single error caught during parsing, compiling, etc. properties: code: type: string @@ -370,8 +337,7 @@ spec: type: object type: array id: - description: a unique identifier for the pod that wrote the - status + description: a unique identifier for the pod that wrote the status type: string observedGeneration: format: int64 @@ -392,7 +358,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.11.3 + controller-gen.kubebuilder.io/version: v0.10.0 creationTimestamp: null name: providers.externaldata.gatekeeper.sh spec: @@ -405,22 +371,17 @@ spec: scope: Cluster versions: - deprecated: true - deprecationWarning: externaldata.gatekeeper.sh/v1alpha1 is deprecated. Use externaldata.gatekeeper.sh/v1beta1 - instead. + deprecationWarning: externaldata.gatekeeper.sh/v1alpha1 is deprecated. Use externaldata.gatekeeper.sh/v1beta1 instead. name: v1alpha1 schema: openAPIV3Schema: description: Provider is the Schema for the Provider API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -428,16 +389,13 @@ spec: description: Spec defines the Provider specifications. properties: caBundle: - description: CABundle is a base64-encoded string that contains the - TLS CA bundle in PEM format. It is used to verify the signature - of the provider's certificate. + description: CABundle is a base64-encoded string that contains the TLS CA bundle in PEM format. It is used to verify the signature of the provider's certificate. type: string timeout: description: Timeout is the timeout when querying the provider. type: integer url: - description: URL is the url for the provider. URL is prefixed with - http:// or https://. + description: URL is the url for the provider. URL is prefixed with http:// or https://. type: string type: object type: object @@ -449,14 +407,10 @@ spec: description: Provider is the Schema for the providers API properties: apiVersion: - description: 'APIVersion defines the versioned schema of this representation - of an object. Servers should convert recognized schemas to the latest - internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string kind: - description: 'Kind is a string value representing the REST resource this - object represents. Servers may infer this from the endpoint the client - submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' type: string metadata: type: object @@ -464,16 +418,13 @@ spec: description: Spec defines the Provider specifications. properties: caBundle: - description: CABundle is a base64-encoded string that contains the - TLS CA bundle in PEM format. It is used to verify the signature - of the provider's certificate. + description: CABundle is a base64-encoded string that contains the TLS CA bundle in PEM format. It is used to verify the signature of the provider's certificate. type: string timeout: description: Timeout is the timeout when querying the provider. type: integer url: - description: URL is the url for the provider. URL is prefixed with - http:// or https://. + description: URL is the url for the provider. URL is prefixed with http:// or https://. type: string type: object type: object diff --git a/constraint/pkg/client/client.go b/constraint/pkg/client/client.go index 93e511f43..d2084a2cf 100644 --- a/constraint/pkg/client/client.go +++ b/constraint/pkg/client/client.go @@ -16,6 +16,7 @@ import ( clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" + "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" "github.com/open-policy-agent/frameworks/constraint/pkg/types" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -607,7 +608,7 @@ func (c *Client) RemoveData(ctx context.Context, data interface{}) (*types.Respo // Review makes sure the provided object satisfies all stored constraints. // On error, the responses return value will still be populated so that // partial results can be analyzed. -func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.QueryOpt) (*types.Responses, error) { +func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Opt) (*types.Responses, error) { responses := types.NewResponses() errMap := make(clienterrors.ErrorMap) @@ -661,7 +662,7 @@ func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Qu for target, review := range reviews { constraints := constraintsByTarget[target] - resp, err := c.review(ctx, target, constraints, review, opts...) + resp, stats, err := c.review(ctx, target, constraints, review, opts...) if err != nil { errMap.Add(target, err) continue @@ -675,6 +676,18 @@ func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Qu resp.Sort() responses.ByTarget[target] = resp + if stats != nil { + // add the target label to these stats for future collation. + targetLabel := &instrumentation.Label{Name: "target", Value: target} + for _, stat := range stats { + if stat.Labels == nil || len(stat.Labels) == 0 { + stat.Labels = []*instrumentation.Label{targetLabel} + } else { + stat.Labels = append(stat.Labels, targetLabel) + } + } + responses.StatsEntries = append(responses.StatsEntries, stats...) + } } if len(errMap) == 0 { @@ -684,8 +697,9 @@ func (c *Client) Review(ctx context.Context, obj interface{}, opts ...drivers.Qu return responses, &errMap } -func (c *Client) review(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) (*types.Response, error) { +func (c *Client) review(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.Opt) (*types.Response, []*instrumentation.StatsEntry, error) { var results []*types.Result + var stats []*instrumentation.StatsEntry var tracesBuilder strings.Builder errs := &errors.ErrorMap{} @@ -694,11 +708,11 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr for _, constraint := range constraints { template, ok := c.templates[strings.ToLower(constraint.GetObjectKind().GroupVersionKind().Kind)] if !ok { - return nil, fmt.Errorf("%w: while loading driver for constraint %s", ErrMissingConstraintTemplate, constraint.GetName()) + return nil, nil, fmt.Errorf("%w: while loading driver for constraint %s", ErrMissingConstraintTemplate, constraint.GetName()) } driver := c.driverForTemplate(template.template) if driver == "" { - return nil, fmt.Errorf("%w: while loading driver for constraint %s", clienterrors.ErrNoDriver, constraint.GetName()) + return nil, nil, fmt.Errorf("%w: while loading driver for constraint %s", clienterrors.ErrNoDriver, constraint.GetName()) } driverToConstraints[driver] = append(driverToConstraints[driver], constraint) } @@ -707,17 +721,21 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr if len(driverToConstraints[driverName]) == 0 { continue } - driverResults, trace, err := driver.Query(ctx, target, driverToConstraints[driverName], review, opts...) + qr, err := driver.Query(ctx, target, driverToConstraints[driverName], review, opts...) if err != nil { errs.Add(driverName, err) continue } - results = append(results, driverResults...) + if qr != nil { + results = append(results, qr.Results...) + + stats = append(stats, qr.StatsEntries...) - if trace != nil { - tracesBuilder.WriteString(fmt.Sprintf("DRIVER %s:\n\n", driverName)) - tracesBuilder.WriteString(*trace) - tracesBuilder.WriteString("\n\n") + if qr.Trace != nil { + tracesBuilder.WriteString(fmt.Sprintf("DRIVER %s:\n\n", driverName)) + tracesBuilder.WriteString(*qr.Trace) + tracesBuilder.WriteString("\n\n") + } } } @@ -740,7 +758,7 @@ func (c *Client) review(ctx context.Context, target string, constraints []*unstr Trace: trace, Target: target, Results: results, - }, errRet + }, stats, errRet } // Dump dumps the state of OPA to aid in debugging. @@ -758,6 +776,28 @@ func (c *Client) Dump(ctx context.Context) (string, error) { return dumpBuilder.String(), nil } +func (c *Client) GetDescriptionForStat(source instrumentation.Source, statName string) string { + if source.Type != instrumentation.EngineSourceType { + return instrumentation.UnknownDescription + } + + // TODO use source.Value once rebased on + // https://github.com/open-policy-agent/frameworks/pull/293 + switch source.Value { + case "rego": + // TODO rework + // desc, err := d.GetDescriptionForStat(statName) + // if err != nil { + // return instrumentation.UnknownDescription + // } + + // return desc + return instrumentation.UnknownDescription + default: + return instrumentation.UnknownDescription + } +} + // knownTargets returns a sorted list of known target names. func (c *Client) knownTargets() []string { var knownTargets []string diff --git a/constraint/pkg/client/drivers/fake/fake.go b/constraint/pkg/client/drivers/fake/fake.go index 8cdfb479d..ce3c6ffb1 100644 --- a/constraint/pkg/client/drivers/fake/fake.go +++ b/constraint/pkg/client/drivers/fake/fake.go @@ -197,7 +197,7 @@ func (d *Driver) RemoveData(ctx context.Context, target string, path storage.Pat return nil } -func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) ([]*types.Result, *string, error) { +func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.Opt) (*drivers.QueryResponse, error) { results := []*types.Result{} for i := range constraints { constraint := constraints[i] @@ -209,7 +209,8 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru } results = append(results, result) } - return results, nil, nil + + return &drivers.QueryResponse{Results: results}, nil } func (d *Driver) Dump(ctx context.Context) (string, error) { diff --git a/constraint/pkg/client/drivers/interface.go b/constraint/pkg/client/drivers/interface.go index 38902853c..62e77b8d1 100644 --- a/constraint/pkg/client/drivers/interface.go +++ b/constraint/pkg/client/drivers/interface.go @@ -4,7 +4,6 @@ import ( "context" "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" - "github.com/open-policy-agent/frameworks/constraint/pkg/types" "github.com/open-policy-agent/opa/storage" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -40,11 +39,9 @@ type Driver interface { RemoveData(ctx context.Context, target string, path storage.Path) error // Query runs the passed target's Constraints against review. - // - // Returns results for each violated Constraint. - // Returns a trace if specified in query options or enabled at Driver creation. + // Returns a QueryResponse type. // Returns an error if there was a problem executing the Query. - Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...QueryOpt) ([]*types.Result, *string, error) + Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...Opt) (*QueryResponse, error) // Dump outputs the entire state of compiled Templates, added Constraints, and // cached data used for referential Constraints. diff --git a/constraint/pkg/client/drivers/query_opts.go b/constraint/pkg/client/drivers/query_opts.go index 404c32ebb..b35b9c40b 100644 --- a/constraint/pkg/client/drivers/query_opts.go +++ b/constraint/pkg/client/drivers/query_opts.go @@ -1,16 +1,41 @@ package drivers -type QueryCfg struct { +type Options struct { + DriverCfg *DriverCfg + EngineCfg *EngineCfg +} + +type EngineCfg struct { TracingEnabled bool } -// QueryOpt specifies optional arguments for Rego queries. -type QueryOpt func(*QueryCfg) +// Opt specifies optional arguments for constraint framework +// client APIs. +type Opt func(*Options) // Tracing enables Rego tracing for a single query. // If tracing is enabled for the Driver, Tracing(false) does not disable Tracing. -func Tracing(enabled bool) QueryOpt { - return func(cfg *QueryCfg) { - cfg.TracingEnabled = enabled +func Tracing(enabled bool) Opt { + return func(cfg *Options) { + if cfg.EngineCfg == nil { + cfg.EngineCfg = &EngineCfg{} + } + cfg.EngineCfg.TracingEnabled = enabled + } +} + +type DriverCfg struct { + StatsEnabled bool +} + +// Stats(true) enables the driver to gather evaluation stats for a single +// query. If stats is enabled for the Driver at construction time, then +// Stats(false) does not disable Stats for this single query. +func Stats(enabled bool) Opt { + return func(cfg *Options) { + if cfg.DriverCfg == nil { + cfg.DriverCfg = &DriverCfg{} + } + cfg.DriverCfg.StatsEnabled = enabled } } diff --git a/constraint/pkg/client/drivers/rego/args.go b/constraint/pkg/client/drivers/rego/args.go index 9868cc308..ae21df98d 100644 --- a/constraint/pkg/client/drivers/rego/args.go +++ b/constraint/pkg/client/drivers/rego/args.go @@ -154,6 +154,16 @@ func Externs(externs ...string) Arg { } } +// GatherMetrics starts collecting the various metrics around the +// underlying engine's eval() calls. +func GatherMetrics() Arg { + return func(driver *Driver) error { + driver.gatherMetrics = true + + return nil + } +} + // Currently rules should only access data.inventory. var validDataFields = map[string]bool{ "inventory": true, diff --git a/constraint/pkg/client/drivers/rego/driver.go b/constraint/pkg/client/drivers/rego/driver.go index c1b546e12..9a2bff332 100644 --- a/constraint/pkg/client/drivers/rego/driver.go +++ b/constraint/pkg/client/drivers/rego/driver.go @@ -12,6 +12,8 @@ import ( "time" "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" + "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" @@ -31,6 +33,12 @@ import ( const ( libRoot = "data.lib" violation = "violation" + + templateRunTimeNanosName = "templateRunTimeNanos" + templateRunTimeMillisDescription = "the number of nanoseconds it took to evaluate all constraints for a template" + + constraintCountName = "constraintCount" + constraintCountDescription = "the number of constraints that were evaluated at the same time for the given constraint kind" ) var _ drivers.Driver = &Driver{} @@ -72,14 +80,9 @@ type Driver struct { // clientCertWatcher is a watcher for the TLS certificate used to communicate with providers. clientCertWatcher *certwatcher.CertWatcher -} -// EvaluationMeta has rego specific metadata from evaluation. -type EvaluationMeta struct { - // TemplateRunTime is the number of milliseconds it took to evaluate all constraints for a template. - TemplateRunTime float64 `json:"templateRunTime"` - // ConstraintCount indicates how many constraints were evaluated for an underlying rego engine eval call. - ConstraintCount uint `json:"constraintCount"` + // gatherMetrics controls whether the driver attempts to gather any metrics around its API calls + gatherMetrics bool } // Name returns the name of the driver. @@ -188,11 +191,12 @@ func (d *Driver) RemoveData(ctx context.Context, target string, path storage.Pat // input is the already-parsed Rego Value to use as input. // Returns the Rego results, the trace if requested, or an error if there was // a problem executing the query. -func (d *Driver) eval(ctx context.Context, compiler *ast.Compiler, target string, path []string, input ast.Value, opts ...drivers.QueryOpt) (rego.ResultSet, *string, error) { - cfg := &drivers.QueryCfg{} +func (d *Driver) eval(ctx context.Context, compiler *ast.Compiler, target string, path []string, input ast.Value, opts ...drivers.Opt) (rego.ResultSet, *string, error) { + allOptions := &drivers.Options{} for _, opt := range opts { - opt(cfg) + opt(allOptions) } + cfg := allOptions.EngineCfg queryPath := strings.Builder{} queryPath.WriteString("data") @@ -216,7 +220,7 @@ func (d *Driver) eval(ctx context.Context, compiler *ast.Compiler, target string } buf := topdown.NewBufferTracer() - if d.traceEnabled || cfg.TracingEnabled { + if d.traceEnabled || (cfg != nil && cfg.TracingEnabled) { args = append(args, rego.QueryTracer(buf)) } @@ -224,7 +228,7 @@ func (d *Driver) eval(ctx context.Context, compiler *ast.Compiler, target string res, err := r.Eval(ctx) var t *string - if d.traceEnabled || cfg.TracingEnabled { + if d.traceEnabled || (cfg != nil && cfg.TracingEnabled) { b := &bytes.Buffer{} topdown.PrettyTrace(b, *buf) t = pointer.String(b.String()) @@ -233,9 +237,9 @@ func (d *Driver) eval(ctx context.Context, compiler *ast.Compiler, target string return res, t, err } -func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.QueryOpt) ([]*types.Result, *string, error) { +func (d *Driver) Query(ctx context.Context, target string, constraints []*unstructured.Unstructured, review interface{}, opts ...drivers.Opt) (*drivers.QueryResponse, error) { if len(constraints) == 0 { - return nil, nil, nil + return nil, nil } constraintsByKind := toConstraintsByKind(constraints) @@ -250,12 +254,21 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru // once per call to Query instead of once per compiler. reviewMap, err := toInterfaceMap(review) if err != nil { - return nil, nil, err + return nil, err } d.mtx.RLock() defer d.mtx.RUnlock() + allOptions := &drivers.Options{} + for _, opt := range opts { + opt(allOptions) + } + driverCfg := allOptions.DriverCfg + engineCfg := allOptions.EngineCfg + + var statsEntries []*instrumentation.StatsEntry + for kind, kindConstraints := range constraintsByKind { evalStartTime := time.Now() compiler := d.compilers.getCompiler(target, kind) @@ -263,14 +276,14 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru // The Template was just removed, so the Driver is in an inconsistent // state with Client. Raise this as an error rather than attempting to // continue. - return nil, nil, fmt.Errorf("missing Template %q for target %q", kind, target) + return nil, fmt.Errorf("missing Template %q for target %q", kind, target) } // Parse input into an ast.Value to avoid round-tripping through JSON when // possible. parsedInput, err := toParsedInput(target, kindConstraints, reviewMap) if err != nil { - return nil, nil, err + return nil, err } resultSet, trace, err := d.eval(ctx, compiler, target, path, parsedInput, opts...) @@ -297,25 +310,58 @@ func (d *Driver) Query(ctx context.Context, target string, constraints []*unstru kindResults, err := drivers.ToResults(constraintsMap, resultSet) if err != nil { - return nil, nil, err - } - - for _, result := range kindResults { - result.EvaluationMeta = EvaluationMeta{ - TemplateRunTime: float64(evalEndTime.Nanoseconds()) / 1000000, - ConstraintCount: uint(len(kindResults)), - } + return nil, err } results = append(results, kindResults...) + + if d.gatherMetrics || (driverCfg != nil && driverCfg.StatsEnabled) { + statsEntries = append(statsEntries, + &instrumentation.StatsEntry{ + Scope: instrumentation.TemplateScope, + StatsFor: kind, + Stats: []*instrumentation.Stat{ + { + Name: templateRunTimeNanosName, + Value: uint64(evalEndTime.Nanoseconds()), + Source: instrumentation.Source{ + Type: instrumentation.EngineSourceType, + Value: instrumentation.RegoEngineSource, + }, + }, + { + Name: constraintCountName, + Value: len(kindConstraints), + Source: instrumentation.Source{ + Type: instrumentation.EngineSourceType, + Value: instrumentation.RegoEngineSource, + }, + }, + }, + Labels: []*instrumentation.Label{ + { + Name: "TracingEnabled", + Value: d.traceEnabled || (engineCfg != nil && engineCfg.TracingEnabled), + }, + { + Name: "PrintEnabled", + Value: d.printEnabled, + }, + { + Name: "EnableExternalDataClientAuth", + Value: d.enableExternalDataClientAuth, + }, + }, + }) + } } traceString := traceBuilder.String() if len(traceString) != 0 { - return results, &traceString, nil + return &drivers.QueryResponse{Results: results, Trace: &traceString, StatsEntries: statsEntries}, nil } - return results, nil, nil + return &drivers.QueryResponse{Results: results, StatsEntries: statsEntries}, nil } func (d *Driver) Dump(ctx context.Context) (string, error) { @@ -358,6 +404,17 @@ func (d *Driver) Dump(ctx context.Context) (string, error) { return string(b), nil } +func (d *Driver) GetDescriptionForStat(statName string) (string, error) { + switch statName { + case templateRunTimeNanosName: + return templateRunTimeMillisDescription, nil + case constraintCountName: + return constraintCountDescription, nil + default: + return "", fmt.Errorf("unknown stat name") + } +} + func (d *Driver) getTLSCertificate() (*tls.Certificate, error) { if !d.enableExternalDataClientAuth { return nil, nil diff --git a/constraint/pkg/client/drivers/rego/driver_unit_test.go b/constraint/pkg/client/drivers/rego/driver_unit_test.go index 2b54f5daa..c25dc405e 100644 --- a/constraint/pkg/client/drivers/rego/driver_unit_test.go +++ b/constraint/pkg/client/drivers/rego/driver_unit_test.go @@ -44,6 +44,14 @@ fooisbar[msg] { } ` + NeverViolate string = ` + package foobar + + violation[{"msg": "always violate"}] { + false + } +` + ExternalData string = ` package foobar @@ -140,7 +148,7 @@ func TestDriver_Query(t *testing.T) { t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) } - res, _, err := d.Query( + qr, err := d.Query( ctx, cts.MockTargetHandler, []*unstructured.Unstructured{cts.MakeConstraint(t, "Fakes", "foo-1")}, @@ -149,7 +157,7 @@ func TestDriver_Query(t *testing.T) { if err != nil { t.Fatalf("got Query() error = %v, want %v", err, nil) } - if len(res) == 0 { + if len(qr.Results) == 0 { t.Fatalf("got 0 errors on normal query; want 1") } @@ -159,7 +167,7 @@ func TestDriver_Query(t *testing.T) { t.Fatalf("got RemoveData() error = %v, want %v", err, nil) } - res, _, err = d.Query( + qr, err = d.Query( ctx, cts.MockTargetHandler, []*unstructured.Unstructured{cts.MakeConstraint(t, "Fakes", "foo-1")}, @@ -168,24 +176,185 @@ func TestDriver_Query(t *testing.T) { if err != nil { t.Fatalf("got Query() (#2) error = %v, want %v", err, nil) } - if len(res) == 0 { + if len(qr.Results) == 0 { t.Fatalf("got 0 errors on data-less query; want 1") } - - stats, ok := res[0].EvaluationMeta.(EvaluationMeta) - if !ok { - t.Fatalf("could not type convert to RegoEvaluationMeta") - } - - if stats.TemplateRunTime == 0 { - t.Fatalf("expected %v's value to be positive was zero", "TemplateRunTime") - } - - if stats.ConstraintCount != uint(1) { - t.Fatalf("expected %v constraint count, got %v", 1, "ConstraintCount") - } } +// // TestDriver_Query_StatsEntries tests that StatsEntries are populated for Query calls. +// func TestDriver_Query_StatsEntries(t *testing.T) { +// // given a ConstraintTemplate that always violates +// d, err := New(GatherMetrics()) +// if err != nil { +// t.Fatal(err) +// } + +// tmpl := cts.New(cts.OptTargets(cts.Target(cts.MockTargetHandler, AlwaysViolate))) +// ctx := context.Background() +// if err := d.AddTemplate(ctx, tmpl); err != nil { +// t.Fatalf("got AddTemplate() error = %v, want %v", err, nil) +// } + +// if err := d.AddConstraint(ctx, cts.MakeConstraint(t, cts.MockTemplate, "foo-0")); err != nil { +// t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) +// } +// if err := d.AddConstraint(ctx, cts.MakeConstraint(t, cts.MockTemplate, "foo-1")); err != nil { +// t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) +// } +// if err := d.AddConstraint(ctx, cts.MakeConstraint(t, cts.MockTemplate, "foo-2")); err != nil { +// t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) +// } + +// // when a driver makes a Query call +// res, _, err := d.Query( +// ctx, +// cts.MockTargetHandler, +// []*unstructured.Unstructured{cts.MakeConstraint(t, cts.MockTemplate, "foo-0"), cts.MakeConstraint(t, cts.MockTemplate, "foo-1")}, +// map[string]interface{}{"hello": "there"}, +// ) +// if err != nil { +// t.Fatalf("got Query() error = %v, want %v", err, nil) +// } +// if len(res) != 2 { +// t.Fatalf("got %d errors on normal query; want 2", len(res)) +// } + +// // then expext two stats entries that correspond to the +// // two constraints that were passed in to the Query call +// ses := d.statsEntries +// if len(ses) != 2 { +// t.Fatalf("got %d errors on normal query; want 2", len(ses)) +// } +// ck0, ok0 := ses[0].Key.(drivers.ConstraintKey) +// if !ok0 { +// t.Fatalf("cannot convert to ConstraintKey") +// } +// if ck0.Kind != cts.MockTemplate && ck0.Kind != "foo-0" { +// t.Fatalf("did not find constraint key %+v in the stats entries", ck0) +// } +// namedStats := ses[0].AllStats +// if len(namedStats) != 1 { +// t.Fatalf("expected 1 namedStats but got %d", len(namedStats)) +// } + +// // it's enough to just check that the first named stats is set properly +// if namedStats[0].StatsName != "EvaluationMetadata" { +// t.Fatalf("named stats unknown: %s", namedStats[0].StatsName) +// } +// if len(namedStats[0].Stats) != 2 { +// t.Fatalf("expected 2 stats but got %d", len(namedStats[0].Stats)) +// } + +// // check that we have both the run time and the engine set +// stats := namedStats[0].Stats +// if stats[0].Name != "templateRunTimeMillis" { +// t.Fatalf("unknown stat: %s", stats[0].Name) +// } +// if stats[1].Name != "engineType" { +// t.Fatalf("unknown stat: %s", stats[1].Name) +// } else if stats[1].Value != "rego" { +// t.Fatalf("expected: rego, got: %s", stats[1].Value) +// } + +// // just check for another key's existence since values are added programaitcally +// ck1, ok1 := ses[1].Key.(drivers.ConstraintKey) +// if !ok1 { +// t.Fatalf("cannot convert to ConstraintKey") +// } +// if ck1.Kind != cts.MockTemplate && ck1.Kind != "foo-1" { +// t.Fatalf("did not find constraint key %+v in the stats entries", ck1) +// } + +// // when a subsequent Query call is made +// res2, _, err2 := d.Query( +// ctx, +// cts.MockTargetHandler, +// []*unstructured.Unstructured{cts.MakeConstraint(t, cts.MockTemplate, "foo-2")}, +// map[string]interface{}{"hello": "there"}, +// ) +// if err2 != nil { +// t.Fatalf("got Query() error = %v, want %v", err, nil) +// } +// if len(res2) == 0 { +// t.Fatalf("got %d errors on normal query; want 1", len(res)) +// } + +// // then expect a new StatsEntry entirely, with no +// // leaks of the previous data in it. +// ses2 := d.statsEntries +// if reflect.DeepEqual(ses, ses2) { +// t.Fatalf("got the same slice on a new Query call") +// } +// for _, se := range ses2 { +// ck, ok := se.Key.(drivers.ConstraintKey) +// if !ok { +// t.Fatalf("cannot convert to ConstraintKey") +// } + +// for _, otherSe := range ses { +// otherCk, ok := otherSe.Key.(drivers.ConstraintKey) +// if !ok { +// t.Fatalf("cannot convert to ConstraintKey") +// } + +// if otherCk == ck { +// t.Fatalf("did not expect to find the previous stats entries after the second Query call") +// } +// } +// } +// } + +// // TestDriver_Query_StatsEntries_nonViolating tests that StatsEntries are populated for +// // Query calls when constrainst don't violate. +// func TestDriver_Query_StatsEntries_nonViolating(t *testing.T) { +// // given a ConstraintTemplate that never violates +// d, err := New(GatherMetrics()) +// if err != nil { +// t.Fatal(err) +// } + +// tmpl := cts.New(cts.OptTargets(cts.Target(cts.MockTargetHandler, NeverViolate))) +// ctx := context.Background() +// if err := d.AddTemplate(ctx, tmpl); err != nil { +// t.Fatalf("got AddTemplate() error = %v, want %v", err, nil) +// } + +// if err := d.AddConstraint(ctx, cts.MakeConstraint(t, cts.MockTemplate, "foo-0")); err != nil { +// t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) +// } + +// // when a driver makes a Query call +// res, _, err := d.Query( +// ctx, +// cts.MockTargetHandler, +// []*unstructured.Unstructured{cts.MakeConstraint(t, cts.MockTemplate, "foo-0")}, +// map[string]interface{}{"hi": "there"}, +// ) +// if err != nil { +// t.Fatalf("got Query() error = %v, want %v", err, nil) +// } +// if len(res) != 0 { +// t.Fatalf("got %d errors on normal query; want 0", len(res)) +// } + +// // then we want to see a stats entry for the non violating constraint +// ses := d.statsEntries +// if len(ses) != 1 { +// t.Fatalf("got %d errors on normal query; want 1", len(ses)) +// } +// ck, ok := ses[0].Key.(drivers.ConstraintKey) +// if !ok { +// t.Fatalf("cannot convert to ConstraintKey") +// } +// if ck.Kind != cts.MockTemplate && ck.Kind != "foo-0" { +// t.Fatalf("did not find constraint key %+v in the stats entries", ck) +// } +// namedStats := ses[0].AllStats +// if len(namedStats) != 1 { +// t.Fatalf("expected 1 namedStats but got %d", len(namedStats)) +// } +// } + func TestDriver_ExternalData(t *testing.T) { for _, tt := range []struct { name string @@ -311,7 +480,7 @@ func TestDriver_ExternalData(t *testing.T) { t.Fatalf("got AddConstraint() error = %v, want %v", err, nil) } - res, _, err := d.Query( + qr, err := d.Query( ctx, cts.MockTargetHandler, []*unstructured.Unstructured{cts.MakeConstraint(t, "Fakes", "foo-1")}, @@ -320,11 +489,11 @@ func TestDriver_ExternalData(t *testing.T) { if err != nil { t.Fatalf("got Query() error = %v, want %v", err, nil) } - if tt.errorExpected && len(res) == 0 { + if tt.errorExpected && len(qr.Results) == 0 { t.Fatalf("got 0 errors on normal query; want 1") } - if !tt.errorExpected && len(res) > 0 { - t.Fatalf("got %d errors on normal query; want 0", len(res)) + if !tt.errorExpected && len(qr.Results) > 0 { + t.Fatalf("got %d errors on normal query; want 0", len(qr.Results)) } }) } diff --git a/constraint/pkg/client/drivers/types.go b/constraint/pkg/client/drivers/types.go new file mode 100644 index 000000000..7e0b9738b --- /dev/null +++ b/constraint/pkg/client/drivers/types.go @@ -0,0 +1,16 @@ +package drivers + +import ( + "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" + "github.com/open-policy-agent/frameworks/constraint/pkg/types" +) + +// QueryResponse encapsulates the values returned on Query: +// - Results includes a Result for each violated Constraint. +// - Trace is the evaluation trace on Query if specified in query options or enabled at Driver creation. +// - StatsEntries include any Stats that the engine gathered on Query. +type QueryResponse struct { + Results []*types.Result + Trace *string + StatsEntries []*instrumentation.StatsEntry +} diff --git a/constraint/pkg/client/e2e_test.go b/constraint/pkg/client/e2e_test.go index 3d755c961..e26ddb9f9 100644 --- a/constraint/pkg/client/e2e_test.go +++ b/constraint/pkg/client/e2e_test.go @@ -3,6 +3,7 @@ package client_test import ( "context" "errors" + "reflect" "strconv" "testing" @@ -18,6 +19,7 @@ import ( "github.com/open-policy-agent/frameworks/constraint/pkg/core/templates" "github.com/open-policy-agent/frameworks/constraint/pkg/handler" "github.com/open-policy-agent/frameworks/constraint/pkg/handler/handlertest" + "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" "github.com/open-policy-agent/frameworks/constraint/pkg/types" "github.com/open-policy-agent/opa/topdown/print" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -440,7 +442,7 @@ func TestClient_Review(t *testing.T) { results := responses.Results() - diffOpt := cmpopts.IgnoreFields(types.Result{}, "Metadata", "EvaluationMeta") + diffOpt := cmpopts.IgnoreFields(types.Result{}, "Metadata") if diff := cmp.Diff(tt.wantResults, results, diffOpt); diff != "" { t.Error(diff) } @@ -488,8 +490,7 @@ func TestClient_Review_Details(t *testing.T) { results := responses.Results() - diffOpt := cmpopts.IgnoreFields(types.Result{}, "EvaluationMeta") - if diff := cmp.Diff(want, results, diffOpt); diff != "" { + if diff := cmp.Diff(want, results); diff != "" { t.Error(diff) } } @@ -569,7 +570,7 @@ func TestClient_Review_Print(t *testing.T) { results := rsps.Results() if diff := cmp.Diff(tc.wantResults, results, - cmpopts.IgnoreFields(types.Result{}, "Metadata", "EvaluationMeta")); diff != "" { + cmpopts.IgnoreFields(types.Result{}, "Metadata")); diff != "" { t.Error(diff) } @@ -607,7 +608,7 @@ func TestE2E_RemoveConstraint(t *testing.T) { EnforcementAction: constraints.EnforcementActionDeny, }} - if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(types.Result{}, "Metadata", "EvaluationMeta")); diff != "" { + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(types.Result{}, "Metadata")); diff != "" { t.Fatal(diff) } @@ -656,7 +657,7 @@ func TestE2E_RemoveTemplate(t *testing.T) { EnforcementAction: constraints.EnforcementActionDeny, }} - if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(types.Result{}, "Metadata", "EvaluationMeta")); diff != "" { + if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(types.Result{}, "Metadata")); diff != "" { t.Fatal(diff) } @@ -730,24 +731,105 @@ func TestE2E_Tracing(t *testing.T) { } } -// TestE2E_Review_RegoEvaluationMeta tests that we can get stats out of evaluated constraints. -func TestE2E_Review_RegoEvaluationMeta(t *testing.T) { +func TestE2E_DriverCfg(t *testing.T) { + tests := []struct { + name string + statsEnabled bool + }{ + { + name: "disabled", + statsEnabled: false, + }, + { + name: "enabled", + statsEnabled: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + c := clienttest.New(t) + + _, err := c.AddTemplate(ctx, clienttest.TemplateDeny()) + if err != nil { + t.Fatal(err) + } + + _, err = c.AddConstraint(ctx, cts.MakeConstraint(t, clienttest.KindDeny, "foo")) + if err != nil { + t.Fatal(err) + } + + obj := handlertest.Review{Object: handlertest.Object{Name: "bar"}} + + rsps, err := c.Review(ctx, obj, drivers.Stats(tt.statsEnabled)) + if err != nil { + t.Fatal(err) + } + + stats := rsps.StatsEntries + if stats == nil && tt.statsEnabled { + t.Fatal("got nil stats but stats enabled for Review") + } else if len(stats) != 0 && !tt.statsEnabled { + t.Fatal("got stats but stats disabled") + } + + _, err = c.AddData(ctx, &obj.Object) + if err != nil { + t.Fatal(err) + } + }) + } +} + +// TestE2E_Review_StatsEntries tests that we can get stats out of evaluated constraints. +// In particular, this test makes sure we have stats for both violating constraint kind, name pairs +// and non violating ones. +func TestE2E_Review_StatsEntries(t *testing.T) { + t.Skip("todo while rebasing") + ctx := context.Background() - c := clienttest.New(t) - ct := clienttest.TemplateCheckData() - _, err := c.AddTemplate(ctx, ct) + d, err := rego.New(rego.GatherMetrics()) + if err != nil { + t.Fatal(err) + } + testHandler := &handlertest.Handler{} + c, err := client.NewClient(client.Targets(testHandler), client.Driver(d)) + if err != nil { + t.Fatal(err) + } + + _, err = c.AddTemplate(ctx, clienttest.TemplateCheckData()) + if err != nil { + t.Fatal(err) + } + _, err = c.AddTemplate(ctx, clienttest.TemplateForbidDuplicates()) if err != nil { t.Fatal(err) } - numConstrains := 3 - for i := 1; i < numConstrains+1; i++ { + numConstrains := 3 + kindSet := map[string]struct{}{} + for i := 0; i < numConstrains; i++ { name := "constraint-" + strconv.Itoa(i) - constraint := cts.MakeConstraint(t, clienttest.KindCheckData, name, cts.WantData("bar")) - _, err = c.AddConstraint(ctx, constraint) + + _, err = c.AddConstraint(ctx, cts.MakeConstraint(t, clienttest.KindCheckData, name, cts.WantData("bar"))) + if err != nil { + t.Fatal(err) + } + kindSet[clienttest.KindCheckData] = struct{}{} + + _, err = c.AddConstraint(ctx, cts.MakeConstraint(t, clienttest.KindForbidDuplicates, name)) if err != nil { t.Fatal(err) } + kindSet[clienttest.KindForbidDuplicates] = struct{}{} + } + + // sanity check + if 2 != len(kindSet) { + t.Fatalf("set up failed") } review := handlertest.Review{ @@ -762,21 +844,69 @@ func TestE2E_Review_RegoEvaluationMeta(t *testing.T) { t.Fatal(err) } - results := responses.Results() + for _, se := range responses.StatsEntries { + if se.Scope == instrumentation.TemplateScope { + if se.Stats == nil { + t.Errorf("missing stats for: %v missing stats", se.StatsFor) + } - // for each result check that we have the constraintCount == 3 and a positive templateRunTime - for _, result := range results { - stats, ok := result.EvaluationMeta.(rego.EvaluationMeta) - if !ok { - t.Fatalf("could not type convert to RegoEvaluationMeta") - } + for _, stat := range se.Stats { + // now check that descriptions are able to be fetched. + desc := c.GetDescriptionForStat(instrumentation.RegoSource, stat.Name) + + switch stat.Name { + case "templateRunTimeNanos": + want := "the number of nanoseconds it took to evaluate all constraints for a template" + if desc != want { + t.Errorf("want: %s, got: %s", want, desc) + } + case "constraintCount": + want := "the number of constraints that were evaluated at the same time for the given constraint kind" + if desc != want { + t.Errorf("want: %s, got: %s", want, desc) + } + } + + if stat.Source.Value != instrumentation.RegoEngineSource { + t.Errorf("the only supported source for now is \"rego\" was: %s", stat.Source.Value) + } - if stats.TemplateRunTime == 0 { - t.Fatalf("expected %v's value to be positive was zero", "TemplateRunTime") + switch actualValue := stat.Value.(type) { + case uint64: + case int: + if !(actualValue > 0) { + t.Errorf("expected positive value for stat: %s; got: %d", stat.Name, actualValue) + } + default: + t.Errorf("unknown stat value type: %T for stat: %s", actualValue, actualValue) + } + } + + if se.Labels == nil || len(se.Labels) == 0 { + t.Errorf("expected labels but did not find any") + } + + foundTargetLabel := false + for _, label := range se.Labels { + if label.Name == "target" && label.Value == testHandler.GetName() { + foundTargetLabel = true + break + } + } + + if !foundTargetLabel { + t.Fatalf("did not find target label for: %s", testHandler.GetName()) + } + + delete(kindSet, se.StatsFor) + } else { + t.Fatalf("encountered an unknown Key: %s", reflect.ValueOf(se.Scope)) } + } - if stats.ConstraintCount != uint(numConstrains) { - t.Fatalf("expected %v constraint count, got %v", numConstrains, "ConstraintCount") + if len(kindSet) != 0 { + for key := range kindSet { + t.Errorf("did not see stats for ConstraintKey: %v", key) } } } diff --git a/constraint/pkg/instrumentation/instrumentation.go b/constraint/pkg/instrumentation/instrumentation.go new file mode 100644 index 000000000..f8efc37a4 --- /dev/null +++ b/constraint/pkg/instrumentation/instrumentation.go @@ -0,0 +1,54 @@ +// package instrumentation defines primitives to +// support more observability throughout the constraint framework. +package instrumentation + +const ( + // scope constants. + TemplateScope = "template" + + // description constants. + UnknownDescription = "unknown description" + + // source constants. + EngineSourceType = "engine" + RegoEngineSource = "rego" +) + +var RegoSource = Source{ + Type: EngineSourceType, + Value: RegoEngineSource, +} + +// Label is a name/value tuple to add metadata +// about a StatsEntry. +type Label struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// Source is a special Stat level label of the form type/value +// that is meant to be used to identify where a Stat is coming from. +type Source struct { + Type string `json:"type"` + Value string `json:"value"` +} + +// Stat is a Name, Value, Description tuple that may contain +// metrics or aggregations of metrics. +type Stat struct { + Name string `json:"name"` + Value interface{} `json:"value"` + Source Source `json:"source"` +} + +// StatsEntry comprises of a generalized Key for all associated Stats. +type StatsEntry struct { + // Scope is the level of granularity that the Stats + // were created at. + Scope string `json:"scope"` + // StatsFor is the specific kind of Scope type that Stats + // were created for. + StatsFor string `json:"statsFor"` + Stats []*Stat `json:"stats"` + Labels []*Label `json:"labels,omitempty"` +} diff --git a/constraint/pkg/types/validation.go b/constraint/pkg/types/validation.go index 0c79de7e4..2b3b58912 100644 --- a/constraint/pkg/types/validation.go +++ b/constraint/pkg/types/validation.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/davecgh/go-spew/spew" + "github.com/open-policy-agent/frameworks/constraint/pkg/instrumentation" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -23,13 +24,10 @@ type Result struct { // The enforcement action of the constraint EnforcementAction string `json:"enforcementAction,omitempty"` - - // EvaluationMeta has metadata for a Result's evaluation. - EvaluationMeta interface{} `json:"evaluationMeta,omitempty"` } // Response is a collection of Constraint violations for a particular Target. -// Each Result is for a distinct Constraint. +// Each Result represents a violation for a distinct Constraint. type Response struct { Trace *string Target string @@ -77,14 +75,16 @@ func (r *Response) TraceDump() string { func NewResponses() *Responses { return &Responses{ - ByTarget: make(map[string]*Response), - Handled: make(map[string]bool), + ByTarget: make(map[string]*Response), + Handled: make(map[string]bool), + StatsEntries: make([]*instrumentation.StatsEntry, 0), } } type Responses struct { - ByTarget map[string]*Response - Handled map[string]bool + ByTarget map[string]*Response + Handled map[string]bool + StatsEntries []*instrumentation.StatsEntry } func (r *Responses) Results() []*Result { From 2183ca9892a4772473a99003e30406002666793c Mon Sep 17 00:00:00 2001 From: Alex Pana <8968914+acpana@users.noreply.github.com> Date: Wed, 1 Mar 2023 19:30:37 +0000 Subject: [PATCH 6/6] rework GetDescriptionForStat Signed-off-by: Alex Pana <8968914+acpana@users.noreply.github.com> --- constraint/pkg/client/client.go | 35 +++++++++++++++++++------------ constraint/pkg/client/e2e_test.go | 2 -- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/constraint/pkg/client/client.go b/constraint/pkg/client/client.go index d2084a2cf..8b0aeb1b2 100644 --- a/constraint/pkg/client/client.go +++ b/constraint/pkg/client/client.go @@ -11,6 +11,7 @@ import ( apiconstraints "github.com/open-policy-agent/frameworks/constraint/pkg/apis/constraints" "github.com/open-policy-agent/frameworks/constraint/pkg/client/crds" "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers" + "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego" regoSchema "github.com/open-policy-agent/frameworks/constraint/pkg/client/drivers/rego/schema" "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" clienterrors "github.com/open-policy-agent/frameworks/constraint/pkg/client/errors" @@ -778,24 +779,32 @@ func (c *Client) Dump(ctx context.Context) (string, error) { func (c *Client) GetDescriptionForStat(source instrumentation.Source, statName string) string { if source.Type != instrumentation.EngineSourceType { + // only handle engine source for now return instrumentation.UnknownDescription } - // TODO use source.Value once rebased on - // https://github.com/open-policy-agent/frameworks/pull/293 - switch source.Value { - case "rego": - // TODO rework - // desc, err := d.GetDescriptionForStat(statName) - // if err != nil { - // return instrumentation.UnknownDescription - // } + // this is written in a general form + // but it only works for rego drivers for now. + for dName, d := range c.drivers { + if strings.EqualFold(dName, source.Value) { + if source.Value == instrumentation.RegoSource.Value { + // no other drivers implement the GetDescriptionForStat yet + regoD, ok := d.(*rego.Driver) + if !ok { + return instrumentation.UnknownDescription + } - // return desc - return instrumentation.UnknownDescription - default: - return instrumentation.UnknownDescription + desc, err := regoD.GetDescriptionForStat(statName) + if err != nil { + return instrumentation.UnknownDescription + } + + return desc + } + } } + + return instrumentation.UnknownDescription } // knownTargets returns a sorted list of known target names. diff --git a/constraint/pkg/client/e2e_test.go b/constraint/pkg/client/e2e_test.go index e26ddb9f9..225609973 100644 --- a/constraint/pkg/client/e2e_test.go +++ b/constraint/pkg/client/e2e_test.go @@ -787,8 +787,6 @@ func TestE2E_DriverCfg(t *testing.T) { // In particular, this test makes sure we have stats for both violating constraint kind, name pairs // and non violating ones. func TestE2E_Review_StatsEntries(t *testing.T) { - t.Skip("todo while rebasing") - ctx := context.Background() d, err := rego.New(rego.GatherMetrics()) if err != nil {