diff --git a/pkg/controller/expcertificates/BUILD.bazel b/pkg/controller/expcertificates/BUILD.bazel index facdb7f82..704b9e1f7 100644 --- a/pkg/controller/expcertificates/BUILD.bazel +++ b/pkg/controller/expcertificates/BUILD.bazel @@ -2,19 +2,25 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["util.go"], + srcs = [ + "informers.go", + "listers.go", + "util.go", + ], importpath = "github.com/jetstack/cert-manager/pkg/controller/expcertificates", visibility = ["//visibility:public"], deps = [ "//pkg/apis/certmanager/v1alpha2:go_default_library", "//pkg/client/listers/certmanager/v1alpha2:go_default_library", "//pkg/controller:go_default_library", + "//pkg/controller/expcertificates/internal/predicate:go_default_library", "//pkg/util:go_default_library", "//pkg/util/pki:go_default_library", "@com_github_go_logr_logr//:go_default_library", "@io_k8s_api//core/v1:go_default_library", "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", "@io_k8s_apimachinery//pkg/labels:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", "@io_k8s_client_go//util/workqueue:go_default_library", ], ) @@ -30,6 +36,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//pkg/controller/expcertificates/internal/predicate:all-srcs", "//pkg/controller/expcertificates/issuing:all-srcs", "//pkg/controller/expcertificates/trigger:all-srcs", ], diff --git a/pkg/controller/expcertificates/informers.go b/pkg/controller/expcertificates/informers.go new file mode 100644 index 000000000..efea1da3e --- /dev/null +++ b/pkg/controller/expcertificates/informers.go @@ -0,0 +1,68 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 certificates + +import ( + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/util/workqueue" + + cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2" + controllerpkg "github.com/jetstack/cert-manager/pkg/controller" + "github.com/jetstack/cert-manager/pkg/controller/expcertificates/internal/predicate" +) + +// EnqueueCertificatesForResourceUsingPredicates will return a function +// that can be used as an OnAdd handler for a SharedIndexInformer. +// It should be used as a handler for resources that are referenced +// in some way by Certificates. +// The namespace of the object being processed will be used in the List +// call when enqueuing Certificate resources. +// If no predicate constructors are given, all Certificate resources will be +// enqueued on every invocation. +func EnqueueCertificatesForResourceUsingPredicates(log logr.Logger, queue workqueue.Interface, lister cmlisters.CertificateLister, selector labels.Selector, predicateBuilders ...predicate.ExtractorFunc) func(obj interface{}) { + return func(obj interface{}) { + s, ok := obj.(metav1.Object) + if !ok { + log.Info("Non-Object type resource passed to EnqueueCertificatesForSecretUsingPredicates") + return + } + + // 'Construct' the predicate functions using the given Secret + predicates := make(predicate.Funcs, len(predicateBuilders)) + for i, b := range predicateBuilders { + predicates[i] = b(s.(runtime.Object)) + } + + certs, err := ListCertificatesMatchingPredicates(lister.Certificates(s.GetNamespace()), selector, predicates...) + if err != nil { + log.Error(err, "Failed listing Certificate resources") + return + } + + for _, cert := range certs { + key, err := controllerpkg.KeyFunc(cert) + if err != nil { + log.Error(err, "Error determining 'key' for resource") + continue + } + queue.Add(key) + } + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/BUILD.bazel b/pkg/controller/expcertificates/internal/predicate/BUILD.bazel new file mode 100644 index 000000000..aaa56e89a --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/BUILD.bazel @@ -0,0 +1,51 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "certificate.go", + "certificaterequest.go", + "generic.go", + "predicate.go", + ], + importpath = "github.com/jetstack/cert-manager/pkg/controller/expcertificates/internal/predicate", + visibility = ["//pkg/controller/expcertificates:__subpackages__"], + deps = [ + "//pkg/apis/certmanager/v1alpha2:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = [ + "certificate_test.go", + "certificaterequest_test.go", + "generic_test.go", + "predicate_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//pkg/apis/certmanager/v1alpha2:go_default_library", + "@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library", + "@io_k8s_apimachinery//pkg/runtime:go_default_library", + "@io_k8s_apimachinery//pkg/runtime/schema:go_default_library", + "@io_k8s_apimachinery//pkg/types:go_default_library", + "@io_k8s_utils//pointer:go_default_library", + ], +) diff --git a/pkg/controller/expcertificates/internal/predicate/certificate.go b/pkg/controller/expcertificates/internal/predicate/certificate.go new file mode 100644 index 000000000..f7afe4e13 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/certificate.go @@ -0,0 +1,46 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "k8s.io/apimachinery/pkg/runtime" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +// CertificateSecretName returns a predicate that used to filter Certificates +// to only those with the given 'spec.secretName'. +func CertificateSecretName(name string) Func { + return func(obj runtime.Object) bool { + crt := obj.(*cmapi.Certificate) + return crt.Spec.SecretName == name + } +} + +// CertificateSecretName returns a predicate that used to filter Certificates +// to only those with the given 'status.nextPrivateKeySecretName'. +// It is not possible to select Certificates with a 'nil' secret name using +// this predicate function. +func CertificateNextPrivateKeySecretName(name string) Func { + return func(obj runtime.Object) bool { + crt := obj.(*cmapi.Certificate) + if crt.Status.NextPrivateKeySecretName == nil { + return false + } + return *crt.Status.NextPrivateKeySecretName == name + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/certificate_test.go b/pkg/controller/expcertificates/internal/predicate/certificate_test.go new file mode 100644 index 000000000..4c7d09ea1 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/certificate_test.go @@ -0,0 +1,94 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "testing" + + "k8s.io/utils/pointer" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +func TestCertificateSecretName(t *testing.T) { + certWithSecretName := func(s string) *cmapi.Certificate { + return &cmapi.Certificate{ + Spec: cmapi.CertificateSpec{SecretName: s}, + } + } + tests := map[string]struct { + secretName string + cert *cmapi.Certificate + expected bool + }{ + "returns true if secret name matches": { + secretName: "abc", + cert: certWithSecretName("abc"), + expected: true, + }, + "returns false if secret name does not match": { + secretName: "abc", + cert: certWithSecretName("abcd"), + expected: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := CertificateSecretName(test.secretName)(test.cert) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} + +func TestCertificateNextPrivateKeySecretName(t *testing.T) { + certWithSecretName := func(s *string) *cmapi.Certificate { + return &cmapi.Certificate{ + Status: cmapi.CertificateStatus{NextPrivateKeySecretName: s}, + } + } + tests := map[string]struct { + secretName string + cert *cmapi.Certificate + expected bool + }{ + "returns true if secret name matches": { + secretName: "abc", + cert: certWithSecretName(pointer.StringPtr("abc")), + expected: true, + }, + "returns false if secret name does not match": { + secretName: "abc", + cert: certWithSecretName(pointer.StringPtr("abcd")), + expected: false, + }, + "returns false if secret name is nil": { + secretName: "", + cert: certWithSecretName(nil), + expected: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := CertificateNextPrivateKeySecretName(test.secretName)(test.cert) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/certificaterequest.go b/pkg/controller/expcertificates/internal/predicate/certificaterequest.go new file mode 100644 index 000000000..88f92f4bd --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/certificaterequest.go @@ -0,0 +1,37 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +// CertificateRequestRevision returns a predicate that used to filter +// CertificateRequest to only those with a given 'revision' number. +func CertificateRequestRevision(revision int) Func { + return func(obj runtime.Object) bool { + req := obj.(*cmapi.CertificateRequest) + if req.Annotations == nil { + return false + } + return req.Annotations[cmapi.CertificateRequestRevisionAnnotationKey] == fmt.Sprintf("%d", revision) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/certificaterequest_test.go b/pkg/controller/expcertificates/internal/predicate/certificaterequest_test.go new file mode 100644 index 000000000..d8017da94 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/certificaterequest_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "fmt" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +func TestCertificateRequestRevision(t *testing.T) { + requestWithRevision := func(s int) *cmapi.CertificateRequest { + return &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + cmapi.CertificateRequestRevisionAnnotationKey: fmt.Sprintf("%d", s), + }, + }, + } + } + tests := map[string]struct { + revision int + request *cmapi.CertificateRequest + expected bool + }{ + "returns true if revision matches": { + revision: 30, + request: requestWithRevision(30), + expected: true, + }, + "returns false if revision does not match": { + revision: 29, + request: requestWithRevision(30), + expected: false, + }, + "returns false if revision is not set": { + revision: 0, + request: &cmapi.CertificateRequest{}, + expected: false, + }, + "returns false if revision is empty": { + revision: 0, + request: &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + cmapi.CertificateRequestRevisionAnnotationKey: "", + }, + }, + }, + expected: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := CertificateRequestRevision(test.revision)(test.request) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/generic.go b/pkg/controller/expcertificates/internal/predicate/generic.go new file mode 100644 index 000000000..3f3022353 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/generic.go @@ -0,0 +1,38 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ResourceOwnedBy will filter returned results to only those with the +// given resource as an owner. +func ResourceOwnedBy(owner runtime.Object) Func { + return func(obj runtime.Object) bool { + return metav1.IsControlledBy(obj.(metav1.Object), owner.(metav1.Object)) + } +} + +// ResourceOwnerOf will filter returned results to only those that own the given +// resource. +func ResourceOwnerOf(obj runtime.Object) Func { + return func(ownerObj runtime.Object) bool { + return metav1.IsControlledBy(obj.(metav1.Object), ownerObj.(metav1.Object)) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/generic_test.go b/pkg/controller/expcertificates/internal/predicate/generic_test.go new file mode 100644 index 000000000..c3975d4b8 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/generic_test.go @@ -0,0 +1,104 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +func TestResourceOwnedBy(t *testing.T) { + baseGVK := cmapi.SchemeGroupVersion.WithKind("CertificateRequest") + request := func(name string) *cmapi.CertificateRequest { + return &cmapi.CertificateRequest{ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(name)}} + } + requestWithOwner := func(owner metav1.Object, gvk schema.GroupVersionKind) *cmapi.CertificateRequest { + return &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(owner, gvk)}, + }, + } + } + tests := map[string]struct { + owner runtime.Object + obj runtime.Object + expected bool + }{ + "returns true if resource does own the resource": { + owner: request("base"), + obj: requestWithOwner(request("base"), baseGVK), + expected: true, + }, + "returns false if resource does not own the resource": { + owner: request("base"), + obj: requestWithOwner(request("notbase"), baseGVK), + expected: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := ResourceOwnedBy(test.owner)(test.obj) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} + +func TestResourceOwnerOf(t *testing.T) { + baseGVK := cmapi.SchemeGroupVersion.WithKind("CertificateRequest") + request := func(name string) *cmapi.CertificateRequest { + return &cmapi.CertificateRequest{ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(name)}} + } + requestWithOwner := func(owner metav1.Object, gvk schema.GroupVersionKind) *cmapi.CertificateRequest { + return &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(owner, gvk)}, + }, + } + } + tests := map[string]struct { + ownee runtime.Object + obj runtime.Object + expected bool + }{ + "returns true if resource is owned by object": { + ownee: requestWithOwner(request("base"), baseGVK), + obj: request("base"), + expected: true, + }, + "returns false if resource is not owned by object": { + ownee: requestWithOwner(request("notbase"), baseGVK), + obj: request("base"), + expected: false, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := ResourceOwnerOf(test.ownee)(test.obj) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/predicate.go b/pkg/controller/expcertificates/internal/predicate/predicate.go new file mode 100644 index 000000000..9ea915c6c --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/predicate.go @@ -0,0 +1,58 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// Func is a generic function used to filter various types of resources. +type Func func(obj runtime.Object) bool + +// Funcs is a list of predicates to be AND'd together. +type Funcs []Func + +// Evaluate will evaluate all the predicate functions in order, AND'ing +// together the results. +func (f Funcs) Evaluate(obj runtime.Object) bool { + for _, fn := range f { + if !fn(obj) { + return false + } + } + return true +} + +// An ExtractorFunc applies a transformation to a runtime.Object and creates a +// predicate function based on the result of the transformation. +// This can be used to apply complex lookup logic to determine which resources +// should be enqueued if another resource being watched changes, for example, +// enqueuing all Certificate resources that own a CertificateRequest that has +// been observed, or enqueuing all Certificate resources that specify +// `status.nextPrivateKeySecretName` as the name of the Secret being processed. +type ExtractorFunc func(obj runtime.Object) Func + +// ExtractResourceName is a helper function used to extract a name from a +// metav1.Object being enqueued to construct a Func that is variadic +// based on a string value. +func ExtractResourceName(p func(s string) Func) ExtractorFunc { + return func(obj runtime.Object) Func { + metaObj := obj.(metav1.Object) + return p(metaObj.GetName()) + } +} diff --git a/pkg/controller/expcertificates/internal/predicate/predicate_test.go b/pkg/controller/expcertificates/internal/predicate/predicate_test.go new file mode 100644 index 000000000..bb4221695 --- /dev/null +++ b/pkg/controller/expcertificates/internal/predicate/predicate_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 predicate + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" +) + +func TestFuncs_Evaluate(t *testing.T) { + falseFunc := func(_ runtime.Object) bool { + return false + } + trueFunc := func(_ runtime.Object) bool { + return true + } + tests := map[string]struct { + funcs Funcs + expected bool + }{ + "returns false if one returns false": { + funcs: Funcs{falseFunc}, + expected: false, + }, + "returns false if at least one returns false": { + funcs: Funcs{falseFunc, trueFunc}, + expected: false, + }, + "returns false if at least one returns false (reversed)": { + funcs: Funcs{trueFunc, falseFunc}, + expected: false, + }, + "returns true if all return true": { + funcs: Funcs{trueFunc, trueFunc}, + expected: true, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := test.funcs.Evaluate(nil) + if got != test.expected { + t.Errorf("unexpected response: got=%t, exp=%t", got, test.expected) + } + }) + } +} + +func TestExtractResourceName(t *testing.T) { + expectedValue := "expected-value" + called := false + + fn := ExtractResourceName(func(s string) Func { + called = true + if s != expectedValue { + t.Errorf("function called with unexpected value: got=%s, exp=%s", s, expectedValue) + } + return nil + }) + + obj := &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{Name: expectedValue}} + fn(obj) + if !called { + t.Fatal("unexpected error - function not called!") + } +} diff --git a/pkg/controller/expcertificates/issuing/BUILD.bazel b/pkg/controller/expcertificates/issuing/BUILD.bazel index ededaf695..c001e2889 100644 --- a/pkg/controller/expcertificates/issuing/BUILD.bazel +++ b/pkg/controller/expcertificates/issuing/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//pkg/client/listers/certmanager/v1alpha2:go_default_library", "//pkg/controller:go_default_library", "//pkg/controller/expcertificates:go_default_library", + "//pkg/controller/expcertificates/internal/predicate:go_default_library", "//pkg/logs:go_default_library", "//pkg/util/kube:go_default_library", "//pkg/util/pki:go_default_library", diff --git a/pkg/controller/expcertificates/issuing/issuing_controller.go b/pkg/controller/expcertificates/issuing/issuing_controller.go index d52faf4ed..a3d50ef41 100644 --- a/pkg/controller/expcertificates/issuing/issuing_controller.go +++ b/pkg/controller/expcertificates/issuing/issuing_controller.go @@ -42,6 +42,7 @@ import ( cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2" controllerpkg "github.com/jetstack/cert-manager/pkg/controller" certificates "github.com/jetstack/cert-manager/pkg/controller/expcertificates" + "github.com/jetstack/cert-manager/pkg/controller/expcertificates/internal/predicate" logf "github.com/jetstack/cert-manager/pkg/logs" utilkube "github.com/jetstack/cert-manager/pkg/util/kube" utilpki "github.com/jetstack/cert-manager/pkg/util/pki" @@ -94,12 +95,12 @@ func NewController( certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ - WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(log, queue, certificateGvk, certificates.CertificateGetFunc(certificateInformer.Lister())), + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), predicate.ResourceOwnerOf), }) secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ // Issuer reconciles on changes to the Secret named `spec.nextPrivateKeySecretName` - WorkFunc: certificates.EnqueueCertificatesForSecretNameFunc(log, certificateInformer.Lister(), labels.Everything(), - certificates.WithNextPrivateKeySecretNamePredicateFunc, queue), + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateNextPrivateKeySecretName)), }) // build a list of InformerSynced functions that will be returned by the Register method. @@ -174,8 +175,8 @@ func (c *controller) ProcessItem(ctx context.Context, key string) error { reqs, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), - certificates.WithCertificateRevisionPredicateFunc(nextRevision), - certificates.WithCertificateRequestOwnerPredicateFunc(crt), + predicate.CertificateRequestRevision(nextRevision), + predicate.ResourceOwnedBy(crt), ) if err != nil || len(reqs) != 1 { // If error return. @@ -238,7 +239,6 @@ func (c *controller) failIssueCertificate(ctx context.Context, log logr.Logger, // certificate, and then store the certificate, CA and private key into the // Secret in the appropriate format type. func (c *controller) issueCertificate(ctx context.Context, log logr.Logger, nextRevision int, crt *cmapi.Certificate, req *cmapi.CertificateRequest) error { - csr, err := utilpki.DecodeX509CertificateRequestBytes(req.Spec.CSRPEM) if err != nil { return err diff --git a/pkg/controller/expcertificates/listers.go b/pkg/controller/expcertificates/listers.go new file mode 100644 index 000000000..636989bf1 --- /dev/null +++ b/pkg/controller/expcertificates/listers.go @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Jetstack cert-manager contributors. + +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 certificates + +import ( + "k8s.io/apimachinery/pkg/labels" + + cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" + cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2" + "github.com/jetstack/cert-manager/pkg/controller/expcertificates/internal/predicate" +) + +// ListCertificateRequestsMatchingPredicates will list CertificateRequest +// resources using the provided lister, optionally applying the given predicate +// functions to filter the CertificateRequest resources returned. +func ListCertificateRequestsMatchingPredicates(lister cmlisters.CertificateRequestNamespaceLister, selector labels.Selector, predicates ...predicate.Func) ([]*cmapi.CertificateRequest, error) { + reqs, err := lister.List(selector) + if err != nil { + return nil, err + } + funcs := predicate.Funcs(predicates) + out := make([]*cmapi.CertificateRequest, 0) + for _, req := range reqs { + if funcs.Evaluate(req) { + out = append(out, req) + } + } + + return out, nil +} + +// ListCertificatesMatchingPredicates will list Certificate resources using +// the provided lister, optionally applying the given predicate functions to +// filter the Certificate resources returned. +func ListCertificatesMatchingPredicates(lister cmlisters.CertificateNamespaceLister, selector labels.Selector, predicates ...predicate.Func) ([]*cmapi.Certificate, error) { + reqs, err := lister.List(selector) + if err != nil { + return nil, err + } + funcs := predicate.Funcs(predicates) + out := make([]*cmapi.Certificate, 0) + for _, req := range reqs { + if funcs.Evaluate(req) { + out = append(out, req) + } + } + + return out, nil +} diff --git a/pkg/controller/expcertificates/trigger/BUILD.bazel b/pkg/controller/expcertificates/trigger/BUILD.bazel index 8b0983a1d..a44084e0d 100644 --- a/pkg/controller/expcertificates/trigger/BUILD.bazel +++ b/pkg/controller/expcertificates/trigger/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "//pkg/client/listers/certmanager/v1alpha2:go_default_library", "//pkg/controller:go_default_library", "//pkg/controller/expcertificates:go_default_library", + "//pkg/controller/expcertificates/internal/predicate:go_default_library", "//pkg/logs:go_default_library", "//pkg/util/pki:go_default_library", "@com_github_go_logr_logr//:go_default_library", diff --git a/pkg/controller/expcertificates/trigger/trigger_controller.go b/pkg/controller/expcertificates/trigger/trigger_controller.go index b8891f798..57a94fb63 100644 --- a/pkg/controller/expcertificates/trigger/trigger_controller.go +++ b/pkg/controller/expcertificates/trigger/trigger_controller.go @@ -41,6 +41,7 @@ import ( cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2" controllerpkg "github.com/jetstack/cert-manager/pkg/controller" certificates "github.com/jetstack/cert-manager/pkg/controller/expcertificates" + "github.com/jetstack/cert-manager/pkg/controller/expcertificates/internal/predicate" logf "github.com/jetstack/cert-manager/pkg/logs" ) @@ -86,13 +87,16 @@ func NewController( secretsInformer := factory.Core().V1().Secrets() certificateInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: queue}) + + // When a CertificateRequest resource changes, enqueue the Certificate resource that owns it. certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ - WorkFunc: controllerpkg.HandleOwnedResourceNamespacedFunc(log, queue, certificateGvk, certificates.CertificateGetFunc(certificateInformer.Lister())), + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), predicate.ResourceOwnerOf), }) + // When a Secret resource changes, enqueue any Certificate resources that name it as spec.secretName. secretsInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{ // Trigger reconciles on changes to the Secret named `spec.secretName` - WorkFunc: certificates.EnqueueCertificatesForSecretNameFunc(log, certificateInformer.Lister(), labels.Everything(), - certificates.WithSecretNamePredicateFunc, queue), + WorkFunc: certificates.EnqueueCertificatesForResourceUsingPredicates(log, queue, certificateInformer.Lister(), labels.Everything(), + predicate.ExtractResourceName(predicate.CertificateSecretName)), }) // build a list of InformerSynced functions that will be returned by the Register method. @@ -184,8 +188,8 @@ func (c *controller) buildPolicyInputForCertificate(ctx context.Context, crt *cm if crt.Status.Revision != nil { reqs, err := certificates.ListCertificateRequestsMatchingPredicates(c.certificateRequestLister.CertificateRequests(crt.Namespace), labels.Everything(), - certificates.WithCertificateRequestOwnerPredicateFunc(crt), - certificates.WithCertificateRevisionPredicateFunc(*crt.Status.Revision), + predicate.ResourceOwnedBy(crt), + predicate.CertificateRequestRevision(*crt.Status.Revision), ) if err != nil { return PolicyData{}, err diff --git a/pkg/controller/expcertificates/util.go b/pkg/controller/expcertificates/util.go index e62045016..3641f8827 100644 --- a/pkg/controller/expcertificates/util.go +++ b/pkg/controller/expcertificates/util.go @@ -17,133 +17,15 @@ limitations under the License. package certificates import ( - "fmt" "reflect" - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/util/workqueue" cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2" - cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1alpha2" - controllerpkg "github.com/jetstack/cert-manager/pkg/controller" "github.com/jetstack/cert-manager/pkg/util" "github.com/jetstack/cert-manager/pkg/util/pki" ) -type GetFunc func(namespace, name string) (interface{}, error) - -func CertificateGetFunc(lister cmlisters.CertificateLister) GetFunc { - return func(namespace, name string) (interface{}, error) { - return lister.Certificates(namespace).Get(name) - } -} - -// EnqueueCertificatesForSecretNameFunc will enqueue Certificate resources that -// satisfy a CertificatePredicateFunc based upon the name of the Secret resource -// being processed. -// This is used to trigger Certificates to reconcile for changes to the Secret -// being managed. -func EnqueueCertificatesForSecretNameFunc(log logr.Logger, lister cmlisters.CertificateLister, selector labels.Selector, - secretNamePredicate WithCertificatePredicateFunc, queue workqueue.Interface) func(obj interface{}) { - return func(obj interface{}) { - s, ok := obj.(*corev1.Secret) - if !ok { - log.Info("Non-Secret type resource passed to EnqueueCertificatesForSecretFunc") - return - } - - certs, err := ListCertificatesMatchingPredicate(lister.Certificates(s.Namespace), selector, secretNamePredicate(s.Name)) - if err != nil { - log.Error(err, "Failed listing Certificate resources") - return - } - - for _, cert := range certs { - key, err := controllerpkg.KeyFunc(cert) - if err != nil { - log.Error(err, "Error determining 'key' for resource") - continue - } - queue.Add(key) - } - } -} - -type WithCertificatePredicateFunc func(string) CertificatePredicateFunc - -type CertificatePredicateFunc func(*cmapi.Certificate) bool - -func WithSecretNamePredicateFunc(name string) CertificatePredicateFunc { - return func(crt *cmapi.Certificate) bool { - return crt.Spec.SecretName == name - } -} - -func WithNextPrivateKeySecretNamePredicateFunc(name string) CertificatePredicateFunc { - return func(crt *cmapi.Certificate) bool { - if crt.Status.NextPrivateKeySecretName == nil { - return false - } - return *crt.Status.NextPrivateKeySecretName == name - } -} - -func ListCertificatesMatchingPredicate(lister cmlisters.CertificateNamespaceLister, selector labels.Selector, predicate CertificatePredicateFunc) ([]*cmapi.Certificate, error) { - crts, err := lister.List(selector) - if err != nil { - return nil, err - } - out := make([]*cmapi.Certificate, 0) - for _, crt := range crts { - if predicate(crt) { - out = append(out, crt) - } - } - return out, nil -} - -type CertificateRequestPredicateFunc func(*cmapi.CertificateRequest) bool - -func WithCertificateRevisionPredicateFunc(revision int) CertificateRequestPredicateFunc { - return func(req *cmapi.CertificateRequest) bool { - if req.Annotations == nil { - return false - } - return req.Annotations[cmapi.CertificateRequestRevisionAnnotationKey] == fmt.Sprintf("%d", revision) - } -} - -func WithCertificateRequestOwnerPredicateFunc(owner metav1.Object) CertificateRequestPredicateFunc { - return func(req *cmapi.CertificateRequest) bool { - return metav1.IsControlledBy(req, owner) - } -} - -func ListCertificateRequestsMatchingPredicates(lister cmlisters.CertificateRequestNamespaceLister, selector labels.Selector, predicates ...CertificateRequestPredicateFunc) ([]*cmapi.CertificateRequest, error) { - reqs, err := lister.List(selector) - if err != nil { - return nil, err - } - out := make([]*cmapi.CertificateRequest, 0) - for _, req := range reqs { - matches := true - for _, predicate := range predicates { - if !predicate(req) { - matches = false - break - } - } - if matches { - out = append(out, req) - } - } - - return out, nil -} - // RequestMatchesSpec compares a CertificateRequest with a CertificateSpec // and returns a list of field names on the Certificate that do not match their // counterpart fields on the CertificateRequest.