Repository: deislabs/ratify
Pull request #1402 (2024-04-17 13:11:11)

feat: add NamespacedPolicy CRD [multi-tenancy PR 7]
# Description

## What this PR does / why we need it:
This is the 7th PR for multi-tenancy support. Please review https://github.com/deislabs/ratify/pull/1395 first. Check diff between PR 6 and PR 7 at: https://github.com/binbin-li/ratify/pull/123

1. Created new CRD: NamespacedPolicy for the namespaced Policy resource.
2. Added controller for the NamespacedPolicy and moved some common logics beween Policy Controller and Namespaced Policy Controller to util methods.
3. Updated the implementation of `ActivePolicies` so that it fully supports accessing Policies per namespace.
4. Will add new e2e tests for namespaced CRs once all CRDs supports namespace.

## Which issue(s) this PR fixes *(optional, using `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when the PR gets merged)*:
Fixes #

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Helm Chart Change (any edit/addition/update that is necessary for changes merged to the `main` branch)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Please also list any relevant details for your test configuration

- [ ] Test A
- [ ] Test B

# Checklist:

- [ ] Does the affected code have corresponding tests?
- [ ] Are the changes documented, not just with inline documentation, but also with conceptual documentation such as an overview of a new feature, or task-based documentation like a tutorial? Consider if this change should be announced on your project blog.
- [ ] Does this introduce breaking changes that would require an announcement or bumping the major version?
- [ ] Do all new files have appropriate license header?

# Post Merge Requirements
- [ ] MAINTAINERS: manually trigger the "Publish Package" workflow after merging any PR that indicates `Helm Chart Change`


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/deislabs/ratify/pull/1402?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) Report
Attention: Patch coverage is `77.65957%` with `42 lines` in your changes are missing coverage. Please review.
> Project coverage is 66.14%. Comparing base [(`73ef709`)](https://app.codecov.io/gh/deislabs/ratify/commit/73ef7091e919c18ad37c7ec0a6e00b5471db3748?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) to head [(`620a42f`)](https://app.codecov.io/gh/deislabs/ratify/pull/1402?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs).
> Report is 20 commits behind head on staging.

| [Files](https://app.codecov.io/gh/deislabs/ratify/pull/1402?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) | Patch % | Lines |
|---|---|---|
| [pkg/utils/test\_utils.go](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&filepath=pkg%2Futils%2Ftest_utils.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL3V0aWxzL3Rlc3RfdXRpbHMuZ28=) | 52.94% | [17 Missing and 7 partials :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [pkg/manager/manager.go](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&filepath=pkg%2Fmanager%2Fmanager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL21hbmFnZXIvbWFuYWdlci5nbw==) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [pkg/controllers/clustered/policy\_controller.go](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&filepath=pkg%2Fcontrollers%2Fclustered%2Fpolicy_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL2NvbnRyb2xsZXJzL2NsdXN0ZXJlZC9wb2xpY3lfY29udHJvbGxlci5nbw==) | 92.15% | [4 Missing :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [...kg/controllers/keymanagementprovider\_controller.go](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&filepath=pkg%2Fcontrollers%2Fkeymanagementprovider_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL2NvbnRyb2xsZXJzL2tleW1hbmFnZW1lbnRwcm92aWRlcl9jb250cm9sbGVyLmdv) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [pkg/verifier/notation/truststore.go](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&filepath=pkg%2Fverifier%2Fnotation%2Ftruststore.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL3ZlcmlmaWVyL25vdGF0aW9uL3RydXN0c3RvcmUuZ28=) | 50.00% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1402?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           staging    #1402      +/-   ##
===========================================
+ Coverage    65.31%   66.14%   +0.83%     
===========================================
  Files          105      114       +9     
  Lines         5458     5716     +258     
===========================================
+ Hits          3565     3781     +216     
- Misses        1535     1572      +37     
- Partials       358      363       +5     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/deislabs/ratify/pull/1402?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs).

susanshi: Hi Binbin, does this PR enable customer use Cluster store/verifier , with a name spaced policy? just wondering if we should highlight this scenario in a release note for v1.2
binbin-li: > Hi Binbin, does this PR enable customer use Cluster store/verifier , with a name spaced policy? just wondering if we should highlight this scenario in a release note for v1.2

synced offline, will need to merge all following CRD PRs to enable multi-tenancy.

Patch:
diff --git a/Makefile b/Makefile
index b1a3ac0..08e4709 100644
--- a/Makefile
+++ b/Makefile
@@ -135,7 +135,7 @@ delete-demo-constraints:
 
 .PHONY: deploy-rego-policy
 deploy-rego-policy:
-	kubectl apply -f ./config/samples/policy/config_v1beta1_policy_rego.yaml
+	kubectl apply -f ./config/samples/clustered/policy/config_v1beta1_policy_rego.yaml
 
 .PHONY: deploy-gatekeeper
 deploy-gatekeeper:
diff --git a/PROJECT b/PROJECT
index 75078fb..b91527b 100644
--- a/PROJECT
+++ b/PROJECT
@@ -80,4 +80,12 @@ resources:
   kind: KeyManagementProvider
   path: github.com/deislabs/ratify/api/v1beta1
   version: v1beta1
+- api:
+    crdVersion: v1
+    namespaced: true
+  domain: ratify.deislabs.io
+  group: config
+  kind: NamespacedPolicy
+  path: github.com/deislabs/ratify/api/v1beta1
+  version: v1beta1
 version: "3"
diff --git a/api/unversioned/namespacedpolicy_types.go b/api/unversioned/namespacedpolicy_types.go
new file mode 100644
index 0000000..606fc6b
--- /dev/null
+++ b/api/unversioned/namespacedpolicy_types.go
@@ -0,0 +1,63 @@
+/*
+Copyright The Ratify Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package unversioned
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// NamespacedPolicySpec defines the desired state of Policy
+type NamespacedPolicySpec struct {
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// Type of the policy
+	Type string `json:"type,omitempty"`
+	// Parameters for this policy
+	Parameters runtime.RawExtension `json:"parameters,omitempty"`
+}
+
+// NamespacedPolicyStatus defines the observed state of Policy
+type NamespacedPolicyStatus struct {
+	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// Is successful while applying the policy.
+	IsSuccess bool `json:"issuccess"`
+	// Error message if NamespacedPolicy is not successfully applied.
+	// +optional
+	Error string `json:"error,omitempty"`
+	// Truncated error message if the message is too long
+	// +optional
+	BriefError string `json:"brieferror,omitempty"`
+}
+
+// NamespacedPolicy is the Schema for the policies API
+type NamespacedPolicy struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   NamespacedPolicySpec   `json:"spec,omitempty"`
+	Status NamespacedPolicyStatus `json:"status,omitempty"`
+}
+
+// NamespacedPolicyList contains a list of Policy
+type NamespacedPolicyList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []NamespacedPolicy `json:"items"`
+}
diff --git a/api/unversioned/zz_generated.deepcopy.go b/api/unversioned/zz_generated.deepcopy.go
index 433353d..5e89a6d 100644
--- a/api/unversioned/zz_generated.deepcopy.go
+++ b/api/unversioned/zz_generated.deepcopy.go
@@ -181,6 +181,80 @@ func (in *KeyManagementProviderStatus) DeepCopy() *KeyManagementProviderStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicy) DeepCopyInto(out *NamespacedPolicy) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicy.
+func (in *NamespacedPolicy) DeepCopy() *NamespacedPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicyList) DeepCopyInto(out *NamespacedPolicyList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]NamespacedPolicy, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicyList.
+func (in *NamespacedPolicyList) DeepCopy() *NamespacedPolicyList {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicyList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicySpec) DeepCopyInto(out *NamespacedPolicySpec) {
+	*out = *in
+	in.Parameters.DeepCopyInto(&out.Parameters)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicySpec.
+func (in *NamespacedPolicySpec) DeepCopy() *NamespacedPolicySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicyStatus) DeepCopyInto(out *NamespacedPolicyStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicyStatus.
+func (in *NamespacedPolicyStatus) DeepCopy() *NamespacedPolicyStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicyStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PluginSource) DeepCopyInto(out *PluginSource) {
 	*out = *in
diff --git a/api/v1beta1/namespacedpolicy_types.go b/api/v1beta1/namespacedpolicy_types.go
new file mode 100644
index 0000000..26747d5
--- /dev/null
+++ b/api/v1beta1/namespacedpolicy_types.go
@@ -0,0 +1,80 @@
+/*
+Copyright The Ratify Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
+// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.
+
+// NamespacedPolicySpec defines the desired state of NamespacedPolicy
+type NamespacedPolicySpec struct {
+	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// Type of the policy
+	Type string `json:"type,omitempty"`
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// Parameters for this policy
+	Parameters runtime.RawExtension `json:"parameters,omitempty"`
+}
+
+// NamespacedPolicyStatus defines the observed state of NamespacedPolicy
+type NamespacedPolicyStatus struct {
+	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
+	// Important: Run "make" to regenerate code after modifying this file
+
+	// Is successful while applying the policy.
+	IsSuccess bool `json:"issuccess"`
+	// Error message if policy is not successfully applied.
+	// +optional
+	Error string `json:"error,omitempty"`
+	// Truncated error message if the message is too long
+	// +optional
+	BriefError string `json:"brieferror,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:resource:scope="Namespaced"
+// +kubebuilder:subresource:status
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="IsSuccess",type=boolean,JSONPath=`.status.issuccess`
+// +kubebuilder:printcolumn:name="Error",type=string,JSONPath=`.status.brieferror`
+// NamespacedPolicy is the Schema for the namespacedpolicies API
+type NamespacedPolicy struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   NamespacedPolicySpec   `json:"spec,omitempty"`
+	Status NamespacedPolicyStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:storageversion
+// NamespacedPolicyList contains a list of NamespacedPolicy
+type NamespacedPolicyList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []NamespacedPolicy `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&NamespacedPolicy{}, &NamespacedPolicyList{})
+}
diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go
index 2e097dc..2a98f8a 100644
--- a/api/v1beta1/zz_generated.conversion.go
+++ b/api/v1beta1/zz_generated.conversion.go
@@ -76,6 +76,86 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*KeyManagementProvider)(nil), (*unversioned.KeyManagementProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_KeyManagementProvider_To_unversioned_KeyManagementProvider(a.(*KeyManagementProvider), b.(*unversioned.KeyManagementProvider), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.KeyManagementProvider)(nil), (*KeyManagementProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_KeyManagementProvider_To_v1beta1_KeyManagementProvider(a.(*unversioned.KeyManagementProvider), b.(*KeyManagementProvider), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*KeyManagementProviderList)(nil), (*unversioned.KeyManagementProviderList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_KeyManagementProviderList_To_unversioned_KeyManagementProviderList(a.(*KeyManagementProviderList), b.(*unversioned.KeyManagementProviderList), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.KeyManagementProviderList)(nil), (*KeyManagementProviderList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_KeyManagementProviderList_To_v1beta1_KeyManagementProviderList(a.(*unversioned.KeyManagementProviderList), b.(*KeyManagementProviderList), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*KeyManagementProviderSpec)(nil), (*unversioned.KeyManagementProviderSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec(a.(*KeyManagementProviderSpec), b.(*unversioned.KeyManagementProviderSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.KeyManagementProviderSpec)(nil), (*KeyManagementProviderSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec(a.(*unversioned.KeyManagementProviderSpec), b.(*KeyManagementProviderSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*KeyManagementProviderStatus)(nil), (*unversioned.KeyManagementProviderStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus(a.(*KeyManagementProviderStatus), b.(*unversioned.KeyManagementProviderStatus), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.KeyManagementProviderStatus)(nil), (*KeyManagementProviderStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus(a.(*unversioned.KeyManagementProviderStatus), b.(*KeyManagementProviderStatus), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*NamespacedPolicy)(nil), (*unversioned.NamespacedPolicy)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_NamespacedPolicy_To_unversioned_NamespacedPolicy(a.(*NamespacedPolicy), b.(*unversioned.NamespacedPolicy), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedPolicy)(nil), (*NamespacedPolicy)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_NamespacedPolicy_To_v1beta1_NamespacedPolicy(a.(*unversioned.NamespacedPolicy), b.(*NamespacedPolicy), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*NamespacedPolicyList)(nil), (*unversioned.NamespacedPolicyList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_NamespacedPolicyList_To_unversioned_NamespacedPolicyList(a.(*NamespacedPolicyList), b.(*unversioned.NamespacedPolicyList), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedPolicyList)(nil), (*NamespacedPolicyList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_NamespacedPolicyList_To_v1beta1_NamespacedPolicyList(a.(*unversioned.NamespacedPolicyList), b.(*NamespacedPolicyList), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*NamespacedPolicySpec)(nil), (*unversioned.NamespacedPolicySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec(a.(*NamespacedPolicySpec), b.(*unversioned.NamespacedPolicySpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedPolicySpec)(nil), (*NamespacedPolicySpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec(a.(*unversioned.NamespacedPolicySpec), b.(*NamespacedPolicySpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*NamespacedPolicyStatus)(nil), (*unversioned.NamespacedPolicyStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus(a.(*NamespacedPolicyStatus), b.(*unversioned.NamespacedPolicyStatus), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*unversioned.NamespacedPolicyStatus)(nil), (*NamespacedPolicyStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(a.(*unversioned.NamespacedPolicyStatus), b.(*NamespacedPolicyStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*PluginSource)(nil), (*unversioned.PluginSource)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1beta1_PluginSource_To_unversioned_PluginSource(a.(*PluginSource), b.(*unversioned.PluginSource), scope)
 	}); err != nil {
@@ -313,6 +393,210 @@ func Convert_unversioned_CertificateStoreStatus_To_v1beta1_CertificateStoreStatu
 	return autoConvert_unversioned_CertificateStoreStatus_To_v1beta1_CertificateStoreStatus(in, out, s)
 }
 
+func autoConvert_v1beta1_KeyManagementProvider_To_unversioned_KeyManagementProvider(in *KeyManagementProvider, out *unversioned.KeyManagementProvider, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_v1beta1_KeyManagementProvider_To_unversioned_KeyManagementProvider is an autogenerated conversion function.
+func Convert_v1beta1_KeyManagementProvider_To_unversioned_KeyManagementProvider(in *KeyManagementProvider, out *unversioned.KeyManagementProvider, s conversion.Scope) error {
+	return autoConvert_v1beta1_KeyManagementProvider_To_unversioned_KeyManagementProvider(in, out, s)
+}
+
+func autoConvert_unversioned_KeyManagementProvider_To_v1beta1_KeyManagementProvider(in *unversioned.KeyManagementProvider, out *KeyManagementProvider, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_unversioned_KeyManagementProvider_To_v1beta1_KeyManagementProvider is an autogenerated conversion function.
+func Convert_unversioned_KeyManagementProvider_To_v1beta1_KeyManagementProvider(in *unversioned.KeyManagementProvider, out *KeyManagementProvider, s conversion.Scope) error {
+	return autoConvert_unversioned_KeyManagementProvider_To_v1beta1_KeyManagementProvider(in, out, s)
+}
+
+func autoConvert_v1beta1_KeyManagementProviderList_To_unversioned_KeyManagementProviderList(in *KeyManagementProviderList, out *unversioned.KeyManagementProviderList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]unversioned.KeyManagementProvider)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_v1beta1_KeyManagementProviderList_To_unversioned_KeyManagementProviderList is an autogenerated conversion function.
+func Convert_v1beta1_KeyManagementProviderList_To_unversioned_KeyManagementProviderList(in *KeyManagementProviderList, out *unversioned.KeyManagementProviderList, s conversion.Scope) error {
+	return autoConvert_v1beta1_KeyManagementProviderList_To_unversioned_KeyManagementProviderList(in, out, s)
+}
+
+func autoConvert_unversioned_KeyManagementProviderList_To_v1beta1_KeyManagementProviderList(in *unversioned.KeyManagementProviderList, out *KeyManagementProviderList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]KeyManagementProvider)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_unversioned_KeyManagementProviderList_To_v1beta1_KeyManagementProviderList is an autogenerated conversion function.
+func Convert_unversioned_KeyManagementProviderList_To_v1beta1_KeyManagementProviderList(in *unversioned.KeyManagementProviderList, out *KeyManagementProviderList, s conversion.Scope) error {
+	return autoConvert_unversioned_KeyManagementProviderList_To_v1beta1_KeyManagementProviderList(in, out, s)
+}
+
+func autoConvert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec(in *KeyManagementProviderSpec, out *unversioned.KeyManagementProviderSpec, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Parameters = in.Parameters
+	return nil
+}
+
+// Convert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec is an autogenerated conversion function.
+func Convert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec(in *KeyManagementProviderSpec, out *unversioned.KeyManagementProviderSpec, s conversion.Scope) error {
+	return autoConvert_v1beta1_KeyManagementProviderSpec_To_unversioned_KeyManagementProviderSpec(in, out, s)
+}
+
+func autoConvert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec(in *unversioned.KeyManagementProviderSpec, out *KeyManagementProviderSpec, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Parameters = in.Parameters
+	return nil
+}
+
+// Convert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec is an autogenerated conversion function.
+func Convert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec(in *unversioned.KeyManagementProviderSpec, out *KeyManagementProviderSpec, s conversion.Scope) error {
+	return autoConvert_unversioned_KeyManagementProviderSpec_To_v1beta1_KeyManagementProviderSpec(in, out, s)
+}
+
+func autoConvert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus(in *KeyManagementProviderStatus, out *unversioned.KeyManagementProviderStatus, s conversion.Scope) error {
+	out.IsSuccess = in.IsSuccess
+	out.Error = in.Error
+	out.BriefError = in.BriefError
+	out.LastFetchedTime = (*v1.Time)(unsafe.Pointer(in.LastFetchedTime))
+	out.Properties = in.Properties
+	return nil
+}
+
+// Convert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus is an autogenerated conversion function.
+func Convert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus(in *KeyManagementProviderStatus, out *unversioned.KeyManagementProviderStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_KeyManagementProviderStatus_To_unversioned_KeyManagementProviderStatus(in, out, s)
+}
+
+func autoConvert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus(in *unversioned.KeyManagementProviderStatus, out *KeyManagementProviderStatus, s conversion.Scope) error {
+	out.IsSuccess = in.IsSuccess
+	out.Error = in.Error
+	out.BriefError = in.BriefError
+	out.LastFetchedTime = (*v1.Time)(unsafe.Pointer(in.LastFetchedTime))
+	out.Properties = in.Properties
+	return nil
+}
+
+// Convert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus is an autogenerated conversion function.
+func Convert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus(in *unversioned.KeyManagementProviderStatus, out *KeyManagementProviderStatus, s conversion.Scope) error {
+	return autoConvert_unversioned_KeyManagementProviderStatus_To_v1beta1_KeyManagementProviderStatus(in, out, s)
+}
+
+func autoConvert_v1beta1_NamespacedPolicy_To_unversioned_NamespacedPolicy(in *NamespacedPolicy, out *unversioned.NamespacedPolicy, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_v1beta1_NamespacedPolicy_To_unversioned_NamespacedPolicy is an autogenerated conversion function.
+func Convert_v1beta1_NamespacedPolicy_To_unversioned_NamespacedPolicy(in *NamespacedPolicy, out *unversioned.NamespacedPolicy, s conversion.Scope) error {
+	return autoConvert_v1beta1_NamespacedPolicy_To_unversioned_NamespacedPolicy(in, out, s)
+}
+
+func autoConvert_unversioned_NamespacedPolicy_To_v1beta1_NamespacedPolicy(in *unversioned.NamespacedPolicy, out *NamespacedPolicy, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_unversioned_NamespacedPolicy_To_v1beta1_NamespacedPolicy is an autogenerated conversion function.
+func Convert_unversioned_NamespacedPolicy_To_v1beta1_NamespacedPolicy(in *unversioned.NamespacedPolicy, out *NamespacedPolicy, s conversion.Scope) error {
+	return autoConvert_unversioned_NamespacedPolicy_To_v1beta1_NamespacedPolicy(in, out, s)
+}
+
+func autoConvert_v1beta1_NamespacedPolicyList_To_unversioned_NamespacedPolicyList(in *NamespacedPolicyList, out *unversioned.NamespacedPolicyList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]unversioned.NamespacedPolicy)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_v1beta1_NamespacedPolicyList_To_unversioned_NamespacedPolicyList is an autogenerated conversion function.
+func Convert_v1beta1_NamespacedPolicyList_To_unversioned_NamespacedPolicyList(in *NamespacedPolicyList, out *unversioned.NamespacedPolicyList, s conversion.Scope) error {
+	return autoConvert_v1beta1_NamespacedPolicyList_To_unversioned_NamespacedPolicyList(in, out, s)
+}
+
+func autoConvert_unversioned_NamespacedPolicyList_To_v1beta1_NamespacedPolicyList(in *unversioned.NamespacedPolicyList, out *NamespacedPolicyList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]NamespacedPolicy)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_unversioned_NamespacedPolicyList_To_v1beta1_NamespacedPolicyList is an autogenerated conversion function.
+func Convert_unversioned_NamespacedPolicyList_To_v1beta1_NamespacedPolicyList(in *unversioned.NamespacedPolicyList, out *NamespacedPolicyList, s conversion.Scope) error {
+	return autoConvert_unversioned_NamespacedPolicyList_To_v1beta1_NamespacedPolicyList(in, out, s)
+}
+
+func autoConvert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec(in *NamespacedPolicySpec, out *unversioned.NamespacedPolicySpec, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Parameters = in.Parameters
+	return nil
+}
+
+// Convert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec is an autogenerated conversion function.
+func Convert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec(in *NamespacedPolicySpec, out *unversioned.NamespacedPolicySpec, s conversion.Scope) error {
+	return autoConvert_v1beta1_NamespacedPolicySpec_To_unversioned_NamespacedPolicySpec(in, out, s)
+}
+
+func autoConvert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec(in *unversioned.NamespacedPolicySpec, out *NamespacedPolicySpec, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Parameters = in.Parameters
+	return nil
+}
+
+// Convert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec is an autogenerated conversion function.
+func Convert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec(in *unversioned.NamespacedPolicySpec, out *NamespacedPolicySpec, s conversion.Scope) error {
+	return autoConvert_unversioned_NamespacedPolicySpec_To_v1beta1_NamespacedPolicySpec(in, out, s)
+}
+
+func autoConvert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus(in *NamespacedPolicyStatus, out *unversioned.NamespacedPolicyStatus, s conversion.Scope) error {
+	out.IsSuccess = in.IsSuccess
+	out.Error = in.Error
+	out.BriefError = in.BriefError
+	return nil
+}
+
+// Convert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus is an autogenerated conversion function.
+func Convert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus(in *NamespacedPolicyStatus, out *unversioned.NamespacedPolicyStatus, s conversion.Scope) error {
+	return autoConvert_v1beta1_NamespacedPolicyStatus_To_unversioned_NamespacedPolicyStatus(in, out, s)
+}
+
+func autoConvert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(in *unversioned.NamespacedPolicyStatus, out *NamespacedPolicyStatus, s conversion.Scope) error {
+	out.IsSuccess = in.IsSuccess
+	out.Error = in.Error
+	out.BriefError = in.BriefError
+	return nil
+}
+
+// Convert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus is an autogenerated conversion function.
+func Convert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(in *unversioned.NamespacedPolicyStatus, out *NamespacedPolicyStatus, s conversion.Scope) error {
+	return autoConvert_unversioned_NamespacedPolicyStatus_To_v1beta1_NamespacedPolicyStatus(in, out, s)
+}
+
 func autoConvert_v1beta1_PluginSource_To_unversioned_PluginSource(in *PluginSource, out *unversioned.PluginSource, s conversion.Scope) error {
 	out.Artifact = in.Artifact
 	out.AuthProvider = in.AuthProvider
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
index 8e390e3..076806a 100644
--- a/api/v1beta1/zz_generated.deepcopy.go
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -215,6 +215,96 @@ func (in *KeyManagementProviderStatus) DeepCopy() *KeyManagementProviderStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicy) DeepCopyInto(out *NamespacedPolicy) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicy.
+func (in *NamespacedPolicy) DeepCopy() *NamespacedPolicy {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NamespacedPolicy) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicyList) DeepCopyInto(out *NamespacedPolicyList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]NamespacedPolicy, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicyList.
+func (in *NamespacedPolicyList) DeepCopy() *NamespacedPolicyList {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicyList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NamespacedPolicyList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicySpec) DeepCopyInto(out *NamespacedPolicySpec) {
+	*out = *in
+	in.Parameters.DeepCopyInto(&out.Parameters)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicySpec.
+func (in *NamespacedPolicySpec) DeepCopy() *NamespacedPolicySpec {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicySpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NamespacedPolicyStatus) DeepCopyInto(out *NamespacedPolicyStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedPolicyStatus.
+func (in *NamespacedPolicyStatus) DeepCopy() *NamespacedPolicyStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(NamespacedPolicyStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PluginSource) DeepCopyInto(out *PluginSource) {
 	*out = *in
diff --git a/charts/ratify/crds/namespacedpolicy-customresourcedefinition.yaml b/charts/ratify/crds/namespacedpolicy-customresourcedefinition.yaml
new file mode 100644
index 0000000..c18d01a
--- /dev/null
+++ b/charts/ratify/crds/namespacedpolicy-customresourcedefinition.yaml
@@ -0,0 +1,74 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: namespacedpolicies.config.ratify.deislabs.io
+spec:
+  group: config.ratify.deislabs.io
+  names:
+    kind: NamespacedPolicy
+    listKind: NamespacedPolicyList
+    plural: namespacedpolicies
+    singular: namespacedpolicy
+  scope: Namespaced
+  versions:
+    - additionalPrinterColumns:
+        - jsonPath: .status.issuccess
+          name: IsSuccess
+          type: boolean
+        - jsonPath: .status.brieferror
+          name: Error
+          type: string
+      name: v1beta1
+      schema:
+        openAPIV3Schema:
+          description: NamespacedPolicy is the Schema for the namespacedpolicies 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"
+              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"
+              type: string
+            metadata:
+              type: object
+            spec:
+              description: NamespacedPolicySpec defines the desired state of NamespacedPolicy
+              properties:
+                parameters:
+                  description: Parameters for this policy
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type:
+                  description: Type of the policy
+                  type: string
+              type: object
+            status:
+              description: NamespacedPolicyStatus defines the observed state of NamespacedPolicy
+              properties:
+                brieferror:
+                  description: Truncated error message if the message is too long
+                  type: string
+                error:
+                  description: Error message if policy is not successfully applied.
+                  type: string
+                issuccess:
+                  description: Is successful while applying the policy.
+                  type: boolean
+              required:
+                - issuccess
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
diff --git a/charts/ratify/templates/ratify-manager-role-clusterrole.yaml b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml
index 854cf54..a2bd679 100644
--- a/charts/ratify/templates/ratify-manager-role-clusterrole.yaml
+++ b/charts/ratify/templates/ratify-manager-role-clusterrole.yaml
@@ -135,6 +135,32 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - externaldata.gatekeeper.sh
   resources:
diff --git a/config/crd/bases/config.ratify.deislabs.io_namespacedpolicies.yaml b/config/crd/bases/config.ratify.deislabs.io_namespacedpolicies.yaml
new file mode 100644
index 0000000..c18d01a
--- /dev/null
+++ b/config/crd/bases/config.ratify.deislabs.io_namespacedpolicies.yaml
@@ -0,0 +1,74 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.9.2
+  creationTimestamp: null
+  name: namespacedpolicies.config.ratify.deislabs.io
+spec:
+  group: config.ratify.deislabs.io
+  names:
+    kind: NamespacedPolicy
+    listKind: NamespacedPolicyList
+    plural: namespacedpolicies
+    singular: namespacedpolicy
+  scope: Namespaced
+  versions:
+    - additionalPrinterColumns:
+        - jsonPath: .status.issuccess
+          name: IsSuccess
+          type: boolean
+        - jsonPath: .status.brieferror
+          name: Error
+          type: string
+      name: v1beta1
+      schema:
+        openAPIV3Schema:
+          description: NamespacedPolicy is the Schema for the namespacedpolicies 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"
+              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"
+              type: string
+            metadata:
+              type: object
+            spec:
+              description: NamespacedPolicySpec defines the desired state of NamespacedPolicy
+              properties:
+                parameters:
+                  description: Parameters for this policy
+                  type: object
+                  x-kubernetes-preserve-unknown-fields: true
+                type:
+                  description: Type of the policy
+                  type: string
+              type: object
+            status:
+              description: NamespacedPolicyStatus defines the observed state of NamespacedPolicy
+              properties:
+                brieferror:
+                  description: Truncated error message if the message is too long
+                  type: string
+                error:
+                  description: Error message if policy is not successfully applied.
+                  type: string
+                issuccess:
+                  description: Is successful while applying the policy.
+                  type: boolean
+              required:
+                - issuccess
+              type: object
+          type: object
+      served: true
+      storage: true
+      subresources:
+        status: {}
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 26aa14b..5ade6da 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -7,6 +7,7 @@ resources:
   - bases/config.ratify.deislabs.io_certificatestores.yaml
   - bases/config.ratify.deislabs.io_policies.yaml
   - bases/config.ratify.deislabs.io_keymanagementproviders.yaml
+  - bases/config.ratify.deislabs.io_namespacedpolicies.yaml
 #+kubebuilder:scaffold:crdkustomizeresource
 
 patchesStrategicMerge:
@@ -17,6 +18,7 @@ patchesStrategicMerge:
   #- patches/webhook_in_certificatestores.yaml
   #- patches/webhook_in_policies.yaml
   #- patches/webhook_in_keymanagementproviders.yaml
+  #- patches/webhook_in_namespacedpolicies.yaml
   #+kubebuilder:scaffold:crdkustomizewebhookpatch
 
   # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
@@ -26,6 +28,7 @@ patchesStrategicMerge:
   #- patches/cainjection_in_certificatestores.yaml
   #- patches/cainjection_in_policies.yaml
   #- patches/cainjection_in_keymanagementproviders.yaml
+  #- patches/cainjection_in_namespacedpolicies.yaml
 #+kubebuilder:scaffold:crdkustomizecainjectionpatch
 
 # the following config is for teaching kustomize how to do kustomization for CRDs.
diff --git a/config/crd/patches/cainjection_in_clusterpolicies.yaml b/config/crd/patches/cainjection_in_clusterpolicies.yaml
new file mode 100644
index 0000000..8e6a64b
--- /dev/null
+++ b/config/crd/patches/cainjection_in_clusterpolicies.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: clusterpolicies.config.ratify.deislabs.io
diff --git a/config/crd/patches/cainjection_in_namespacedpolicies.yaml b/config/crd/patches/cainjection_in_namespacedpolicies.yaml
new file mode 100644
index 0000000..be47ac5
--- /dev/null
+++ b/config/crd/patches/cainjection_in_namespacedpolicies.yaml
@@ -0,0 +1,7 @@
+# The following patch adds a directive for certmanager to inject CA into the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
+  name: namespacedpolicies.config.ratify.deislabs.io
diff --git a/config/crd/patches/webhook_in_clusterpolicies.yaml b/config/crd/patches/webhook_in_clusterpolicies.yaml
new file mode 100644
index 0000000..de0e79e
--- /dev/null
+++ b/config/crd/patches/webhook_in_clusterpolicies.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: clusterpolicies.config.ratify.deislabs.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/crd/patches/webhook_in_namespacedpolicies.yaml b/config/crd/patches/webhook_in_namespacedpolicies.yaml
new file mode 100644
index 0000000..057d373
--- /dev/null
+++ b/config/crd/patches/webhook_in_namespacedpolicies.yaml
@@ -0,0 +1,16 @@
+# The following patch enables a conversion webhook for the CRD
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  name: namespacedpolicies.config.ratify.deislabs.io
+spec:
+  conversion:
+    strategy: Webhook
+    webhook:
+      clientConfig:
+        service:
+          namespace: system
+          name: webhook-service
+          path: /convert
+      conversionReviewVersions:
+      - v1
diff --git a/config/rbac/namespacedpolicy_editor_role.yaml b/config/rbac/namespacedpolicy_editor_role.yaml
new file mode 100644
index 0000000..fb1e31c
--- /dev/null
+++ b/config/rbac/namespacedpolicy_editor_role.yaml
@@ -0,0 +1,31 @@
+# permissions for end users to edit namespacedpolicies.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: namespacedpolicy-editor-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: ratify
+    app.kubernetes.io/part-of: ratify
+    app.kubernetes.io/managed-by: kustomize
+  name: namespacedpolicy-editor-role
+rules:
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/status
+  verbs:
+  - get
diff --git a/config/rbac/namespacedpolicy_viewer_role.yaml b/config/rbac/namespacedpolicy_viewer_role.yaml
new file mode 100644
index 0000000..4dc2f98
--- /dev/null
+++ b/config/rbac/namespacedpolicy_viewer_role.yaml
@@ -0,0 +1,27 @@
+# permissions for end users to view namespacedpolicies.
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  labels:
+    app.kubernetes.io/name: clusterrole
+    app.kubernetes.io/instance: namespacedpolicy-viewer-role
+    app.kubernetes.io/component: rbac
+    app.kubernetes.io/created-by: ratify
+    app.kubernetes.io/part-of: ratify
+    app.kubernetes.io/managed-by: kustomize
+  name: namespacedpolicy-viewer-role
+rules:
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies
+  verbs:
+  - get
+  - list
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/status
+  verbs:
+  - get
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 36974c9..c787b89 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -57,6 +57,32 @@ rules:
   - get
   - patch
   - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - patch
+  - update
+  - watch
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/finalizers
+  verbs:
+  - update
+- apiGroups:
+  - config.ratify.deislabs.io
+  resources:
+  - namespacedpolicies/status
+  verbs:
+  - get
+  - patch
+  - update
 - apiGroups:
   - config.ratify.deislabs.io
   resources:
diff --git a/config/samples/clustered/policy/config_v1alpha1_policy_json.yaml b/config/samples/clustered/policy/config_v1alpha1_policy_json.yaml
new file mode 100644
index 0000000..a947d59
--- /dev/null
+++ b/config/samples/clustered/policy/config_v1alpha1_policy_json.yaml
@@ -0,0 +1,8 @@
+apiVersion: config.ratify.deislabs.io/v1alpha1
+kind: Policy # Policy applies to the cluster.
+metadata:
+  name: "configpolicy" # Ensure that metadata.name is either 'regopolicy' or 'configpolicy'
+spec:
+  parameters:
+    artifactVerificationPolicies:
+      default: "all"
diff --git a/config/samples/policy/config_v1alpha1_policy_rego.yaml b/config/samples/clustered/policy/config_v1alpha1_policy_rego.yaml
similarity index 83%
rename from config/samples/policy/config_v1alpha1_policy_rego.yaml
rename to config/samples/clustered/policy/config_v1alpha1_policy_rego.yaml
index f809783..45c3695 100644
--- a/config/samples/policy/config_v1alpha1_policy_rego.yaml
+++ b/config/samples/clustered/policy/config_v1alpha1_policy_rego.yaml
@@ -1,7 +1,7 @@
 apiVersion: config.ratify.deislabs.io/v1alpha1
-kind: Policy
+kind: Policy # Policy applies to the cluster.
 metadata:
-  name: "regopolicy"
+  name: "regopolicy" # Ensure that metadata.name is either 'regopolicy' or 'configpolicy'
 spec:
   parameters:
     passthroughEnabled: false
diff --git a/config/samples/clustered/policy/config_v1beta1_policy_json.yaml b/config/samples/clustered/policy/config_v1beta1_policy_json.yaml
new file mode 100644
index 0000000..00c3533
--- /dev/null
+++ b/config/samples/clustered/policy/config_v1beta1_policy_json.yaml
@@ -0,0 +1,9 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: Policy # Policy applies to the cluster.
+metadata:
+  name: "ratify-policy" # metadata.name MUST be set to ratify-policy since v1beta1.
+spec:
+  type: "config-policy" # Ensure that spec.type is either 'rego-policy' or 'config-policy' in v1beta1.
+  parameters:
+    artifactVerificationPolicies:
+      default: "all"
diff --git a/config/samples/policy/config_v1beta1_policy_rego.yaml b/config/samples/clustered/policy/config_v1beta1_policy_rego.yaml
similarity index 74%
rename from config/samples/policy/config_v1beta1_policy_rego.yaml
rename to config/samples/clustered/policy/config_v1beta1_policy_rego.yaml
index 89ac6a0..6bc1451 100644
--- a/config/samples/policy/config_v1beta1_policy_rego.yaml
+++ b/config/samples/clustered/policy/config_v1beta1_policy_rego.yaml
@@ -1,9 +1,9 @@
 apiVersion: config.ratify.deislabs.io/v1beta1
-kind: Policy
+kind: Policy # Policy applies to the cluster.
 metadata:
-  name: "ratify-policy"
+  name: "ratify-policy" # metadata.name MUST be set to ratify-policy since v1beta1.
 spec:
-  type: "rego-policy"
+  type: "rego-policy" # Ensure that spec.type is either 'rego-policy' or 'config-policy' in v1beta1.
   parameters:
     passthroughEnabled: false
     policy: |
diff --git a/config/samples/namespaced/policy/config_v1beta1_policy_json.yaml b/config/samples/namespaced/policy/config_v1beta1_policy_json.yaml
new file mode 100644
index 0000000..bc4b329
--- /dev/null
+++ b/config/samples/namespaced/policy/config_v1beta1_policy_json.yaml
@@ -0,0 +1,9 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: NamespacedPolicy # NamespacedPolicy only applies to specified namespace.
+metadata:
+  name: "ratify-policy" # metadata.name MUST be set to ratify-policy since v1beta1.
+spec:
+  type: "config-policy" # Ensure that spec.type is either 'rego-policy' or 'config-policy' in v1beta1.
+  parameters:
+    artifactVerificationPolicies:
+      default: "all"
diff --git a/config/samples/namespaced/policy/config_v1beta1_policy_rego.yaml b/config/samples/namespaced/policy/config_v1beta1_policy_rego.yaml
new file mode 100644
index 0000000..9aaec13
--- /dev/null
+++ b/config/samples/namespaced/policy/config_v1beta1_policy_rego.yaml
@@ -0,0 +1,31 @@
+apiVersion: config.ratify.deislabs.io/v1beta1
+kind: NamespacedPolicy # NamespacedPolicy only applies to specified namespace.
+metadata:
+  name: "ratify-policy" # metadata.name MUST be set to ratify-policy since v1beta1.
+spec:
+  type: "rego-policy" # Ensure that spec.type is either 'rego-policy' or 'config-policy' in v1beta1.
+  parameters:
+    passthroughEnabled: false
+    policy: |
+      package ratify.policy
+
+      default valid := false
+
+      # all artifacts MUST be valid
+      valid {
+        not failed_verify(input)
+      }
+
+      # all reports MUST pass the verification
+      failed_verify(reports) {
+        [path, value] := walk(reports)
+        value == false
+        path[count(path) - 1] == "isSuccess"
+      }
+
+      # each artifact MUST have at least one report
+      failed_verify(reports) {
+        [path, value] := walk(reports)
+        path[count(path) - 1] == "verifierReports"
+        count(value) == 0
+      }
diff --git a/config/samples/policy/config_v1alpha1_policy_json.yaml b/config/samples/policy/config_v1alpha1_policy_json.yaml
deleted file mode 100644
index 127bab7..0000000
--- a/config/samples/policy/config_v1alpha1_policy_json.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-apiVersion: config.ratify.deislabs.io/v1alpha1
-kind: Policy
-metadata:
-  name: "configpolicy"
-spec:
-  parameters:
-    artifactVerificationPolicies:
-      default: "all"
diff --git a/config/samples/policy/config_v1beta1_policy_json.yaml b/config/samples/policy/config_v1beta1_policy_json.yaml
deleted file mode 100644
index 1fbf1ad..0000000
--- a/config/samples/policy/config_v1beta1_policy_json.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-apiVersion: config.ratify.deislabs.io/v1beta1
-kind: Policy
-metadata:
-  name: "ratify-policy"
-spec:
-  type: "config-policy"
-  parameters:
-    artifactVerificationPolicies:
-      default: "all"
diff --git a/httpserver/handlers.go b/httpserver/handlers.go
index 46267bf..4f4d02a 100644
--- a/httpserver/handlers.go
+++ b/httpserver/handlers.go
@@ -257,7 +257,7 @@ func (server *Server) validateComponents(ctx context.Context, handlerComponents
 			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.ReferrerStore).WithDetail("referrer store config should have at least one store")
 		}
 		if server.GetExecutor(ctx).PolicyEnforcer == nil {
-			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.PolicyProvider).WithDetail("policy provider config must be specified")
+			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.PolicyProvider).WithDetail("policy provider config is not provided")
 		}
 		if len(server.GetExecutor(ctx).Verifiers) == 0 {
 			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.Verifier).WithDetail("verifiers config should have at least one verifier")
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
index cc6866e..adda96a 100644
--- a/internal/constants/constants.go
+++ b/internal/constants/constants.go
@@ -19,3 +19,4 @@ package constants
 const RatifyPolicy = "ratify-policy"
 const EmptyNamespace = ""
 const NamespaceSeperator = "/"
+const MaxBriefErrLength = 30
diff --git a/pkg/controllers/policy_controller.go b/pkg/controllers/clusterresource/policy_controller.go
similarity index 69%
rename from pkg/controllers/policy_controller.go
rename to pkg/controllers/clusterresource/policy_controller.go
index babec48..56283e5 100644
--- a/pkg/controllers/policy_controller.go
+++ b/pkg/controllers/clusterresource/policy_controller.go
@@ -13,18 +13,16 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package controllers
+package clusterresource
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
 	"github.com/deislabs/ratify/internal/constants"
-	"github.com/deislabs/ratify/pkg/policyprovider"
-	"github.com/deislabs/ratify/pkg/policyprovider/config"
-	pf "github.com/deislabs/ratify/pkg/policyprovider/factory"
+	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/controllers/utils"
 	"github.com/sirupsen/logrus"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
@@ -52,13 +50,12 @@ func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 
 	var policy configv1beta1.Policy
 	var resource = req.Name
-	policyLogger.Infof("Reconciling Policy %s", resource)
+	policyLogger.Infof("Reconciling Cluster Policy %s", resource)
 
 	if err := r.Get(ctx, req.NamespacedName, &policy); err != nil {
 		if apierrors.IsNotFound(err) {
 			policyLogger.Infof("delete event detected, removing policy %s", resource)
-			// TODO: pass the actual namespace once multi-tenancy is supported.
-			NamespacedPolicies.DeletePolicy(constants.EmptyNamespace, resource)
+			controllers.NamespacedPolicies.DeletePolicy(constants.EmptyNamespace, resource)
 		} else {
 			policyLogger.Error("failed to get Policy: ", err)
 		}
@@ -90,47 +87,15 @@ func (r *PolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
 }
 
 func policyAddOrReplace(spec configv1beta1.PolicySpec) error {
-	policyEnforcer, err := specToPolicyEnforcer(spec)
+	policyEnforcer, err := utils.SpecToPolicyEnforcer(spec.Parameters.Raw, spec.Type)
 	if err != nil {
 		return fmt.Errorf("failed to create policy enforcer: %w", err)
 	}
 
-	// TODO: pass the actual namespace once multi-tenancy is supported.
-	NamespacedPolicies.AddPolicy(constants.EmptyNamespace, constants.RatifyPolicy, policyEnforcer)
+	controllers.NamespacedPolicies.AddPolicy(constants.EmptyNamespace, constants.RatifyPolicy, policyEnforcer)
 	return nil
 }
 
-func specToPolicyEnforcer(spec configv1beta1.PolicySpec) (policyprovider.PolicyProvider, error) {
-	policyConfig, err := rawToPolicyConfig(spec.Parameters.Raw, spec.Type)
-	if err != nil {
-		return nil, fmt.Errorf("failed to parse policy config: %w", err)
-	}
-
-	policyEnforcer, err := pf.CreatePolicyProviderFromConfig(policyConfig)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create policy provider: %w", err)
-	}
-
-	return policyEnforcer, nil
-}
-
-func rawToPolicyConfig(raw []byte, policyName string) (config.PoliciesConfig, error) {
-	pluginConfig := config.PolicyPluginConfig{}
-
-	if string(raw) == "" {
-		return config.PoliciesConfig{}, fmt.Errorf("no policy parameters provided")
-	}
-	if err := json.Unmarshal(raw, &pluginConfig); err != nil {
-		return config.PoliciesConfig{}, fmt.Errorf("unable to decode policy parameters.Raw: %s, err: %w", raw, err)
-	}
-
-	pluginConfig["name"] = policyName
-
-	return config.PoliciesConfig{
-		PolicyPlugin: pluginConfig,
-	}, nil
-}
-
 func writePolicyStatus(ctx context.Context, r client.StatusClient, policy *configv1beta1.Policy, logger *logrus.Entry, isSuccess bool, errString string) {
 	if isSuccess {
 		updatePolicySuccessStatus(policy)
@@ -150,8 +115,8 @@ func updatePolicySuccessStatus(policy *configv1beta1.Policy) {
 
 func updatePolicyErrorStatus(policy *configv1beta1.Policy, errString string) {
 	briefErr := errString
-	if len(errString) > maxBriefErrLength {
-		briefErr = fmt.Sprintf("%s...", errString[:maxBriefErrLength])
+	if len(errString) > constants.MaxBriefErrLength {
+		briefErr = fmt.Sprintf("%s...", errString[:constants.MaxBriefErrLength])
 	}
 	policy.Status.IsSuccess = false
 	policy.Status.Error = errString
diff --git a/pkg/controllers/namespaceresource/policy_controller.go b/pkg/controllers/namespaceresource/policy_controller.go
new file mode 100644
index 0000000..988bdd3
--- /dev/null
+++ b/pkg/controllers/namespaceresource/policy_controller.go
@@ -0,0 +1,124 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package namespaceresource
+
+import (
+	"context"
+	"fmt"
+
+	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
+	"github.com/deislabs/ratify/internal/constants"
+	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/controllers/utils"
+	"github.com/sirupsen/logrus"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+// PolicyReconciler reconciles a Policy object
+type PolicyReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedpolicies,verbs=get;list;watch;create;update;patch;delete
+//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedpolicies/status,verbs=get;update;patch
+//+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=namespacedpolicies/finalizers,verbs=update
+
+// Reconcile is part of the main kubernetes reconciliation loop which aims to
+// move the current state of the cluster closer to the desired state.
+//
+// For more details, check Reconcile and its Result here:
+// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile
+func (r *PolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	policyLogger := logrus.WithContext(ctx)
+
+	var policy configv1beta1.NamespacedPolicy
+	var resource = req.Name
+	policyLogger.Infof("Reconciling Namespaced Policy %s", resource)
+
+	if err := r.Get(ctx, req.NamespacedName, &policy); err != nil {
+		if apierrors.IsNotFound(err) {
+			policyLogger.Infof("delete event detected, removing policy %s", resource)
+			controllers.NamespacedPolicies.DeletePolicy(req.Namespace, resource)
+		} else {
+			policyLogger.Error("failed to get Policy: ", err)
+		}
+		return ctrl.Result{}, client.IgnoreNotFound(err)
+	}
+
+	if resource != constants.RatifyPolicy {
+		errStr := fmt.Sprintf("metadata.name must be ratify-policy, got %s", resource)
+		policyLogger.Error(errStr)
+		writePolicyStatus(ctx, r, &policy, policyLogger, false, errStr)
+		return ctrl.Result{}, nil
+	}
+
+	if err := policyAddOrReplace(policy.Spec, req.Namespace); err != nil {
+		policyLogger.Error("unable to create policy from policy crd: ", err)
+		writePolicyStatus(ctx, r, &policy, policyLogger, false, err.Error())
+		return ctrl.Result{}, err
+	}
+
+	writePolicyStatus(ctx, r, &policy, policyLogger, true, "")
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *PolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&configv1beta1.NamespacedPolicy{}).
+		Complete(r)
+}
+
+func policyAddOrReplace(spec configv1beta1.NamespacedPolicySpec, namespace string) error {
+	policyEnforcer, err := utils.SpecToPolicyEnforcer(spec.Parameters.Raw, spec.Type)
+	if err != nil {
+		return fmt.Errorf("failed to create policy enforcer: %w", err)
+	}
+
+	controllers.NamespacedPolicies.AddPolicy(namespace, constants.RatifyPolicy, policyEnforcer)
+	return nil
+}
+
+func writePolicyStatus(ctx context.Context, r client.StatusClient, policy *configv1beta1.NamespacedPolicy, logger *logrus.Entry, isSuccess bool, errString string) {
+	if isSuccess {
+		updatePolicySuccessStatus(policy)
+	} else {
+		updatePolicyErrorStatus(policy, errString)
+	}
+	if statusErr := r.Status().Update(ctx, policy); statusErr != nil {
+		logger.Error(statusErr, ", unable to update policy error status")
+	}
+}
+
+func updatePolicySuccessStatus(policy *configv1beta1.NamespacedPolicy) {
+	policy.Status.IsSuccess = true
+	policy.Status.Error = ""
+	policy.Status.BriefError = ""
+}
+
+func updatePolicyErrorStatus(policy *configv1beta1.NamespacedPolicy, errString string) {
+	briefErr := errString
+	if len(errString) > constants.MaxBriefErrLength {
+		briefErr = fmt.Sprintf("%s...", errString[:constants.MaxBriefErrLength])
+	}
+	policy.Status.IsSuccess = false
+	policy.Status.Error = errString
+	policy.Status.BriefError = briefErr
+}
diff --git a/pkg/controllers/utils/policy.go b/pkg/controllers/utils/policy.go
new file mode 100644
index 0000000..27014d7
--- /dev/null
+++ b/pkg/controllers/utils/policy.go
@@ -0,0 +1,54 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package utils
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/deislabs/ratify/pkg/policyprovider"
+	"github.com/deislabs/ratify/pkg/policyprovider/config"
+	pf "github.com/deislabs/ratify/pkg/policyprovider/factory"
+)
+
+func SpecToPolicyEnforcer(raw []byte, policyType string) (policyprovider.PolicyProvider, error) {
+	policyConfig, err := rawToPolicyConfig(raw, policyType)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse policy config: %w", err)
+	}
+
+	policyEnforcer, err := pf.CreatePolicyProviderFromConfig(policyConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create policy provider: %w", err)
+	}
+
+	return policyEnforcer, nil
+}
+
+func rawToPolicyConfig(raw []byte, policyType string) (config.PoliciesConfig, error) {
+	pluginConfig := config.PolicyPluginConfig{}
+
+	if string(raw) == "" {
+		return config.PoliciesConfig{}, fmt.Errorf("no policy parameters provided")
+	}
+	if err := json.Unmarshal(raw, &pluginConfig); err != nil {
+		return config.PoliciesConfig{}, fmt.Errorf("unable to decode policy parameters.Raw: %s, err: %w", raw, err)
+	}
+
+	pluginConfig["name"] = policyType
+
+	return config.PoliciesConfig{
+		PolicyPlugin: pluginConfig,
+	}, nil
+}
diff --git a/pkg/customresources/policies/api.go b/pkg/customresources/policies/api.go
index 1973c8c..92d87ee 100644
--- a/pkg/customresources/policies/api.go
+++ b/pkg/customresources/policies/api.go
@@ -27,7 +27,4 @@ type PolicyManager interface {
 
 	// DeletePolicy deletes the policy from the given scope.
 	DeletePolicy(scope, policyName string)
-
-	// IsEmpty returns true if there are no policies.
-	IsEmpty() bool
 }
diff --git a/pkg/customresources/policies/policies.go b/pkg/customresources/policies/policies.go
index d562d65..425d3bc 100644
--- a/pkg/customresources/policies/policies.go
+++ b/pkg/customresources/policies/policies.go
@@ -16,6 +16,8 @@ limitations under the License.
 package policies
 
 import (
+	"sync"
+
 	"github.com/deislabs/ratify/internal/constants"
 	"github.com/deislabs/ratify/pkg/policyprovider"
 )
@@ -28,25 +30,26 @@ type PolicyWrapper struct {
 
 // ActivePolicies implements PolicyManager interface.
 type ActivePolicies struct {
-	// TODO: Implement concurrent safety using sync.Map
-	// ScopedPolicies is a mapping from scope to a policy.
-	// Note: Scope is utilized for organizing and isolating verifiers. In a Kubernetes (K8s) environment, the scope can be either a namespace or an empty string ("") for cluster-wide verifiers.
-	ScopedPolicies map[string]PolicyWrapper
+	// scopedPolicies is a mapping from scope to a policy.
+	// Note: Scope is utilized for organizing and isolating policies. In a Kubernetes (K8s) environment, the scope can be either a namespace or an empty string ("") for cluster-wide policy.
+	scopedPolicies sync.Map
 }
 
 func NewActivePolicies() PolicyManager {
-	return &ActivePolicies{
-		ScopedPolicies: make(map[string]PolicyWrapper),
-	}
+	return &ActivePolicies{}
 }
 
 // GetPolicy fulfills the PolicyManager interface.
 // It returns the policy for the given scope. If no policy is found for the given scope, it returns cluster-wide policy.
-// TODO: Current implementation always fetches the cluster-wide policy. Will implement the logic to fetch the policy for the given scope.
-func (p *ActivePolicies) GetPolicy(_ string) policyprovider.PolicyProvider {
-	policy, ok := p.ScopedPolicies[constants.EmptyNamespace]
-	if ok {
-		return policy.Policy
+func (p *ActivePolicies) GetPolicy(scope string) policyprovider.PolicyProvider {
+	if scopedPolicy, ok := p.scopedPolicies.Load(scope); ok {
+		return scopedPolicy.(PolicyWrapper).Policy
+	}
+
+	if scope != constants.EmptyNamespace {
+		if policy, ok := p.scopedPolicies.Load(constants.EmptyNamespace); ok {
+			return policy.(PolicyWrapper).Policy
+		}
 	}
 	return nil
 }
@@ -54,24 +57,18 @@ func (p *ActivePolicies) GetPolicy(_ string) policyprovider.PolicyProvider {
 // AddPolicy fulfills the PolicyManager interface.
 // It adds the given policy under the given scope.
 func (p *ActivePolicies) AddPolicy(scope, policyName string, policy policyprovider.PolicyProvider) {
-	p.ScopedPolicies[scope] = PolicyWrapper{
+	p.scopedPolicies.Store(scope, PolicyWrapper{
 		Name:   policyName,
 		Policy: policy,
-	}
+	})
 }
 
 // DeletePolicy fulfills the PolicyManager interface.
 // It deletes the policy from the given scope.
 func (p *ActivePolicies) DeletePolicy(scope, policyName string) {
-	if policy, ok := p.ScopedPolicies[scope]; ok {
-		if policy.Name == policyName {
-			delete(p.ScopedPolicies, scope)
+	if policy, ok := p.scopedPolicies.Load(scope); ok {
+		if policy.(PolicyWrapper).Name == policyName {
+			p.scopedPolicies.Delete(scope)
 		}
 	}
 }
-
-// IsEmpty fulfills the PolicyManager interface.
-// IsEmpty returns true if there are no policies.
-func (p *ActivePolicies) IsEmpty() bool {
-	return len(p.ScopedPolicies) == 0
-}
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
index 2f1874f..6cafbac 100644
--- a/pkg/manager/manager.go
+++ b/pkg/manager/manager.go
@@ -50,6 +50,8 @@ import (
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
 	ctxUtils "github.com/deislabs/ratify/internal/context"
 	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/controllers/clusterresource"
+	"github.com/deislabs/ratify/pkg/controllers/namespaceresource"
 	ef "github.com/deislabs/ratify/pkg/executor/core"
 	//+kubebuilder:scaffold:imports
 )
@@ -212,7 +214,14 @@ func StartManager(certRotatorReady chan struct{}, probeAddr string) {
 		setupLog.Error(err, "unable to create controller", "controller", "Certificate Store")
 		os.Exit(1)
 	}
-	if err = (&controllers.PolicyReconciler{
+	if err = (&namespaceresource.PolicyReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "Namespaced Policy")
+		os.Exit(1)
+	}
+	if err = (&clusterresource.PolicyReconciler{
 		Client: mgr.GetClient(),
 		Scheme: mgr.GetScheme(),
 	}).SetupWithManager(mgr); err != nil {


Test patch:
diff --git a/httpserver/server_test.go b/httpserver/server_test.go
index cb04b73..1aaf98e 100644
--- a/httpserver/server_test.go
+++ b/httpserver/server_test.go
@@ -592,7 +592,7 @@ func TestServer_Verify_PolicyEnforcerConfigInvalid_Failure(t *testing.T) {
 			t.Fatalf("failed to decode response body: %v", err)
 		}
 		retFirstErr := respBody.Response.Items[0].Error
-		expectedErr := ratifyerrors.ErrorCodeConfigInvalid.WithComponentType(ratifyerrors.PolicyProvider).WithDetail("policy provider config must be specified").Error()
+		expectedErr := ratifyerrors.ErrorCodeConfigInvalid.WithComponentType(ratifyerrors.PolicyProvider).WithDetail("policy provider config is not provided").Error()
 		if retFirstErr != expectedErr {
 			t.Fatalf("Expected first subject error to be %s but got %s", expectedErr, retFirstErr)
 		}
diff --git a/pkg/controllers/clusterresource/policy_controller_test.go b/pkg/controllers/clusterresource/policy_controller_test.go
new file mode 100644
index 0000000..eb37135
--- /dev/null
+++ b/pkg/controllers/clusterresource/policy_controller_test.go
@@ -0,0 +1,240 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package clusterresource
+
+import (
+	"context"
+	"testing"
+
+	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
+	"github.com/deislabs/ratify/internal/constants"
+	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/customresources/policies"
+	_ "github.com/deislabs/ratify/pkg/policyprovider/configpolicy"
+	test "github.com/deislabs/ratify/pkg/utils"
+	"github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+const (
+	policyName1 = "policy1"
+	policyName2 = "policy2"
+)
+
+func TestPolicyAddOrReplace(t *testing.T) {
+	testCases := []struct {
+		name       string
+		spec       configv1beta1.PolicySpec
+		policyName string
+		expectErr  bool
+	}{
+		{
+			name: "invalid spec",
+			spec: configv1beta1.PolicySpec{
+				Type: policyName1,
+			},
+			expectErr: true,
+		},
+		{
+			name: "valid spec",
+			spec: configv1beta1.PolicySpec{
+				Parameters: runtime.RawExtension{
+					Raw: []byte("{\"name\": \"configpolicy\"}"),
+				},
+				Type: "configpolicy",
+			},
+			expectErr: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := policyAddOrReplace(tc.spec)
+
+			if tc.expectErr != (err != nil) {
+				t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil)
+			}
+		})
+	}
+}
+
+func TestWritePolicyStatus(t *testing.T) {
+	logger := logrus.WithContext(context.Background())
+	testCases := []struct {
+		name       string
+		isSuccess  bool
+		policy     *configv1beta1.Policy
+		errString  string
+		reconciler client.StatusClient
+	}{
+		{
+			name:       "success status",
+			isSuccess:  true,
+			policy:     &configv1beta1.Policy{},
+			reconciler: &test.MockStatusClient{},
+		},
+		{
+			name:       "error status",
+			isSuccess:  false,
+			policy:     &configv1beta1.Policy{},
+			errString:  "a long error string that exceeds the max length of 30 characters",
+			reconciler: &test.MockStatusClient{},
+		},
+		{
+			name:      "status update failed",
+			isSuccess: true,
+			policy:    &configv1beta1.Policy{},
+			reconciler: &test.MockStatusClient{
+				UpdateFailed: true,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			writePolicyStatus(context.Background(), tc.reconciler, tc.policy, logger, tc.isSuccess, tc.errString)
+		})
+	}
+}
+
+func TestPolicyReconcile(t *testing.T) {
+	tests := []struct {
+		name           string
+		policy         *configv1beta1.Policy
+		req            *reconcile.Request
+		expectedErr    bool
+		expectedPolicy bool
+	}{
+		{
+			name: "nonexistent policy",
+			req: &reconcile.Request{
+				NamespacedName: types.NamespacedName{Name: "nonexistent"},
+			},
+			policy: &configv1beta1.Policy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "",
+					Name:      policyName1,
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: false,
+		},
+		{
+			name: "no policy parameters provided",
+			policy: &configv1beta1.Policy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "",
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.PolicySpec{
+					Type: "regopolicy",
+				},
+			},
+			expectedErr:    true,
+			expectedPolicy: false,
+		},
+		{
+			name: "wrong policy name",
+			policy: &configv1beta1.Policy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "",
+					Name:      "ratify-policy2",
+				},
+				Spec: configv1beta1.PolicySpec{
+					Type: "regopolicy",
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: false,
+		},
+		{
+			name: "invalid params",
+			policy: &configv1beta1.Policy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "",
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.PolicySpec{
+					Type: "regopolicy",
+					Parameters: runtime.RawExtension{
+						Raw: []byte("test"),
+					},
+				},
+			},
+			expectedErr:    true,
+			expectedPolicy: false,
+		},
+		{
+			name: "valid params",
+			policy: &configv1beta1.Policy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "",
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.PolicySpec{
+					Type: "configpolicy",
+					Parameters: runtime.RawExtension{
+						Raw: []byte("{\"passthroughEnabled:\": false}"),
+					},
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			resetPolicyMap()
+			scheme, err := test.CreateScheme()
+			if err != nil {
+				t.Fatalf("CreateScheme() expected no error, actual %v", err)
+			}
+			client := fake.NewClientBuilder().WithScheme(scheme)
+			client.WithObjects(tt.policy)
+			r := &PolicyReconciler{
+				Scheme: scheme,
+				Client: client.Build(),
+			}
+			var req reconcile.Request
+			if tt.req != nil {
+				req = *tt.req
+			} else {
+				req = reconcile.Request{
+					NamespacedName: test.KeyFor(tt.policy),
+				}
+			}
+			_, err = r.Reconcile(context.Background(), req)
+			if tt.expectedErr != (err != nil) {
+				t.Fatalf("Reconcile() expected error to be %t, actual %t", tt.expectedErr, err != nil)
+			}
+
+			policy := controllers.NamespacedPolicies.GetPolicy(constants.EmptyNamespace)
+			if (policy != nil) != tt.expectedPolicy {
+				t.Fatalf("Expected policy to be %t, got %t", tt.expectedPolicy, policy != nil)
+			}
+		})
+	}
+}
+
+func resetPolicyMap() {
+	controllers.NamespacedPolicies = policies.NewActivePolicies()
+}
diff --git a/pkg/controllers/namespaceresource/policy_controller_test.go b/pkg/controllers/namespaceresource/policy_controller_test.go
new file mode 100644
index 0000000..2764f0f
--- /dev/null
+++ b/pkg/controllers/namespaceresource/policy_controller_test.go
@@ -0,0 +1,269 @@
+/*
+Copyright The Ratify Authors.
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package namespaceresource
+
+import (
+	"context"
+	"testing"
+
+	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
+	"github.com/deislabs/ratify/pkg/controllers"
+	"github.com/deislabs/ratify/pkg/customresources/policies"
+	_ "github.com/deislabs/ratify/pkg/policyprovider/configpolicy"
+	_ "github.com/deislabs/ratify/pkg/policyprovider/regopolicy"
+	test "github.com/deislabs/ratify/pkg/utils"
+	"github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+const (
+	policyName1   = "policy1"
+	policyName2   = "policy2"
+	testNamespace = "testNamespace"
+)
+
+func TestPolicyAddOrReplace(t *testing.T) {
+	testCases := []struct {
+		name       string
+		spec       configv1beta1.NamespacedPolicySpec
+		policyName string
+		expectErr  bool
+	}{
+		{
+			name: "invalid spec",
+			spec: configv1beta1.NamespacedPolicySpec{
+				Type: policyName1,
+			},
+			expectErr: true,
+		},
+		{
+			name: "valid spec",
+			spec: configv1beta1.NamespacedPolicySpec{
+				Parameters: runtime.RawExtension{
+					Raw: []byte("{\"name\": \"configpolicy\"}"),
+				},
+				Type: "configpolicy",
+			},
+			expectErr: false,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := policyAddOrReplace(tc.spec, testNamespace)
+
+			if tc.expectErr != (err != nil) {
+				t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil)
+			}
+		})
+	}
+}
+
+func TestPolicyAddedTwice(t *testing.T) {
+	resetPolicyMap()
+	spec1 := configv1beta1.NamespacedPolicySpec{
+		Parameters: runtime.RawExtension{
+			Raw: []byte("{\"name\": \"configpolicy\"}"),
+		},
+		Type: "configpolicy",
+	}
+	spec2 := configv1beta1.NamespacedPolicySpec{
+		Type: "regopolicy",
+		Parameters: runtime.RawExtension{
+			Raw: []byte("{\"name\": \"regopolicy\", \"policy\": \"package ratify.policy\"}"),
+		},
+	}
+	if err := policyAddOrReplace(spec1, testNamespace); err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+	if err := policyAddOrReplace(spec2, testNamespace); err != nil {
+		t.Fatalf("expected no error, got %v", err)
+	}
+
+	policyType := controllers.NamespacedPolicies.GetPolicy(testNamespace).GetPolicyType(context.Background())
+	if policyType != "regopolicy" {
+		t.Fatalf("expected policy type to be regopolicy, got %s", policyType)
+	}
+}
+
+func TestWritePolicyStatus(t *testing.T) {
+	logger := logrus.WithContext(context.Background())
+	testCases := []struct {
+		name       string
+		isSuccess  bool
+		policy     *configv1beta1.NamespacedPolicy
+		errString  string
+		reconciler client.StatusClient
+	}{
+		{
+			name:       "success status",
+			isSuccess:  true,
+			policy:     &configv1beta1.NamespacedPolicy{},
+			reconciler: &test.MockStatusClient{},
+		},
+		{
+			name:       "error status",
+			isSuccess:  false,
+			policy:     &configv1beta1.NamespacedPolicy{},
+			errString:  "a long error string that exceeds the max length of 30 characters",
+			reconciler: &test.MockStatusClient{},
+		},
+		{
+			name:      "status update failed",
+			isSuccess: true,
+			policy:    &configv1beta1.NamespacedPolicy{},
+			reconciler: &test.MockStatusClient{
+				UpdateFailed: true,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			writePolicyStatus(context.Background(), tc.reconciler, tc.policy, logger, tc.isSuccess, tc.errString)
+		})
+	}
+}
+
+func TestPolicyReconcile(t *testing.T) {
+	tests := []struct {
+		name           string
+		policy         *configv1beta1.NamespacedPolicy
+		req            *reconcile.Request
+		expectedErr    bool
+		expectedPolicy bool
+	}{
+		{
+			name: "nonexistent policy",
+			req: &reconcile.Request{
+				NamespacedName: types.NamespacedName{Name: "nonexistent"},
+			},
+			policy: &configv1beta1.NamespacedPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      policyName1,
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: false,
+		},
+		{
+			name: "no policy parameters provided",
+			policy: &configv1beta1.NamespacedPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.NamespacedPolicySpec{
+					Type: "regopolicy",
+				},
+			},
+			expectedErr:    true,
+			expectedPolicy: false,
+		},
+		{
+			name: "wrong policy name",
+			policy: &configv1beta1.NamespacedPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      "ratify-policy2",
+				},
+				Spec: configv1beta1.NamespacedPolicySpec{
+					Type: "regopolicy",
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: false,
+		},
+		{
+			name: "invalid params",
+			policy: &configv1beta1.NamespacedPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.NamespacedPolicySpec{
+					Type: "regopolicy",
+					Parameters: runtime.RawExtension{
+						Raw: []byte("test"),
+					},
+				},
+			},
+			expectedErr:    true,
+			expectedPolicy: false,
+		},
+		{
+			name: "valid params",
+			policy: &configv1beta1.NamespacedPolicy{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: testNamespace,
+					Name:      "ratify-policy",
+				},
+				Spec: configv1beta1.NamespacedPolicySpec{
+					Type: "configpolicy",
+					Parameters: runtime.RawExtension{
+						Raw: []byte("{\"passthroughEnabled:\": false}"),
+					},
+				},
+			},
+			expectedErr:    false,
+			expectedPolicy: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			resetPolicyMap()
+			scheme, err := test.CreateScheme()
+			if err != nil {
+				t.Fatalf("CreateScheme() expected no error, actual %v", err)
+			}
+			client := fake.NewClientBuilder().WithScheme(scheme)
+			client.WithObjects(tt.policy)
+			r := &PolicyReconciler{
+				Scheme: scheme,
+				Client: client.Build(),
+			}
+			var req reconcile.Request
+			if tt.req != nil {
+				req = *tt.req
+			} else {
+				req = reconcile.Request{
+					NamespacedName: test.KeyFor(tt.policy),
+				}
+			}
+			_, err = r.Reconcile(context.Background(), req)
+			if tt.expectedErr != (err != nil) {
+				t.Fatalf("Reconcile() expected error to be %t, actual %t", tt.expectedErr, err != nil)
+			}
+			test := controllers.NamespacedPolicies
+
+			policy := test.GetPolicy(testNamespace)
+			if (policy != nil) != tt.expectedPolicy {
+				t.Fatalf("Expected policy to be %t, got %t", tt.expectedPolicy, policy != nil)
+			}
+		})
+	}
+}
+
+func resetPolicyMap() {
+	controllers.NamespacedPolicies = policies.NewActivePolicies()
+}
diff --git a/pkg/controllers/policy_controller_test.go b/pkg/controllers/utils/policy_test.go
similarity index 53%
rename from pkg/controllers/policy_controller_test.go
rename to pkg/controllers/utils/policy_test.go
index 6e81a57..752910c 100644
--- a/pkg/controllers/policy_controller_test.go
+++ b/pkg/controllers/utils/policy_test.go
@@ -3,9 +3,7 @@ Copyright The Ratify Authors.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-
 http://www.apache.org/licenses/LICENSE-2.0
-
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -13,55 +11,20 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package controllers
+package utils
 
 import (
-	"context"
-	"errors"
 	"reflect"
 	"testing"
 
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
-	"github.com/deislabs/ratify/pkg/policyprovider/config"
 	_ "github.com/deislabs/ratify/pkg/policyprovider/configpolicy"
-	"github.com/sirupsen/logrus"
-	"k8s.io/apimachinery/pkg/runtime"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-)
 
-const (
-	policyName1 = "policy1"
-	policyName2 = "policy2"
+	"github.com/deislabs/ratify/pkg/policyprovider/config"
+	"k8s.io/apimachinery/pkg/runtime"
 )
 
-type mockResourceWriter struct {
-	updateFailed bool
-}
-
-func (w mockResourceWriter) Create(_ context.Context, _ client.Object, _ client.Object, _ ...client.SubResourceCreateOption) error {
-	return nil
-}
-
-func (w mockResourceWriter) Update(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error {
-	if w.updateFailed {
-		return errors.New("update failed")
-	}
-	return nil
-}
-
-func (w mockResourceWriter) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error {
-	return nil
-}
-
-type mockStatusClient struct {
-	updateFailed bool
-}
-
-func (c mockStatusClient) Status() client.SubResourceWriter {
-	writer := mockResourceWriter{}
-	writer.updateFailed = c.updateFailed
-	return writer
-}
+const policyName1 = "policy1"
 
 func TestRawToPolicyConfig(t *testing.T) {
 	testCases := []struct {
@@ -151,7 +114,7 @@ func TestSpecToPolicyEnforcer(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			provider, err := specToPolicyEnforcer(tc.spec)
+			provider, err := SpecToPolicyEnforcer(tc.spec.Parameters.Raw, tc.spec.Type)
 
 			if tc.expectErr != (err != nil) {
 				t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil)
@@ -162,79 +125,3 @@ func TestSpecToPolicyEnforcer(t *testing.T) {
 		})
 	}
 }
-
-func TestPolicyAddOrReplace(t *testing.T) {
-	testCases := []struct {
-		name       string
-		spec       configv1beta1.PolicySpec
-		policyName string
-		expectErr  bool
-	}{
-		{
-			name: "invalid spec",
-			spec: configv1beta1.PolicySpec{
-				Type: policyName1,
-			},
-			expectErr: true,
-		},
-		{
-			name: "valid spec",
-			spec: configv1beta1.PolicySpec{
-				Parameters: runtime.RawExtension{
-					Raw: []byte("{\"name\": \"configpolicy\"}"),
-				},
-				Type: "configpolicy",
-			},
-			expectErr: false,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			err := policyAddOrReplace(tc.spec)
-
-			if tc.expectErr != (err != nil) {
-				t.Fatalf("Expected error to be %t, got %t", tc.expectErr, err != nil)
-			}
-		})
-	}
-}
-
-func TestWritePolicyStatus(t *testing.T) {
-	logger := logrus.WithContext(context.Background())
-	testCases := []struct {
-		name       string
-		isSuccess  bool
-		policy     *configv1beta1.Policy
-		errString  string
-		reconciler client.StatusClient
-	}{
-		{
-			name:       "success status",
-			isSuccess:  true,
-			policy:     &configv1beta1.Policy{},
-			reconciler: &mockStatusClient{},
-		},
-		{
-			name:       "error status",
-			isSuccess:  false,
-			policy:     &configv1beta1.Policy{},
-			errString:  "a long error string that exceeds the max length of 30 characters",
-			reconciler: &mockStatusClient{},
-		},
-		{
-			name:      "status update failed",
-			isSuccess: true,
-			policy:    &configv1beta1.Policy{},
-			reconciler: &mockStatusClient{
-				updateFailed: true,
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			writePolicyStatus(context.Background(), tc.reconciler, tc.policy, logger, tc.isSuccess, tc.errString)
-		})
-	}
-}
diff --git a/pkg/controllers/verifier_controller_test.go b/pkg/controllers/verifier_controller_test.go
index 71d2afa..105b67a 100644
--- a/pkg/controllers/verifier_controller_test.go
+++ b/pkg/controllers/verifier_controller_test.go
@@ -17,6 +17,7 @@ package controllers
 
 import (
 	"context"
+	"errors"
 	"os"
 	"strings"
 	"testing"
@@ -30,6 +31,35 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+type mockResourceWriter struct {
+	updateFailed bool
+}
+
+func (w mockResourceWriter) Create(_ context.Context, _ client.Object, _ client.Object, _ ...client.SubResourceCreateOption) error {
+	return nil
+}
+
+func (w mockResourceWriter) Update(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error {
+	if w.updateFailed {
+		return errors.New("update failed")
+	}
+	return nil
+}
+
+func (w mockResourceWriter) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error {
+	return nil
+}
+
+type mockStatusClient struct {
+	updateFailed bool
+}
+
+func (c mockStatusClient) Status() client.SubResourceWriter {
+	writer := mockResourceWriter{}
+	writer.updateFailed = c.updateFailed
+	return writer
+}
+
 const licenseChecker = "licensechecker"
 
 func TestMain(m *testing.M) {
diff --git a/pkg/customresources/policies/policies_test.go b/pkg/customresources/policies/policies_test.go
index 69cd407..6acfbc4 100644
--- a/pkg/customresources/policies/policies_test.go
+++ b/pkg/customresources/policies/policies_test.go
@@ -62,17 +62,9 @@ var (
 func TestPoliciesOperations(t *testing.T) {
 	policies := NewActivePolicies()
 
-	if !policies.IsEmpty() {
-		t.Errorf("Expected policies to be empty")
-	}
-
 	policies.AddPolicy(namespace1, name1, policy1)
 	policies.AddPolicy(namespace2, name1, policy2)
 
-	if policies.IsEmpty() {
-		t.Errorf("Expected policies to not be empty")
-	}
-
 	if policies.GetPolicy(namespace1) != policy1 {
 		t.Errorf("Expected policy1 to be returned")
 	}
diff --git a/pkg/utils/test_utils.go b/pkg/utils/test_utils.go
index 4cb400d..5e01007 100644
--- a/pkg/utils/test_utils.go
+++ b/pkg/utils/test_utils.go
@@ -16,10 +16,66 @@ limitations under the License.
 package utils
 
 import (
+	"context"
+	"errors"
 	"os"
 	"path/filepath"
+
+	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+type MockResourceWriter struct {
+	updateFailed bool
+}
+
+func (w MockResourceWriter) Create(_ context.Context, _ client.Object, _ client.Object, _ ...client.SubResourceCreateOption) error {
+	return nil
+}
+
+func (w MockResourceWriter) Update(_ context.Context, _ client.Object, _ ...client.SubResourceUpdateOption) error {
+	if w.updateFailed {
+		return errors.New("update failed")
+	}
+	return nil
+}
+
+func (w MockResourceWriter) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error {
+	return nil
+}
+
+type MockStatusClient struct {
+	UpdateFailed bool
+}
+
+func (c MockStatusClient) Status() client.SubResourceWriter {
+	writer := MockResourceWriter{}
+	writer.updateFailed = c.UpdateFailed
+	return writer
+}
+
+func CreateScheme() (*runtime.Scheme, error) {
+	scheme := runtime.NewScheme()
+
+	b := runtime.SchemeBuilder{
+		clientgoscheme.AddToScheme,
+		configv1beta1.AddToScheme,
+	}
+
+	if err := b.AddToScheme(scheme); err != nil {
+		return nil, err
+	}
+
+	return scheme, nil
+}
+
+func KeyFor(obj client.Object) types.NamespacedName {
+	return client.ObjectKeyFromObject(obj)
+}
+
 func CreatePlugin(pluginName string) (string, error) {
 	tempDir, err := os.MkdirTemp("", "directory")
 	if err != nil {