diff --git a/cmd/smith/app/bundle_controller.go b/cmd/smith/app/bundle_controller.go index 5628787..dc4734a 100644 --- a/cmd/smith/app/bundle_controller.go +++ b/cmd/smith/app/bundle_controller.go @@ -116,8 +116,9 @@ func (c *BundleControllerConstructor) New(config *ctrl.Config, cctx *ctrl.Contex if err != nil { return nil, err } + crdGVK := apiext_v1b1.SchemeGroupVersion.WithKind("CustomResourceDefinition") crdInf, err := apiExtensionsInformer(config, cctx, apiExtClient, - apiext_v1b1.SchemeGroupVersion.WithKind("CustomResourceDefinition"), + crdGVK, apiext_v1b1inf.NewCustomResourceDefinitionInformer) if err != nil { return nil, err @@ -148,7 +149,7 @@ func (c *BundleControllerConstructor) New(config *ctrl.Config, cctx *ctrl.Contex if err != nil { return nil, err } - resourceInfs[apiext_v1b1.SchemeGroupVersion.WithKind("CustomResourceDefinition")] = crdInf + resourceInfs[crdGVK] = crdInf resourceInfs[smith_v1.BundleGVK] = bundleInf for gvk, inf := range resourceInfs { if err = multiStore.AddInformer(gvk, inf); err != nil { @@ -226,7 +227,10 @@ func (c *BundleControllerConstructor) New(config *ctrl.Config, cctx *ctrl.Contex Broadcaster: broadcaster, Recorder: recorder, } - cntrlr.Prepare(crdInf, resourceInfs) + err = cntrlr.Prepare(crdInf, resourceInfs) + if err != nil { + return nil, err + } return &ctrl.Constructed{ Interface: cntrlr, diff --git a/it/BUILD.bazel b/it/BUILD.bazel index 7728f87..b83390e 100644 --- a/it/BUILD.bazel +++ b/it/BUILD.bazel @@ -26,6 +26,7 @@ go_library( "//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/go.uber.org/zap:go_default_library", "//vendor/go.uber.org/zap/zaptest:go_default_library", + "//vendor/k8s.io/api/apps/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1beta1:go_default_library", @@ -34,6 +35,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/kubernetes:go_default_library", @@ -51,6 +53,7 @@ go_test( srcs = [ "adoption_test.go", "crd_attribute_test.go", + "deployment_dependencies_test.go", "deployment_ready_test.go", "main_test.go", "resource_deletion_test.go", @@ -67,6 +70,7 @@ go_test( "//examples/sleeper:go_default_library", "//examples/sleeper/pkg/apis/sleeper/v1:go_default_library", "//pkg/apis/smith/v1:go_default_library", + "//pkg/specchecker/builtin:go_default_library", "//pkg/util/testing:go_default_library", "//vendor/github.com/ash2k/stager:go_default_library", "//vendor/github.com/atlassian/ctrl/apis/condition/v1:go_default_library", @@ -76,7 +80,10 @@ go_test( "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library", + "//vendor/k8s.io/client-go/tools/cache:go_default_library", + "//vendor/k8s.io/client-go/tools/watch:go_default_library", ], ) diff --git a/it/deployment_dependencies_test.go b/it/deployment_dependencies_test.go new file mode 100644 index 0000000..0cd6704 --- /dev/null +++ b/it/deployment_dependencies_test.go @@ -0,0 +1,196 @@ +package it + +import ( + "context" + "testing" + + smith_v1 "github.com/atlassian/smith/pkg/apis/smith/v1" + "github.com/atlassian/smith/pkg/specchecker/builtin" + "github.com/stretchr/testify/require" + apps_v1 "k8s.io/api/apps/v1" + core_v1 "k8s.io/api/core/v1" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/tools/cache" + toolswatch "k8s.io/client-go/tools/watch" +) + +const ( + deploymentDependenciesConfigMapName = "map1" + deploymentDependenciesSecretName = "secret1" +) + +func TestConfigMapAndSecretToDeploymentToBundleIndex(t *testing.T) { + t.Parallel() + + bundle := constructDeploymentDependenciesBundle() + SetupApp(t, bundle, false, true, assertConfigMapAndSecretToDeploymentToBundleIndex) +} + +func assertConfigMapAndSecretToDeploymentToBundleIndex(ctx context.Context, t *testing.T, cfg *Config, args ...interface{}) { + // initial create of ConfigMap and Secret + cm, err := cfg.MainClient.CoreV1().ConfigMaps(cfg.Namespace).Create(deploymentDependenciesConfigMap()) + require.NoError(cfg.T, err) + secret, err := cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Create(deploymentDependenciesSecret()) + require.NoError(cfg.T, err) + + // Wait for stable state + deploymentName := cfg.Bundle.Spec.Resources[0].Spec.Object.(meta_v1.Object).GetName() + lw := cache.NewListWatchFromClient(cfg.MainClient.AppsV1().RESTClient(), "deployments", cfg.Namespace, fields.Everything()) + cond := IsPodSpecAnnotationCond(t, cfg.Namespace, deploymentName, builtin.EnvRefHashAnnotation, "97b5461821487e047e760880764dd9b31a5b7f39e8961b98b809c6d3f5b3cf3a") + _, err = toolswatch.UntilWithSync(ctx, lw, &apps_v1.Deployment{}, nil, cond) + require.NoError(cfg.T, err) + + // Update ConfigMap + cm.Data["b"] = "a" + _, err = cfg.MainClient.CoreV1().ConfigMaps(cfg.Namespace).Update(cm) + require.NoError(cfg.T, err) + + // Wait for updated annotation + cond = IsPodSpecAnnotationCond(t, cfg.Namespace, deploymentName, builtin.EnvRefHashAnnotation, "5fb60505d125dd42160a9ef237873345020a90e8d607861ced50a641c3a800a9") + _, err = toolswatch.UntilWithSync(ctx, lw, &apps_v1.Deployment{}, nil, cond) + require.NoError(cfg.T, err) + + // Update Secret + secret.StringData = map[string]string{ + "z": "c", + } + _, err = cfg.MainClient.CoreV1().Secrets(cfg.Namespace).Update(secret) + require.NoError(cfg.T, err) + + // Wait for updated annotation + cond = IsPodSpecAnnotationCond(t, cfg.Namespace, deploymentName, builtin.EnvRefHashAnnotation, "20ed86a5106aab7e892f498f38dc1b713405d864878e6a074377fbdcba129599") + _, err = toolswatch.UntilWithSync(ctx, lw, &apps_v1.Deployment{}, nil, cond) + require.NoError(cfg.T, err) +} + +func constructDeploymentDependenciesBundle() *smith_v1.Bundle { + var ( + replicas int32 = 1 + minReadySeconds int32 = 1 + revisionHistoryLimit int32 = 10 + progressDeadlineSeconds int32 = 5 + terminationGracePeriodSeconds int64 = 30 + labelMap = map[string]string{ + "name": string(deploymentResourceName), + } + ) + return &smith_v1.Bundle{ + TypeMeta: meta_v1.TypeMeta{ + Kind: smith_v1.BundleResourceKind, + APIVersion: smith_v1.BundleResourceGroupVersion, + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: "bundle-dt", + }, + Spec: smith_v1.BundleSpec{ + Resources: []smith_v1.Resource{ + { + Name: deploymentResourceName, + Spec: smith_v1.ResourceSpec{ + Object: &apps_v1.Deployment{ + TypeMeta: meta_v1.TypeMeta{ + Kind: "Deployment", + APIVersion: apps_v1.SchemeGroupVersion.String(), + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: string(deploymentResourceName), + }, + Spec: apps_v1.DeploymentSpec{ + Selector: &meta_v1.LabelSelector{ + MatchLabels: labelMap, + }, + Replicas: &replicas, + Template: core_v1.PodTemplateSpec{ + ObjectMeta: meta_v1.ObjectMeta{ + Labels: labelMap, + }, + Spec: core_v1.PodSpec{ + Containers: []core_v1.Container{ + { + Name: "container1", + Image: "roaanv/k8sti", + EnvFrom: []core_v1.EnvFromSource{ + { + ConfigMapRef: &core_v1.ConfigMapEnvSource{ + LocalObjectReference: core_v1.LocalObjectReference{ + Name: deploymentDependenciesConfigMapName, + }, + }, + }, + { + SecretRef: &core_v1.SecretEnvSource{ + LocalObjectReference: core_v1.LocalObjectReference{ + Name: deploymentDependenciesSecretName, + }, + }, + }, + }, + ImagePullPolicy: core_v1.PullAlways, + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: core_v1.TerminationMessageReadFile, + }, + }, + DNSPolicy: core_v1.DNSClusterFirst, + RestartPolicy: core_v1.RestartPolicyAlways, + SchedulerName: "default-scheduler", + SecurityContext: &core_v1.PodSecurityContext{}, + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + }, + }, + MinReadySeconds: minReadySeconds, + ProgressDeadlineSeconds: &progressDeadlineSeconds, + RevisionHistoryLimit: &revisionHistoryLimit, + + Strategy: apps_v1.DeploymentStrategy{ + Type: apps_v1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &apps_v1.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "25%", + }, + MaxSurge: &intstr.IntOrString{ + Type: intstr.String, + StrVal: "25%", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func deploymentDependenciesConfigMap() *core_v1.ConfigMap { + return &core_v1.ConfigMap{ + TypeMeta: meta_v1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: core_v1.SchemeGroupVersion.String(), + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: deploymentDependenciesConfigMapName, + }, + Data: map[string]string{ + "a": "b", + }, + } +} + +func deploymentDependenciesSecret() *core_v1.Secret { + return &core_v1.Secret{ + TypeMeta: meta_v1.TypeMeta{ + Kind: "Secret", + APIVersion: core_v1.SchemeGroupVersion.String(), + }, + ObjectMeta: meta_v1.ObjectMeta{ + Name: deploymentDependenciesSecretName, + }, + StringData: map[string]string{ + "x": "b", + }, + } +} diff --git a/it/utils_for_tests.go b/it/utils_for_tests.go index 201f5f6..4dd3685 100644 --- a/it/utils_for_tests.go +++ b/it/utils_for_tests.go @@ -28,6 +28,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" + apps_v1 "k8s.io/api/apps/v1" core_v1 "k8s.io/api/core/v1" apiExtClientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apiext_v1b1inf "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1beta1" @@ -36,6 +37,7 @@ import ( meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -45,6 +47,14 @@ import ( toolswatch "k8s.io/client-go/tools/watch" ) +var ( + appsV1Scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(apps_v1.SchemeBuilder.AddToScheme(appsV1Scheme)) +} + type TestFunc func(context.Context, *testing.T, *Config, ...interface{}) type Config struct { @@ -161,6 +171,31 @@ func IsBundleObservedGenerationCond(namespace, name string) toolswatch.Condition } } +func IsPodSpecAnnotationCond(t *testing.T, namespace, name, annotation, value string) toolswatch.ConditionFunc { + return func(event watch.Event) (bool, error) { + metaObj := event.Object.(meta_v1.Object) + if metaObj.GetNamespace() != namespace || metaObj.GetName() != name { + return false, nil + } + deployment := &apps_v1.Deployment{} + err := util.ConvertType(appsV1Scheme, event.Object, deployment) + if err != nil { + return false, err + } + annotations := deployment.Spec.Template.Annotations + actual, ok := annotations[annotation] + if !ok { + t.Logf("Pod Spec annotation %s is not set", annotation) + return false, nil + } + if actual != value { + t.Logf("Ignoring Pod Spec annotation %s value %s. Expecting %s", annotation, actual, value) + return false, nil + } + return true, nil + } +} + func TestSetup(t *testing.T) (*rest.Config, *kubernetes.Clientset, *smithClientset.Clientset) { config, err := options.LoadRestClientConfig("voyager-test", options.RestClientOptions{ APIQPS: 10, diff --git a/pkg/controller/bundlec/BUILD.bazel b/pkg/controller/bundlec/BUILD.bazel index 8378f84..f1494ca 100644 --- a/pkg/controller/bundlec/BUILD.bazel +++ b/pkg/controller/bundlec/BUILD.bazel @@ -34,6 +34,7 @@ go_library( "//vendor/github.com/pkg/errors:go_default_library", "//vendor/github.com/prometheus/client_golang/prometheus:go_default_library", "//vendor/go.uber.org/zap:go_default_library", + "//vendor/k8s.io/api/apps/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", diff --git a/pkg/controller/bundlec/controller.go b/pkg/controller/bundlec/controller.go index ac1fac1..7a18f5f 100644 --- a/pkg/controller/bundlec/controller.go +++ b/pkg/controller/bundlec/controller.go @@ -13,8 +13,11 @@ import ( "github.com/atlassian/smith/pkg/plugin" "github.com/atlassian/smith/pkg/statuschecker" "github.com/atlassian/smith/pkg/store" + "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "go.uber.org/zap" + apps_v1 "k8s.io/api/apps/v1" + core_v1 "k8s.io/api/core/v1" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -24,6 +27,14 @@ import ( "k8s.io/client-go/tools/record" ) +const ( + byConfigMapNamespaceNameIndexName = "ByConfigMap" + bySecretNamespaceNameIndexName = "BySecret" +) + +type byIndexFunc func(indexName, indexKey string) ([]interface{}, error) +type indexKeyFunc func(namespace, name string) string + type Controller struct { // wg.Wait() is called from Run() and first wg.Add() may be called concurrently from CRD listener // to start an Informer. This is a data race. This mutex is used to ensure ordering. @@ -66,23 +77,50 @@ type Controller struct { } // Prepare prepares the controller to be run. -func (c *Controller) Prepare(crdInf cache.SharedIndexInformer, resourceInfs map[schema.GroupVersionKind]cache.SharedIndexInformer) { +func (c *Controller) Prepare(crdInf cache.SharedIndexInformer, resourceInfs map[schema.GroupVersionKind]cache.SharedIndexInformer) error { c.crdContext, c.crdContextCancel = context.WithCancel(context.Background()) crdInf.AddEventHandler(&crdEventHandler{ controller: c, watchers: make(map[string]watchState), }) - + deploymentInf := resourceInfs[apps_v1.SchemeGroupVersion.WithKind("Deployment")] + err := deploymentInf.AddIndexers(cache.Indexers{ + byConfigMapNamespaceNameIndexName: byConfigMapNamespaceNameIndex, + bySecretNamespaceNameIndexName: bySecretNamespaceNameIndex, + }) + if err != nil { + return errors.WithStack(err) + } + deploymentByIndex := deploymentInf.GetIndexer().ByIndex + // ConfigMap -> Deployment -> Bundle event propagation + configMapGVK := core_v1.SchemeGroupVersion.WithKind("ConfigMap") + configMapInf := resourceInfs[configMapGVK] + configMapInf.AddEventHandler(&handlers.LookupHandler{ + Logger: c.Logger, + WorkQueue: c.WorkQueue, + Gvk: configMapGVK, + Lookup: c.lookupBundleByDeploymentByIndex(deploymentByIndex, byConfigMapNamespaceNameIndexName, byConfigMapNamespaceNameIndexKey), + }) + // Secret -> Deployment -> Bundle event propagation + secretGVK := core_v1.SchemeGroupVersion.WithKind("Secret") + secretInf := resourceInfs[secretGVK] + secretInf.AddEventHandler(&handlers.LookupHandler{ + Logger: c.Logger, + WorkQueue: c.WorkQueue, + Gvk: secretGVK, + Lookup: c.lookupBundleByDeploymentByIndex(deploymentByIndex, bySecretNamespaceNameIndexName, bySecretNamespaceNameIndexKey), + }) + // Standard handler for gvk, resourceInf := range resourceInfs { - resourceHandler := &handlers.ControlledResourceHandler{ + resourceInf.AddEventHandler(&handlers.ControlledResourceHandler{ Logger: c.Logger, WorkQueue: c.WorkQueue, ControllerIndex: &controllerIndexAdapter{bundleStore: c.BundleStore}, ControllerGvk: smith_v1.BundleGVK, Gvk: gvk, - } - resourceInf.AddEventHandler(resourceHandler) + }) } + return nil } // Run begins watching and syncing. @@ -110,6 +148,37 @@ func (c *Controller) Run(ctx context.Context) { <-ctx.Done() } +// lookupBundleByDeploymentByIndex returns a function that can be used to perform lookups of Bundles that contain +// Deployment objects that reference ConfigMap/Secret objects. +func (c *Controller) lookupBundleByDeploymentByIndex(byIndex byIndexFunc, indexName string, indexKey indexKeyFunc) func(runtime.Object) ([]runtime.Object, error) { + deploymentGK := schema.GroupKind{ + Group: apps_v1.GroupName, + Kind: "Deployment", + } + return func(obj runtime.Object) ([]runtime.Object /*bundles*/, error) { + // obj is ConfigMap or Secret + objMeta := obj.(meta_v1.Object) + // find all Deployments that reference this obj + deploymentsFromIndex, err := byIndex(indexName, indexKey(objMeta.GetNamespace(), objMeta.GetName())) + if err != nil { + return nil, err + } + var bundles []runtime.Object + for _, deploymentInterface := range deploymentsFromIndex { + deployment := deploymentInterface.(*apps_v1.Deployment) + // find all Bundles that reference this Deployment + bundlesForDeployment, err := c.BundleStore.GetBundlesByObject(deploymentGK, deployment.Namespace, deployment.Name) + if err != nil { + return nil, err + } + for _, bundle := range bundlesForDeployment { + bundles = append(bundles, bundle) + } + } + return bundles, nil + } +} + type controllerIndexAdapter struct { bundleStore BundleStore } @@ -125,3 +194,77 @@ func (c *controllerIndexAdapter) ControllerByObject(gk schema.GroupKind, namespa } return objs, nil } + +func byConfigMapNamespaceNameIndex(obj interface{}) ([]string, error) { + d := obj.(*apps_v1.Deployment) + index := configMapNamespaceNameIndexKeysForContainers(d.Namespace, d.Spec.Template.Spec.Containers) + index = append(index, configMapNamespaceNameIndexKeysForContainers(d.Namespace, d.Spec.Template.Spec.InitContainers)...) + return index, nil +} + +func configMapNamespaceNameIndexKeysForContainers(namespace string, containers []core_v1.Container) []string { + var indexKeys []string + for _, container := range containers { + for _, envFrom := range container.EnvFrom { + configMapRef := envFrom.ConfigMapRef + if configMapRef == nil { + continue + } + indexKeys = append(indexKeys, byConfigMapNamespaceNameIndexKey(namespace, configMapRef.Name)) + } + for _, env := range container.Env { + valueFrom := env.ValueFrom + if valueFrom == nil { + continue + } + + configMapKeyRef := valueFrom.ConfigMapKeyRef + if configMapKeyRef == nil { + continue + } + indexKeys = append(indexKeys, byConfigMapNamespaceNameIndexKey(namespace, configMapKeyRef.Name)) + } + } + return indexKeys +} + +func byConfigMapNamespaceNameIndexKey(configMapNamespace, configMapName string) string { + return configMapNamespace + "/" + configMapName +} + +func bySecretNamespaceNameIndex(obj interface{}) ([]string, error) { + d := obj.(*apps_v1.Deployment) + index := secretNamespaceNameIndexKeysForContainers(d.Namespace, d.Spec.Template.Spec.Containers) + index = append(index, secretNamespaceNameIndexKeysForContainers(d.Namespace, d.Spec.Template.Spec.InitContainers)...) + return index, nil +} + +func secretNamespaceNameIndexKeysForContainers(namespace string, containers []core_v1.Container) []string { + var indexKeys []string + for _, container := range containers { + for _, envFrom := range container.EnvFrom { + secretRef := envFrom.SecretRef + if secretRef == nil { + continue + } + indexKeys = append(indexKeys, bySecretNamespaceNameIndexKey(namespace, secretRef.Name)) + } + for _, env := range container.Env { + valueFrom := env.ValueFrom + if valueFrom == nil { + continue + } + + secretKeyRef := valueFrom.SecretKeyRef + if secretKeyRef == nil { + continue + } + indexKeys = append(indexKeys, bySecretNamespaceNameIndexKey(namespace, secretKeyRef.Name)) + } + } + return indexKeys +} + +func bySecretNamespaceNameIndexKey(secretNamespace, secretName string) string { + return secretNamespace + "/" + secretName +} diff --git a/pkg/specchecker/builtin/process_deployment.go b/pkg/specchecker/builtin/process_deployment.go index 2c3bfe2..2b7101c 100644 --- a/pkg/specchecker/builtin/process_deployment.go +++ b/pkg/specchecker/builtin/process_deployment.go @@ -171,11 +171,12 @@ func (deployment) generateHashForContainers(store specchecker.Store, namespace s } } for _, env := range container.Env { - if env.ValueFrom == nil { + valueFrom := env.ValueFrom + if valueFrom == nil { continue } - secretKeyRef := env.ValueFrom.SecretKeyRef + secretKeyRef := valueFrom.SecretKeyRef if secretKeyRef != nil { err := specchecker.HashSecretRef(store, namespace, secretKeyRef.Name, sets.NewString(secretKeyRef.Key), secretKeyRef.Optional, hasher) if err != nil { @@ -183,7 +184,7 @@ func (deployment) generateHashForContainers(store specchecker.Store, namespace s } } - configMapKeyRef := env.ValueFrom.ConfigMapKeyRef + configMapKeyRef := valueFrom.ConfigMapKeyRef if configMapKeyRef != nil { err := specchecker.HashConfigMapRef(store, namespace, configMapKeyRef.Name, sets.NewString(configMapKeyRef.Key), configMapKeyRef.Optional, hasher) if err != nil {