Adds CertificateRequest approver controller. This controller will

currently _always_ set the Approved condition to true on
CertificateRequests

Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
This commit is contained in:
joshvanl 2021-03-04 16:27:33 +00:00
parent 0ef25daeb3
commit 2db7582586
4 changed files with 472 additions and 0 deletions

View File

@ -0,0 +1,56 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"approver.go",
"sync.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/approver",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/client/clientset/versioned:go_default_library",
"//pkg/client/listers/certmanager/v1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/logs:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/api/errors:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_client_go//tools/cache:go_default_library",
"@io_k8s_client_go//tools/record:go_default_library",
"@io_k8s_client_go//util/workqueue: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 = ["approver_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/controller/test:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_client_go//testing:go_default_library",
"@io_k8s_utils//clock/testing:go_default_library",
],
)

View File

@ -0,0 +1,103 @@
/*
Copyright 2021 The cert-manager 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 approver
import (
"context"
"github.com/go-logr/logr"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
cmlisters "github.com/jetstack/cert-manager/pkg/client/listers/certmanager/v1"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
const (
ControllerName = "certificaterequests-approver"
)
// Controller is a CertificateRequest controller which manages the "Approved"
// condition. In the absence of any automated policy engine, this controller
// will _always_ set the "Approved" condition to True. All CertificateRequest
// signing controllers should wait until the "Approved" condition is set to
// True before processing.
type Controller struct {
// logger to be used by this controller
log logr.Logger
certificateRequestLister cmlisters.CertificateRequestLister
cmClient cmclient.Interface
recorder record.EventRecorder
queue workqueue.RateLimitingInterface
}
func init() {
// create certificate request approver controller
controllerpkg.Register(ControllerName, func(ctx *controllerpkg.Context) (controllerpkg.Interface, error) {
return controllerpkg.NewBuilder(ctx, ControllerName).
For(new(Controller)).Complete()
})
}
// Register registers and constructs the controller using the provided context.
// It returns the workqueue to be used to enqueue items, a list of
// InformerSynced functions that must be synced, or an error.
func (c *Controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) {
c.log = logf.FromContext(ctx.RootContext, ControllerName)
c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName)
certificateRequestInformer := ctx.SharedInformerFactory.Certmanager().V1().CertificateRequests()
mustSync := append([]cache.InformerSynced{certificateRequestInformer.Informer().HasSynced})
certificateRequestInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue})
c.certificateRequestLister = certificateRequestInformer.Lister()
c.cmClient = ctx.CMClient
c.recorder = ctx.Recorder
c.log.V(logf.DebugLevel).Info("certificate request approver controller registered")
return c.queue, mustSync, nil
}
func (c *Controller) ProcessItem(ctx context.Context, key string) error {
log := logf.FromContext(ctx)
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
log.Error(err, "invalid resource key")
return nil
}
cr, err := c.certificateRequestLister.CertificateRequests(namespace).Get(name)
if apierrors.IsNotFound(err) {
log.Error(err, "certificate request in work queue no longer exists")
return nil
}
if err != nil {
return err
}
ctx = logf.NewContext(ctx, logf.WithResource(log, cr))
return c.Sync(ctx, cr)
}

View File

@ -0,0 +1,240 @@
/*
Copyright 2021 The cert-manager 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 approver
import (
"context"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
coretesting "k8s.io/client-go/testing"
fakeclock "k8s.io/utils/clock/testing"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
)
func TestProcessItem(t *testing.T) {
// now time is the current time at the start of the test (the clock is fixed)
now := time.Now()
metaNow := metav1.NewTime(now)
tests := map[string]struct {
// key that should be passed to ProcessItem.
// if not set, the 'namespace/name' of the 'CertificateRequest' field will be used.
// if neither is set, the key will be ""
key string
// CertificateRequest to be synced for the test.
// if not set, the 'key' will be passed to ProcessItem instead.
request *cmapi.CertificateRequest
// expectedEvent, if set, is an 'event string' that is expected to be fired.
expectedEvent string
// expectedConditions is the expected set of conditions on the
// CertificateRequest resource if an Update is made.
// If nil, no update is expected.
// If empty, an update to the empty set/nil is expected.
expectedConditions []cmapi.CertificateRequestCondition
// err is the expected error text returned by the controller, if any.
err string
}{
"do nothing if an empty 'key' is used": {},
"do nothing if an invalid 'key' is used": {
key: "abc/def/ghi",
},
"do nothing if a key references a Certificate that does not exist": {
key: "namespace/name",
},
"do nothing if CertificateRequest already has 'Approved' True condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionApproved,
Status: cmmeta.ConditionTrue,
},
},
},
},
},
"do nothing if CertificateRequest already has 'Denied' True condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionApproved,
Status: cmmeta.ConditionFalse,
},
},
},
},
},
"do nothing if CertificateRequest already has 'Ready' Failed condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionReady,
Status: cmmeta.ConditionFalse,
Reason: cmapi.CertificateRequestReasonFailed,
},
},
},
},
},
"do nothing if CertificateRequest already has 'Ready' Issued condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionReady,
Status: cmmeta.ConditionTrue,
Reason: cmapi.CertificateRequestReasonIssued,
},
},
},
},
},
"approve CertificateRequest if no condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{},
},
},
expectedConditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionApproved,
Status: cmmeta.ConditionTrue,
Reason: cmapi.CertificateRequestReasonApproved,
Message: ApprovedMessage,
LastTransitionTime: &metaNow,
},
},
expectedEvent: "Normal Approved Certificate request has been approved by cert-manager.io",
},
"approve CertificateRequest has 'Ready' Pending condition": {
request: &cmapi.CertificateRequest{
ObjectMeta: metav1.ObjectMeta{Namespace: "testns", Name: "test"},
Status: cmapi.CertificateRequestStatus{
Conditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionReady,
Status: cmmeta.ConditionFalse,
Reason: cmapi.CertificateRequestReasonPending,
},
},
},
},
expectedConditions: []cmapi.CertificateRequestCondition{
{
Type: cmapi.CertificateRequestConditionReady,
Status: cmmeta.ConditionFalse,
Reason: cmapi.CertificateRequestReasonPending,
},
{
Type: cmapi.CertificateRequestConditionApproved,
Status: cmmeta.ConditionTrue,
Reason: cmapi.CertificateRequestReasonApproved,
Message: ApprovedMessage,
LastTransitionTime: &metaNow,
},
},
expectedEvent: "Normal Approved Certificate request has been approved by cert-manager.io",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
// Create and initialise a new unit test builder
builder := &testpkg.Builder{
T: t,
Clock: fakeclock.NewFakeClock(now),
}
if test.request != nil {
builder.CertManagerObjects = append(builder.CertManagerObjects, test.request)
}
builder.Init()
c := new(Controller)
_, _, err := c.Register(builder.Context)
if err != nil {
t.Fatal(err)
}
if test.expectedConditions != nil {
if test.request == nil {
t.Fatal("cannot expect an Update operation if test.request is nil")
}
expectedRequest := test.request.DeepCopy()
expectedRequest.Status.Conditions = test.expectedConditions
builder.ExpectedActions = append(builder.ExpectedActions,
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
"status",
test.request.Namespace,
expectedRequest,
)),
)
}
if test.expectedEvent != "" {
builder.ExpectedEvents = []string{test.expectedEvent}
}
// Start the informers and begin processing updates
builder.Start()
defer builder.Stop()
key := test.key
if key == "" && test.request != nil {
key, err = controllerpkg.KeyFunc(test.request)
if err != nil {
t.Fatal(err)
}
}
// Call ProcessItem
err = c.ProcessItem(context.Background(), key)
switch {
case err != nil:
if test.err != err.Error() {
t.Errorf("error text did not match, got=%s, exp=%s", err.Error(), test.err)
}
default:
if test.err != "" {
t.Errorf("got no error but expected: %s", test.err)
}
}
if err := builder.AllEventsCalled(); err != nil {
builder.T.Error(err)
}
if err := builder.AllActionsExecuted(); err != nil {
builder.T.Error(err)
}
if err := builder.AllReactorsCalled(); err != nil {
builder.T.Error(err)
}
})
}
}

View File

@ -0,0 +1,73 @@
/*
Copyright 2021 The cert-manager 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 approver
import (
"context"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
const (
ApprovedMessage = "Certificate request has been approved by cert-manager.io"
)
// Sync will set the "Approved" condition to True on synced
// CertificateRequests. If the "Denied", "Approved" or "Ready" condition alrady
// exists, exit early.
func (c *Controller) Sync(ctx context.Context, cr *cmapi.CertificateRequest) (err error) {
log := logf.FromContext(ctx, "approver")
switch {
case
// If the CertificateRequest has already been approved, exit early.
apiutil.CertificateRequestHasApproved(cr),
// If the CertificateRequest has already been denied, exit early.
apiutil.CertificateRequestHasDenied(cr),
// If the CertificateRequest is "Issued" or "Failed", exit early.
apiutil.CertificateRequestReadyReason(cr) == cmapi.CertificateRequestReasonFailed,
apiutil.CertificateRequestReadyReason(cr) == cmapi.CertificateRequestReasonIssued:
return nil
}
// Update the CertificateRequest approved condition to true.
apiutil.SetCertificateRequestCondition(cr,
cmapi.CertificateRequestConditionApproved,
cmmeta.ConditionTrue,
cmapi.CertificateRequestReasonApproved,
ApprovedMessage,
)
// Update CertificateRequest with
_, err = c.cmClient.CertmanagerV1().CertificateRequests(cr.Namespace).UpdateStatus(ctx, cr, metav1.UpdateOptions{})
if err != nil {
return err
}
c.recorder.Event(cr, corev1.EventTypeNormal, cmapi.CertificateRequestReasonApproved, ApprovedMessage)
log.V(logf.DebugLevel).Info("approved certificate request")
return nil
}