Repository: deislabs/ratify
Pull request #1358 (2024-04-01 14:35:27)

feat: add verifiers interface to wrap up operations on namespaced verifiers [multi-tenancy PR 2]
# Description

## What this PR does / why we need it:
This is the second PR for multi-tenancy support. Please review [this PR](https://github.com/deislabs/ratify/pull/1356) first.
1. Add a `VerifierManager` interface to wrap operations on the namespaced verifier resources.
2. Update verifier references to use the new Verifiers interface instead of directly accessing the verifier slice.
3. Move the `VerifierMap` variable under `controllers` to a separate file so that both clustered and namespaced controllers could share it in the future.
4. Add `context` param to the `getExecutor` method so that Ratify could fetch the corresponding verifiers/stores/policies per namespace during initializing executor.
5. Updated unit tests. Some unit tests are not added in this PR as we'll change the behavior soon.
6. For the interface implementation, it's in a transition stage that keeps the old behavior. Will update the implementation once Ratify fully supports multi-tenancy.

## 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/1358?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 `87.23404%` with `6 lines` in your changes are missing coverage. Please review.
> Project coverage is 65.09%. Comparing base [(`45c0f81`)](https://app.codecov.io/gh/deislabs/ratify/commit/45c0f81314ea5d26ec69672def593a6a8f464f1c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) to head [(`5bb783a`)](https://app.codecov.io/gh/deislabs/ratify/pull/1358?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs).
> Report is 38 commits behind head on main.

| [Files](https://app.codecov.io/gh/deislabs/ratify/pull/1358?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) | Patch % | Lines |
|---|---|---|
| [httpserver/handlers.go](https://app.codecov.io/gh/deislabs/ratify/pull/1358?src=pr&el=tree&filepath=httpserver%2Fhandlers.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-aHR0cHNlcnZlci9oYW5kbGVycy5nbw==) | 57.14% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1358?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [config/config.go](https://app.codecov.io/gh/deislabs/ratify/pull/1358?src=pr&el=tree&filepath=config%2Fconfig.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-Y29uZmlnL2NvbmZpZy5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1358?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs) |
| [pkg/controllers/verifier\_controller.go](https://app.codecov.io/gh/deislabs/ratify/pull/1358?src=pr&el=tree&filepath=pkg%2Fcontrollers%2Fverifier_controller.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=deislabs#diff-cGtnL2NvbnRyb2xsZXJzL3ZlcmlmaWVyX2NvbnRyb2xsZXIuZ28=) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/deislabs/ratify/pull/1358?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             @@
##             main    #1358      +/-   ##
==========================================
+ Coverage   61.60%   65.09%   +3.49%     
==========================================
  Files          98       88      -10     
  Lines        6248     4747    -1501     
==========================================
- Hits         3849     3090     -759     
+ Misses       2066     1368     -698     
+ Partials      333      289      -44     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/deislabs/ratify/pull/1358?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).


Patch:
diff --git a/config/configManager.go b/config/configManager.go
index e43f253..a507d32 100644
--- a/config/configManager.go
+++ b/config/configManager.go
@@ -16,6 +16,7 @@ limitations under the License.
 package config
 
 import (
+	"context"
 	"os"
 	"time"
 
@@ -25,7 +26,7 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-type GetExecutor func() *ef.Executor
+type GetExecutor func(context.Context) *ef.Executor
 
 var (
 	configHash string
@@ -38,7 +39,7 @@ func GetExecutorAndWatchForUpdate(configFilePath string) (GetExecutor, error) {
 	cf, err := Load(configFilePath)
 
 	if err != nil {
-		return func() *ef.Executor { return &ef.Executor{} }, err
+		return func(context.Context) *ef.Executor { return &ef.Executor{} }, err
 	}
 
 	configHash = cf.fileHash
@@ -46,7 +47,7 @@ func GetExecutorAndWatchForUpdate(configFilePath string) (GetExecutor, error) {
 	stores, verifiers, policyEnforcer, err := CreateFromConfig(cf)
 
 	if err != nil {
-		return func() *ef.Executor { return &ef.Executor{} }, err
+		return func(context.Context) *ef.Executor { return &ef.Executor{} }, err
 	}
 
 	executor = ef.Executor{
@@ -59,12 +60,12 @@ func GetExecutorAndWatchForUpdate(configFilePath string) (GetExecutor, error) {
 	err = watchForConfigurationChange(configFilePath)
 
 	if err != nil {
-		return func() *ef.Executor { return &ef.Executor{} }, err
+		return func(context.Context) *ef.Executor { return &ef.Executor{} }, err
 	}
 
 	logrus.Info("configuration successfully loaded.")
 
-	return func() *ef.Executor { return &executor }, nil
+	return func(context.Context) *ef.Executor { return &executor }, nil
 }
 
 func reloadExecutor(configFilePath string) {
diff --git a/httpserver/handlers.go b/httpserver/handlers.go
index 57c999b..46267bf 100644
--- a/httpserver/handlers.go
+++ b/httpserver/handlers.go
@@ -83,11 +83,6 @@ func (server *Server) verify(ctx context.Context, w http.ResponseWriter, r *http
 				results = append(results, returnItem)
 				mu.Unlock()
 			}()
-			if err := server.validateComponents(verifyComponents); err != nil {
-				logger.GetLogger(ctx, server.LogOption).Error(err)
-				returnItem.Error = err.Error()
-				return
-			}
 			requestKey, err := pkgUtils.ParseRequestKey(key)
 			if err != nil {
 				returnItem.Error = err.Error()
@@ -100,6 +95,12 @@ func (server *Server) verify(ctx context.Context, w http.ResponseWriter, r *http
 			}
 			ctx = ctxUtils.SetContextWithNamespace(ctx, requestKey.Namespace)
 
+			if err := server.validateComponents(ctx, verifyComponents); err != nil {
+				logger.GetLogger(ctx, server.LogOption).Error(err)
+				returnItem.Error = err.Error()
+				return
+			}
+
 			if subjectReference.Digest.String() == "" {
 				logger.GetLogger(ctx, server.LogOption).Warn("Digest should be used instead of tagged reference. The resolved digest may not point to the same signed artifact, since tags are mutable.")
 			}
@@ -129,7 +130,7 @@ func (server *Server) verify(ctx context.Context, w http.ResponseWriter, r *http
 				verifyParameters := executor.VerifyParameters{
 					Subject: resolvedSubjectReference,
 				}
-				if result, err = server.GetExecutor().VerifySubject(ctx, verifyParameters); err != nil {
+				if result, err = server.GetExecutor(ctx).VerifySubject(ctx, verifyParameters); err != nil {
 					returnItem.Error = errors.ErrorCodeExecutorFailure.WithError(err).WithComponentType(errors.Executor).Error()
 					return
 				}
@@ -145,7 +146,7 @@ func (server *Server) verify(ctx context.Context, w http.ResponseWriter, r *http
 					logger.GetLogger(ctx, server.LogOption).Infof("verify result for subject %s: %s", resolvedSubjectReference, string(res))
 				}
 			}
-			returnItem.Value = fromVerifyResult(result, server.GetExecutor().PolicyEnforcer.GetPolicyType(ctx))
+			returnItem.Value = fromVerifyResult(result, server.GetExecutor(ctx).PolicyEnforcer.GetPolicyType(ctx))
 			logger.GetLogger(ctx, server.LogOption).Debugf("verification: execution time for image %s: %dms", resolvedSubjectReference, time.Since(routineStartTime).Milliseconds())
 		}(utils.SanitizeString(key), ctx)
 	}
@@ -192,11 +193,6 @@ func (server *Server) mutate(ctx context.Context, w http.ResponseWriter, r *http
 				results = append(results, returnItem)
 				mu.Unlock()
 			}()
-			if err := server.validateComponents(mutateComponents); err != nil {
-				logger.GetLogger(ctx, server.LogOption).Error(err)
-				returnItem.Error = err.Error()
-				return
-			}
 			requestKey, err := pkgUtils.ParseRequestKey(image)
 			if err != nil {
 				returnItem.Error = err.Error()
@@ -209,11 +205,18 @@ func (server *Server) mutate(ctx context.Context, w http.ResponseWriter, r *http
 				returnItem.Error = err.Error()
 				return
 			}
+
 			ctx = ctxUtils.SetContextWithNamespace(ctx, requestKey.Namespace)
 
+			if err := server.validateComponents(ctx, mutateComponents); err != nil {
+				logger.GetLogger(ctx, server.LogOption).Error(err)
+				returnItem.Error = err.Error()
+				return
+			}
+
 			if parsedReference.Digest == "" {
 				var selectedStore referrerstore.ReferrerStore
-				for _, store := range server.GetExecutor().ReferrerStores {
+				for _, store := range server.GetExecutor(ctx).ReferrerStores {
 					if store.Name() == server.MutationStoreName {
 						selectedStore = store
 						break
@@ -243,20 +246,20 @@ func (server *Server) mutate(ctx context.Context, w http.ResponseWriter, r *http
 	return sendResponse(&results, "", w, http.StatusOK, true)
 }
 
-func (server *Server) validateComponents(handlerComponents string) error {
+func (server *Server) validateComponents(ctx context.Context, handlerComponents string) error {
 	if handlerComponents == mutateComponents {
-		if len(server.GetExecutor().ReferrerStores) == 0 {
+		if len(server.GetExecutor(ctx).ReferrerStores) == 0 {
 			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.ReferrerStore).WithDetail("referrer store config should have at least one store")
 		}
 	}
 	if handlerComponents == verifyComponents {
-		if len(server.GetExecutor().ReferrerStores) == 0 {
+		if len(server.GetExecutor(ctx).ReferrerStores) == 0 {
 			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.ReferrerStore).WithDetail("referrer store config should have at least one store")
 		}
-		if server.GetExecutor().PolicyEnforcer == nil {
+		if server.GetExecutor(ctx).PolicyEnforcer == nil {
 			return errors.ErrorCodeConfigInvalid.WithComponentType(errors.PolicyProvider).WithDetail("policy provider config must be specified")
 		}
-		if len(server.GetExecutor().Verifiers) == 0 {
+		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/httpserver/server.go b/httpserver/server.go
index c52daf1..bca7db4 100644
--- a/httpserver/server.go
+++ b/httpserver/server.go
@@ -178,13 +178,13 @@ func (server *Server) registerHandlers() error {
 	if err != nil {
 		return err
 	}
-	server.register(http.MethodPost, verifyPath, processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false))
+	server.register(http.MethodPost, verifyPath, processTimeout(server.verify, server.GetExecutor(server.Context).GetVerifyRequestTimeout(), false))
 
 	mutatePath, err := url.JoinPath(ServerRootURL, "mutate")
 	if err != nil {
 		return err
 	}
-	server.register(http.MethodPost, mutatePath, processTimeout(server.mutate, server.GetExecutor().GetMutationRequestTimeout(), true))
+	server.register(http.MethodPost, mutatePath, processTimeout(server.mutate, server.GetExecutor(server.Context).GetMutationRequestTimeout(), true))
 
 	return nil
 }
diff --git a/pkg/controllers/resource_map.go b/pkg/controllers/resource_map.go
new file mode 100644
index 0000000..bc2ea14
--- /dev/null
+++ b/pkg/controllers/resource_map.go
@@ -0,0 +1,18 @@
+/*
+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 controllers
+
+import "github.com/deislabs/ratify/pkg/customresources/verifiers"
+
+var VerifierMap = verifiers.NewActiveVerifiers()
diff --git a/pkg/controllers/verifier_controller.go b/pkg/controllers/verifier_controller.go
index 31cb83a..1cd8bb7 100644
--- a/pkg/controllers/verifier_controller.go
+++ b/pkg/controllers/verifier_controller.go
@@ -24,8 +24,8 @@ import (
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
 	"github.com/deislabs/ratify/config"
 	re "github.com/deislabs/ratify/errors"
+	"github.com/deislabs/ratify/internal/constants"
 	"github.com/deislabs/ratify/pkg/utils"
-	vr "github.com/deislabs/ratify/pkg/verifier"
 	vc "github.com/deislabs/ratify/pkg/verifier/config"
 	vf "github.com/deislabs/ratify/pkg/verifier/factory"
 	"github.com/deislabs/ratify/pkg/verifier/types"
@@ -43,11 +43,6 @@ type VerifierReconciler struct {
 	Scheme *runtime.Scheme
 }
 
-var (
-	// a map to track of active verifiers
-	VerifierMap = map[string]vr.ReferenceVerifier{}
-)
-
 //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=verifiers,verbs=get;list;watch;create;update;patch;delete
 //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=verifiers/status,verbs=get;update;patch
 //+kubebuilder:rbac:groups=config.ratify.deislabs.io,resources=verifiers/finalizers,verbs=update
@@ -72,7 +67,8 @@ func (r *VerifierReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
 	if err := r.Get(ctx, req.NamespacedName, &verifier); err != nil {
 		if apierrors.IsNotFound(err) {
 			verifierLogger.Infof("delete event detected, removing verifier %v", resource)
-			verifierRemove(resource)
+			// TODO: pass the actual namespace once multi-tenancy is supported.
+			VerifierMap.DeleteVerifier(constants.EmptyNamespace, resource)
 		} else {
 			verifierLogger.Error(err, "unable to fetch verifier")
 		}
@@ -122,17 +118,13 @@ func verifierAddOrReplace(spec configv1beta1.VerifierSpec, objectName string, na
 		logrus.Error(err, "unable to create verifier from verifier config")
 		return err
 	}
-	VerifierMap[objectName] = referenceVerifier
+	// TODO: pass the actual namespace once multi-tenancy is supported.
+	VerifierMap.AddVerifier(constants.EmptyNamespace, objectName, referenceVerifier)
 	logrus.Infof("verifier '%v' added to verifier map", referenceVerifier.Name())
 
 	return nil
 }
 
-// remove verifier from map
-func verifierRemove(objectName string) {
-	delete(VerifierMap, objectName)
-}
-
 // returns a verifier reference from spec
 func specToVerifierConfig(verifierSpec configv1beta1.VerifierSpec, verifierName string) (vc.VerifierConfig, error) {
 	verifierConfig := vc.VerifierConfig{}
diff --git a/pkg/customresources/verifiers/api.go b/pkg/customresources/verifiers/api.go
new file mode 100644
index 0000000..7d35e7b
--- /dev/null
+++ b/pkg/customresources/verifiers/api.go
@@ -0,0 +1,38 @@
+/*
+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 verifiers
+
+import (
+	vr "github.com/deislabs/ratify/pkg/verifier"
+)
+
+// VerifierManager is an interface that defines the methods for managing verifiers across different scopes.
+type VerifierManager interface {
+	// GetVerifiers returns verifiers under the given scope.
+	GetVerifiers(scope string) []vr.ReferenceVerifier
+
+	// AddVerifier adds a verifier to the given scope.
+	AddVerifier(scope, verifierName string, verifier vr.ReferenceVerifier)
+
+	// DeleteVerifier deletes a verifier from the given scope.
+	DeleteVerifier(scope, verifierName string)
+
+	// IsEmpty returns true if verifiers are empty.
+	IsEmpty() bool
+
+	// GetVerifierCount returns the number of verifiers in all scopes.
+	GetVerifierCount() int
+}
diff --git a/pkg/customresources/verifiers/verifiers.go b/pkg/customresources/verifiers/verifiers.go
new file mode 100644
index 0000000..5825353
--- /dev/null
+++ b/pkg/customresources/verifiers/verifiers.go
@@ -0,0 +1,81 @@
+/*
+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 verifiers
+
+import (
+	vr "github.com/deislabs/ratify/pkg/verifier"
+)
+
+// ActiveVerifiers implements VerifierManger interface.
+type ActiveVerifiers struct {
+	// TODO: Implement concurrent safety using sync.Map
+	// The structure of the map is as follows:
+	// The first level maps from scope to verifiers
+	// The second level maps from verifier name to verifier
+	// Example:
+	// {
+	//   "namespace1": {
+	//     "verifier1": verifier1,
+	//     "verifier2": verifier2
+	//   }
+	// }
+	// 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.
+	ScopedVerifiers map[string]map[string]vr.ReferenceVerifier
+}
+
+func NewActiveVerifiers() VerifierManager {
+	return &ActiveVerifiers{
+		ScopedVerifiers: make(map[string]map[string]vr.ReferenceVerifier),
+	}
+}
+
+// GetVerifiers implements the Verifiers interface.
+// It returns a list of verifiers for the given scope. If no verifiers are found for the given scope, it returns cluster-wide verifiers.
+// TODO: Current implementation fetches verifiers for all namespaces including cluster-wide ones. Will support actual namespace based verifiers in future.
+func (v *ActiveVerifiers) GetVerifiers(_ string) []vr.ReferenceVerifier {
+	verifiers := []vr.ReferenceVerifier{}
+	for _, namespacedVerifiers := range v.ScopedVerifiers {
+		for _, verifier := range namespacedVerifiers {
+			verifiers = append(verifiers, verifier)
+		}
+	}
+	return verifiers
+}
+
+func (v *ActiveVerifiers) AddVerifier(scope, verifierName string, verifier vr.ReferenceVerifier) {
+	if _, ok := v.ScopedVerifiers[scope]; !ok {
+		v.ScopedVerifiers[scope] = make(map[string]vr.ReferenceVerifier)
+	}
+	v.ScopedVerifiers[scope][verifierName] = verifier
+}
+
+func (v *ActiveVerifiers) DeleteVerifier(scope, verifierName string) {
+	if verifiers, ok := v.ScopedVerifiers[scope]; ok {
+		delete(verifiers, verifierName)
+	}
+}
+
+func (v *ActiveVerifiers) IsEmpty() bool {
+	return v.GetVerifierCount() == 0
+}
+
+func (v *ActiveVerifiers) GetVerifierCount() int {
+	count := 0
+	for _, verifiers := range v.ScopedVerifiers {
+		count += len(verifiers)
+	}
+	return count
+}
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
index 36cd2cd..230e17f 100644
--- a/pkg/manager/manager.go
+++ b/pkg/manager/manager.go
@@ -49,10 +49,10 @@ import (
 
 	configv1alpha1 "github.com/deislabs/ratify/api/v1alpha1"
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
+	ctxUtils "github.com/deislabs/ratify/internal/context"
 	"github.com/deislabs/ratify/pkg/controllers"
 	ef "github.com/deislabs/ratify/pkg/executor/core"
 	"github.com/deislabs/ratify/pkg/referrerstore"
-	vr "github.com/deislabs/ratify/pkg/verifier"
 	//+kubebuilder:scaffold:imports
 )
 
@@ -84,17 +84,11 @@ func StartServer(httpServerAddress, configFilePath, certDirectory, caCertFile st
 	}
 
 	// initialize server
-	server, err := httpserver.NewServer(context.Background(), httpServerAddress, func() *ef.Executor {
-		var activeVerifiers []vr.ReferenceVerifier
+	server, err := httpserver.NewServer(context.Background(), httpServerAddress, func(ctx context.Context) *ef.Executor {
 		var activeStores []referrerstore.ReferrerStore
 		var activePolicyEnforcer policyprovider.PolicyProvider
 
-		// check if there are active verifiers from crd controller
-		if len(controllers.VerifierMap) > 0 {
-			for _, value := range controllers.VerifierMap {
-				activeVerifiers = append(activeVerifiers, value)
-			}
-		}
+		activeVerifiers := controllers.VerifierMap.GetVerifiers(ctxUtils.GetNamespace(ctx))
 
 		// check if there are active stores from crd controller
 		if len(controllers.StoreMap) > 0 {


Test patch:
diff --git a/httpserver/server_test.go b/httpserver/server_test.go
index 9a45872..cb04b73 100644
--- a/httpserver/server_test.go
+++ b/httpserver/server_test.go
@@ -46,7 +46,7 @@ import (
 const testArtifactType string = "test-type1"
 const testImageNameTagged string = "localhost:5000/net-monitor:v1"
 
-func testGetExecutor() *core.Executor {
+func testGetExecutor(context.Context) *core.Executor {
 	return &core.Executor{
 		Verifiers:      []verifier.ReferenceVerifier{},
 		ReferrerStores: []referrerstore.ReferrerStore{},
@@ -138,7 +138,7 @@ func TestServer_Timeout_Failed(t *testing.T) {
 			Verifiers:      []verifier.ReferenceVerifier{ver},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -151,7 +151,7 @@ func TestServer_Timeout_Failed(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -209,7 +209,7 @@ func TestServer_MultipleSubjects_Success(t *testing.T) {
 			},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -222,7 +222,7 @@ func TestServer_MultipleSubjects_Success(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -280,7 +280,7 @@ func TestServer_Mutation_Success(t *testing.T) {
 			Verifiers:      []verifier.ReferenceVerifier{ver},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -294,7 +294,7 @@ func TestServer_Mutation_Success(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.mutate, server.GetExecutor().GetMutationRequestTimeout(), true),
+			handler: processTimeout(server.mutate, server.GetExecutor(nil).GetMutationRequestTimeout(), true),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -356,7 +356,7 @@ func TestServer_Mutation_ReferrerStoreConfigInvalid_Failure(t *testing.T) {
 			Verifiers:      []verifier.ReferenceVerifier{ver},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -370,7 +370,7 @@ func TestServer_Mutation_ReferrerStoreConfigInvalid_Failure(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.mutate, server.GetExecutor().GetMutationRequestTimeout(), true),
+			handler: processTimeout(server.mutate, server.GetExecutor(nil).GetMutationRequestTimeout(), true),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -439,7 +439,7 @@ func TestServer_MultipleRequestsForSameSubject_Success(t *testing.T) {
 			},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -452,7 +452,7 @@ func TestServer_MultipleRequestsForSameSubject_Success(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -491,7 +491,7 @@ func TestServer_Verify_ParseReference_Failure(t *testing.T) {
 			},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -504,7 +504,7 @@ func TestServer_Verify_ParseReference_Failure(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -564,7 +564,7 @@ func TestServer_Verify_PolicyEnforcerConfigInvalid_Failure(t *testing.T) {
 			Verifiers:      []verifier.ReferenceVerifier{ver},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -578,7 +578,7 @@ func TestServer_Verify_PolicyEnforcerConfigInvalid_Failure(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
@@ -633,7 +633,7 @@ func TestServer_Verify_VerifierConfigInvalid_Failure(t *testing.T) {
 			Verifiers:      []verifier.ReferenceVerifier{},
 		}
 
-		getExecutor := func() *core.Executor {
+		getExecutor := func(context.Context) *core.Executor {
 			return ex
 		}
 
@@ -647,7 +647,7 @@ func TestServer_Verify_VerifierConfigInvalid_Failure(t *testing.T) {
 
 		handler := contextHandler{
 			context: server.Context,
-			handler: processTimeout(server.verify, server.GetExecutor().GetVerifyRequestTimeout(), false),
+			handler: processTimeout(server.verify, server.GetExecutor(nil).GetVerifyRequestTimeout(), false),
 		}
 
 		handler.ServeHTTP(responseRecorder, request)
diff --git a/pkg/controllers/verifier_controller_test.go b/pkg/controllers/verifier_controller_test.go
index 6ecad40..48ceea6 100644
--- a/pkg/controllers/verifier_controller_test.go
+++ b/pkg/controllers/verifier_controller_test.go
@@ -23,8 +23,8 @@ import (
 
 	configv1beta1 "github.com/deislabs/ratify/api/v1beta1"
 	"github.com/deislabs/ratify/internal/constants"
+	"github.com/deislabs/ratify/pkg/customresources/verifiers"
 	"github.com/deislabs/ratify/pkg/utils"
-	vr "github.com/deislabs/ratify/pkg/verifier"
 	"github.com/sirupsen/logrus"
 	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -34,7 +34,7 @@ const licenseChecker = "licensechecker"
 
 func TestMain(m *testing.M) {
 	// make sure to reset verifierMap before each test run
-	VerifierMap = map[string]vr.ReferenceVerifier{}
+	VerifierMap = verifiers.NewActiveVerifiers()
 	code := m.Run()
 	os.Exit(code)
 }
@@ -56,15 +56,15 @@ func TestVerifierAdd_EmptyParameter(t *testing.T) {
 	if err := verifierAddOrReplace(testVerifierSpec, sampleName, constants.EmptyNamespace); err != nil {
 		t.Fatalf("verifierAddOrReplace() expected no error, actual %v", err)
 	}
-	if len(VerifierMap) != 1 {
-		t.Fatalf("Verifier map expected size 1, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 1 {
+		t.Fatalf("Verifier map expected size 1, actual %v", VerifierMap.GetVerifierCount())
 	}
 }
 
 func TestVerifierAdd_WithParameters(t *testing.T) {
 	resetVerifierMap()
-	if len(VerifierMap) != 0 {
-		t.Fatalf("Verifier map expected size 0, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 0 {
+		t.Fatalf("Verifier map expected size 0, actual %v", VerifierMap.GetVerifierCount())
 	}
 
 	dirPath, err := utils.CreatePlugin(licenseChecker)
@@ -78,8 +78,8 @@ func TestVerifierAdd_WithParameters(t *testing.T) {
 	if err := verifierAddOrReplace(testVerifierSpec, "testObject", constants.EmptyNamespace); err != nil {
 		t.Fatalf("verifierAddOrReplace() expected no error, actual %v", err)
 	}
-	if len(VerifierMap) != 1 {
-		t.Fatalf("Verifier map expected size 1, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 1 {
+		t.Fatalf("Verifier map expected size 1, actual %v", VerifierMap.GetVerifierCount())
 	}
 }
 
@@ -109,8 +109,8 @@ func TestVerifier_UpdateAndDelete(t *testing.T) {
 	if err := verifierAddOrReplace(testVerifierSpec, licenseChecker, constants.EmptyNamespace); err != nil {
 		t.Fatalf("verifierAddOrReplace() expected no error, actual %v", err)
 	}
-	if len(VerifierMap) != 1 {
-		t.Fatalf("Verifier map expected size 1, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 1 {
+		t.Fatalf("Verifier map expected size 1, actual %v", VerifierMap.GetVerifierCount())
 	}
 
 	// modify the verifier
@@ -121,14 +121,14 @@ func TestVerifier_UpdateAndDelete(t *testing.T) {
 	}
 
 	// validate no verifier has been added
-	if len(VerifierMap) != 1 {
-		t.Fatalf("Verifier map should be 1 after replacement, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 1 {
+		t.Fatalf("Verifier map should be 1 after replacement, actual %v", VerifierMap.GetVerifierCount())
 	}
 
-	verifierRemove(licenseChecker)
+	VerifierMap.DeleteVerifier(constants.EmptyNamespace, licenseChecker)
 
-	if len(VerifierMap) != 0 {
-		t.Fatalf("Verifier map should be 0 after deletion, actual %v", len(VerifierMap))
+	if VerifierMap.GetVerifierCount() != 0 {
+		t.Fatalf("Verifier map should be 0 after deletion, actual %v", VerifierMap.GetVerifierCount())
 	}
 }
 
@@ -206,7 +206,7 @@ func TestGetCertStoreNamespace(t *testing.T) {
 }
 
 func resetVerifierMap() {
-	VerifierMap = map[string]vr.ReferenceVerifier{}
+	VerifierMap = verifiers.NewActiveVerifiers()
 }
 
 func getLicenseCheckerFromParam(parametersString, pluginPath string) configv1beta1.VerifierSpec {
diff --git a/pkg/customresources/verifiers/verifiers_test.go b/pkg/customresources/verifiers/verifiers_test.go
new file mode 100644
index 0000000..652a9cf
--- /dev/null
+++ b/pkg/customresources/verifiers/verifiers_test.go
@@ -0,0 +1,105 @@
+/*
+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 verifiers
+
+import (
+	"context"
+	"testing"
+
+	"github.com/deislabs/ratify/internal/constants"
+	"github.com/deislabs/ratify/pkg/common"
+	"github.com/deislabs/ratify/pkg/ocispecs"
+	"github.com/deislabs/ratify/pkg/referrerstore"
+	"github.com/deislabs/ratify/pkg/verifier"
+)
+
+type mockVerifier struct {
+	name string
+}
+
+func (v mockVerifier) Name() string {
+	return v.name
+}
+
+func (v mockVerifier) Type() string {
+	return "mockType"
+}
+
+func (v mockVerifier) CanVerify(_ context.Context, _ ocispecs.ReferenceDescriptor) bool {
+	return true
+}
+
+func (v mockVerifier) Verify(_ context.Context, _ common.Reference, _ ocispecs.ReferenceDescriptor, _ referrerstore.ReferrerStore) (verifier.VerifierResult, error) {
+	return verifier.VerifierResult{}, nil
+}
+
+func (v mockVerifier) GetNestedReferences() []string {
+	return nil
+}
+
+const (
+	namespace1 = constants.EmptyNamespace
+	namespace2 = "namespace2"
+	name1      = "name1"
+	name2      = "name2"
+)
+
+var (
+	verifier1 = mockVerifier{name: name1}
+	verifier2 = mockVerifier{name: name2}
+)
+
+func TestVerifiersOperations(t *testing.T) {
+	verifiers := NewActiveVerifiers()
+	verifiers.AddVerifier(namespace1, verifier1.Name(), verifier1)
+	verifiers.AddVerifier(namespace1, verifier2.Name(), verifier2)
+	verifiers.AddVerifier(namespace2, verifier1.Name(), verifier1)
+	verifiers.AddVerifier(namespace2, verifier2.Name(), verifier2)
+
+	if verifiers.IsEmpty() {
+		t.Error("Expected verifiers to not be empty")
+	}
+
+	if verifiers.GetVerifierCount() != 4 {
+		t.Errorf("Expected 4 verifiers, got %d", verifiers.GetVerifierCount())
+	}
+
+	if len(verifiers.GetVerifiers(namespace1)) != 4 {
+		t.Errorf("Expected 4 verifiers, got %d", len(verifiers.GetVerifiers(namespace1)))
+	}
+
+	if len(verifiers.GetVerifiers(namespace2)) != 4 {
+		t.Errorf("Expected 4 verifiers, got %d", len(verifiers.GetVerifiers(namespace2)))
+	}
+
+	verifiers.DeleteVerifier(namespace2, verifier1.Name())
+	verifiers.DeleteVerifier(namespace2, verifier2.Name())
+
+	if len(verifiers.GetVerifiers(namespace2)) != 2 {
+		t.Errorf("Expected 2 verifiers, got %d", len(verifiers.GetVerifiers(namespace2)))
+	}
+
+	verifiers.DeleteVerifier(namespace1, verifier1.Name())
+	verifiers.DeleteVerifier(namespace1, verifier2.Name())
+
+	if !verifiers.IsEmpty() {
+		t.Error("Expected verifiers to be empty")
+	}
+
+	if verifiers.GetVerifierCount() != 0 {
+		t.Errorf("Expected 0 verifiers, got %d", verifiers.GetVerifierCount())
+	}
+}