Adds base CertificateSigningRequest cert-manager controller

Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
This commit is contained in:
joshvanl 2021-05-27 00:23:50 +01:00
parent b38519fe66
commit c5c206cace
16 changed files with 1681 additions and 0 deletions

View File

@ -6,6 +6,7 @@ go_library(
"conditions.go",
"duration.go",
"issuers.go",
"kube.go",
"names.go",
"usages.go",
],
@ -15,6 +16,7 @@ go_library(
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/logs:go_default_library",
"@io_k8s_api//certificates/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_utils//clock:go_default_library",
],

View File

@ -52,6 +52,7 @@ filegroup(
"//pkg/controller/cainjector:all-srcs",
"//pkg/controller/certificaterequests:all-srcs",
"//pkg/controller/certificates:all-srcs",
"//pkg/controller/certificatesigningrequests:all-srcs",
"//pkg/controller/clusterissuers:all-srcs",
"//pkg/controller/ingress-shim:all-srcs",
"//pkg/controller/issuers:all-srcs",

View File

@ -0,0 +1,77 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"checks.go",
"controller.go",
"sync.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests",
visibility = ["//visibility:public"],
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/controller/certificatesigningrequests/util:go_default_library",
"//pkg/issuer:go_default_library",
"//pkg/logs:go_default_library",
"@com_github_go_logr_logr//:go_default_library",
"@io_k8s_api//authorization/v1:go_default_library",
"@io_k8s_api//certificates/v1: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_apimachinery//pkg/labels:go_default_library",
"@io_k8s_client_go//kubernetes/typed/authorization/v1:go_default_library",
"@io_k8s_client_go//kubernetes/typed/certificates/v1:go_default_library",
"@io_k8s_client_go//listers/certificates/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",
"@io_k8s_utils//clock:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["controller_test.go"],
embed = [":go_default_library"],
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/controller:go_default_library",
"//pkg/controller/certificatesigningrequests/fake:go_default_library",
"//pkg/controller/certificatesigningrequests/util:go_default_library",
"//pkg/controller/test:go_default_library",
"//test/unit/gen:go_default_library",
"@io_k8s_api//authorization/v1:go_default_library",
"@io_k8s_api//certificates/v1: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/runtime:go_default_library",
"@io_k8s_client_go//testing:go_default_library",
"@io_k8s_utils//clock/testing:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//pkg/controller/certificatesigningrequests/fake:all-srcs",
"//pkg/controller/certificatesigningrequests/util:all-srcs",
],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,83 @@
/*
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 certificatesigningrequests
import (
"fmt"
certificatesv1 "k8s.io/api/certificates/v1"
"k8s.io/apimachinery/pkg/labels"
"github.com/jetstack/cert-manager/pkg/apis/certmanager"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
func (c *Controller) handleGenericIssuer(obj interface{}) {
log := c.log.WithName("handleGenericIssuer")
iss, ok := obj.(cmapi.GenericIssuer)
if !ok {
log.Error(nil, "object does not implement GenericIssuer")
return
}
log = logf.WithResource(log, iss)
crs, err := c.certificateSigningRequestsForGenericIssuer(iss)
if err != nil {
log.Error(err, "error looking up certificates observing issuer or clusterissuer")
return
}
for _, cr := range crs {
log := logf.WithRelatedResource(log, cr)
key, err := keyFunc(cr)
if err != nil {
log.Error(err, "error computing key for resource")
continue
}
c.queue.Add(key)
}
}
func (c *Controller) certificateSigningRequestsForGenericIssuer(iss cmapi.GenericIssuer) ([]*certificatesv1.CertificateSigningRequest, error) {
csrs, err := c.csrLister.List(labels.NewSelector())
if err != nil {
return nil, fmt.Errorf("error listing certificates signing requests: %s", err.Error())
}
_, isClusterIssuer := iss.(*cmapi.ClusterIssuer)
var affected []*certificatesv1.CertificateSigningRequest
for _, csr := range csrs {
ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName)
switch {
case !ok,
ref.Group != certmanager.GroupName,
iss.GetNamespace() != ref.Namespace,
iss.GetName() != ref.Name,
isClusterIssuer && ref.Type != "clusterissuers",
!isClusterIssuer && ref.Type != "issuers":
continue
}
affected = append(affected, csr)
}
return affected, nil
}

View File

@ -0,0 +1,158 @@
/*
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 certificatesigningrequests
import (
"context"
"github.com/go-logr/logr"
certificatesv1 "k8s.io/api/certificates/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
authzclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1"
certificateslisters "k8s.io/client-go/listers/certificates/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
"k8s.io/utils/clock"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/issuer"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
const (
ControllerName = "certificatesigningrequests"
)
var keyFunc = controllerpkg.KeyFunc
// Signer is an implementation of a Kubernetes CertificateSigningRequest
// signer, backed by a cert-manager Issuer.
type Signer interface {
Sign(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error
}
type Controller struct {
helper issuer.Helper
// clientset used to update CertificateSigningRequest API resources
certClient certificatesclient.CertificateSigningRequestInterface
csrLister certificateslisters.CertificateSigningRequestLister
sarClient authzclient.SubjectAccessReviewInterface
queue workqueue.RateLimitingInterface
// logger to be used by this controller
log logr.Logger
// used to record Events about resources to the API
recorder record.EventRecorder
// Signer to call sign function
signer Signer
// the signer kind to react to when a certificate signing request is synced
signerType string
// used for testing
clock clock.Clock
}
func New(signerType string, signer Signer) *Controller {
return &Controller{
signerType: signerType,
signer: signer,
}
}
func (c *Controller) Register(ctx *controllerpkg.Context) (workqueue.RateLimitingInterface, []cache.InformerSynced, error) {
// construct a new named logger to be reused throughout the controller
c.log = logf.FromContext(ctx.RootContext, ControllerName)
// create a queue used to queue up items to be processed
c.queue = workqueue.NewNamedRateLimitingQueue(controllerpkg.DefaultItemBasedRateLimiter(), ControllerName)
c.sarClient = ctx.Client.AuthorizationV1().SubjectAccessReviews()
issuerInformer := ctx.SharedInformerFactory.Certmanager().V1().Issuers()
// obtain references to all the informers used by this controller
csrInformer := ctx.KubeSharedInformerFactory.Certificates().V1().CertificateSigningRequests()
// build a list of InformerSynced functions that will be returned by the Register method.
// the controller will only begin processing items once all of these informers have synced.
mustSync := []cache.InformerSynced{
csrInformer.Informer().HasSynced,
issuerInformer.Informer().HasSynced,
}
// if scoped to a single namespace
// if we are running in non-namespaced mode (i.e. --namespace=""), we also
// register event handlers and obtain a lister for clusterissuers.
clusterIssuerInformer := ctx.SharedInformerFactory.Certmanager().V1().ClusterIssuers()
if ctx.Namespace == "" {
// register handler function for clusterissuer resources
clusterIssuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer})
mustSync = append(mustSync, clusterIssuerInformer.Informer().HasSynced)
}
// set all the references to the listers for used by the Sync function
c.csrLister = csrInformer.Lister()
// register handler functions
csrInformer.Informer().AddEventHandler(&controllerpkg.QueuingEventHandler{Queue: c.queue})
issuerInformer.Informer().AddEventHandler(&controllerpkg.BlockingEventHandler{WorkFunc: c.handleGenericIssuer})
// create an issuer helper for reading generic issuers
c.helper = issuer.NewHelper(issuerInformer.Lister(), clusterIssuerInformer.Lister())
c.clock = ctx.Clock
// recorder records events about resources to the Kubernetes api
c.recorder = ctx.Recorder
c.certClient = ctx.Client.CertificatesV1().CertificateSigningRequests()
c.log.V(logf.DebugLevel).Info("new certificate signing request controller registered",
"type", c.signerType)
return c.queue, mustSync, nil
}
func (c *Controller) ProcessItem(ctx context.Context, key string) error {
log := logf.FromContext(ctx)
dbg := log.V(logf.DebugLevel)
_, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
log.Error(err, "invalid resource key")
return nil
}
csr, err := c.csrLister.Get(name)
if apierrors.IsNotFound(err) {
dbg.Info("certificate request in work queue no longer exists", "error", err.Error())
return nil
}
if err != nil {
return err
}
ctx = logf.NewContext(ctx, logf.WithResource(log, csr))
return c.Sync(ctx, csr)
}

View File

@ -0,0 +1,674 @@
/*
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 certificatesigningrequests
import (
"context"
"errors"
"testing"
"time"
authzv1 "k8s.io/api/authorization/v1"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
coretesting "k8s.io/client-go/testing"
fakeclock "k8s.io/utils/clock/testing"
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"
controllerpkg "github.com/jetstack/cert-manager/pkg/controller"
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake"
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
"github.com/jetstack/cert-manager/test/unit/gen"
)
func TestController(t *testing.T) {
fixedClockStart := time.Now()
fixedClock := fakeclock.NewFakeClock(fixedClockStart)
metaFixedClockStart := metav1.NewTime(fixedClockStart)
signerExpectNoCall := func(t *testing.T) Signer {
return &fake.Signer{
FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
t.Fatal("unexpected sign call")
return nil
},
}
}
sarReactionExpectNoCall := func(t *testing.T) coretesting.ReactionFunc {
return func(_ coretesting.Action) (bool, runtime.Object, error) {
t.Fatal("unexpected call")
return true, nil, nil
}
}
sarReactionAllow := func(t *testing.T) coretesting.ReactionFunc {
return func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, &authzv1.SubjectAccessReview{
Status: authzv1.SubjectAccessReviewStatus{
Allowed: true,
},
}, nil
}
}
tests := map[string]struct {
// key that should be passed to ProcessItem. If not set, the
// 'namespace/name' of the 'CertificateSigningRequest' field will be used.
// If neither is set, the key will be "".
key string
// CertificateSigningRequest to be synced for the test. If not set, the
// 'key' will be passed to ProcessItem instead.
existingCSR *certificatesv1.CertificateSigningRequest
// If not nil, generic issuer object will be made available for the test
existingIssuer runtime.Object
signerType string
signerImpl func(t *testing.T) Signer
sarReaction func(t *testing.T) coretesting.ReactionFunc
wantSARCreation []*authzv1.SubjectAccessReview
// wantEvent, if set, is an 'event string' that is expected to be fired.
wantEvent string
// wantConditions is the expected set of conditions on the
// CertificateSigningRequest resource if an Update is made.
// If nil, no update is expected.
// If empty, an update to the empty set/nil is expected.
wantConditions []certificatesv1.CertificateSigningRequestCondition
wantErr bool
}{
"do nothing if an empty 'key' is used": {
signerType: apiutil.IssuerCA,
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if an invalid 'key' is used": {
key: "abc/def/ghi",
signerType: apiutil.IssuerCA,
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if a key references a CertificateSigningRequest that does not exist": {
key: "namespace/name",
signerType: apiutil.IssuerCA,
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if a key references a CertificateSigningRequest that has a malformed SignerName for cert-manager.io": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("malformed.signer.name/"),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"if CertificateSigningRequest references the cert-manager.io signer group but the type is not recognised, should ignore": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("foo.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if CertificateSigningRequest has a SignerName not for cert-manager.io": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.my-group.io/hello.world"),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if CertificateSigningRequest is marked as Failed": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateFailed,
Status: corev1.ConditionTrue,
Reason: "FailedReason",
Message: "Failed message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if CertificateSigningRequest is no yet approved": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"do nothing if CertificateSigningRequest already has a non empty Certificate present": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
gen.SetCertificateSigningRequestCertificate([]byte("non-empty-certificate")),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"if CertificateSigningRequest references an Issuer that does not exist, should fire an event that it can't be found": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
existingIssuer: nil,
wantEvent: "Warning IssuerNotFound Referenced Issuer hello/world not found",
},
"if CertificateSigningRequest references an Issuer that does not yet have a type, should fire an event it doesn't have a type": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello")),
wantEvent: "Warning IssuerTypeMissing Referenced Issuer hello/world is missing type",
},
"if CertificateSigningRequest references an Issuer which does not match the same signer type, should ignore": {
signerType: apiutil.IssuerSelfSigned,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
),
},
"do nothing if CertificateSigningRequest references a signer that is not 'issuers' or 'clusterissuers'": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("not-issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
},
"if CertificateSigningRequest references a issuers signer but the SubjectAccessReview errors, should error": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestUsername("user-1"),
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
gen.SetCertificateSigningRequestUID("uid-1"),
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
"extra": []string{"1", "2"},
}),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: func(t *testing.T) coretesting.ReactionFunc {
return func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("this is a simulated error")
}
},
wantSARCreation: []*authzv1.SubjectAccessReview{
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "world",
Version: "*",
},
},
},
},
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
),
wantErr: true,
},
"if CertificateSigningRequest references a issuers signer but the requesting user does not have permissions, should update Failed": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestUsername("user-1"),
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
gen.SetCertificateSigningRequestUID("uid-1"),
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
"extra": []string{"1", "2"},
}),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: func(t *testing.T) coretesting.ReactionFunc {
return func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, &authzv1.SubjectAccessReview{
Status: authzv1.SubjectAccessReviewStatus{
Allowed: false,
},
}, nil
}
},
wantSARCreation: []*authzv1.SubjectAccessReview{
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "world",
Version: "*",
},
},
},
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "*",
Version: "*",
},
},
},
},
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
),
wantEvent: "Warning DeniedReference Requester may not reference Namespaced Issuer hello/world",
wantConditions: []certificatesv1.CertificateSigningRequestCondition{
{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
},
{
Type: certificatesv1.CertificateFailed,
Status: corev1.ConditionTrue,
Reason: "DeniedReference",
Message: "Requester may not reference Namespaced Issuer hello/world",
LastTransitionTime: metaFixedClockStart,
LastUpdateTime: metaFixedClockStart,
},
},
},
"if CertificateSigningRequest references a clusterissuers signer but the signer name contains a namespace, should update with failed": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("clusterissuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionExpectNoCall,
existingIssuer: gen.ClusterIssuer("world",
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
),
wantEvent: "Warning BadSignerName Signer clusterissuers may not be referenced with namespace (hello)",
wantConditions: []certificatesv1.CertificateSigningRequestCondition{
{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
},
{
Type: certificatesv1.CertificateFailed,
Status: corev1.ConditionTrue,
Reason: "BadSignerName",
Message: "Signer clusterissuers may not be referenced with namespace (hello)",
LastTransitionTime: metaFixedClockStart,
LastUpdateTime: metaFixedClockStart,
},
},
},
"if CertificateSigningRequest references a issuers signer but the Issuer is not ready, fire event not Ready": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestUsername("user-1"),
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
gen.SetCertificateSigningRequestUID("uid-1"),
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
"extra": []string{"1", "2"},
}),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: signerExpectNoCall,
sarReaction: sarReactionAllow,
wantSARCreation: []*authzv1.SubjectAccessReview{
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "world",
Version: "*",
},
},
},
},
wantEvent: "Warning IssuerNotReady Referenced Issuer hello/world does not have a Ready status condition",
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
),
},
"if CertificateSigningRequest called invoked sign but it errors, should return error": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestUsername("user-1"),
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
gen.SetCertificateSigningRequestUID("uid-1"),
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
"extra": []string{"1", "2"},
}),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: func(t *testing.T) Signer {
return &fake.Signer{
FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
return errors.New("this is a simulated error")
},
}
},
sarReaction: sarReactionAllow,
wantSARCreation: []*authzv1.SubjectAccessReview{
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "world",
Version: "*",
},
},
},
},
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmmeta.ConditionTrue,
Reason: "IssuerReady",
Message: "Issuer ready message",
}),
),
wantErr: true,
},
"if CertificateSigningRequest called invoked sign and doesn't error, should return no error": {
signerType: apiutil.IssuerCA,
existingCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/hello.world"),
gen.SetCertificateSigningRequestUsername("user-1"),
gen.SetCertificateSigningRequestGroups([]string{"group-1", "group-2"}),
gen.SetCertificateSigningRequestUID("uid-1"),
gen.SetCertificateSigningRequestExtra(map[string]certificatesv1.ExtraValue{
"extra": []string{"1", "2"},
}),
gen.SetCertificateSigningRequestStatusCondition(certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "ApprovedReason",
Message: "Approved message",
}),
),
signerImpl: func(t *testing.T) Signer {
return &fake.Signer{
FakeSign: func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error {
return nil
},
}
},
sarReaction: sarReactionAllow,
wantSARCreation: []*authzv1.SubjectAccessReview{
{
Spec: authzv1.SubjectAccessReviewSpec{
User: "user-1",
Groups: []string{"group-1", "group-2"},
Extra: map[string]authzv1.ExtraValue{
"extra": []string{"1", "2"},
},
UID: "uid-1",
ResourceAttributes: &authzv1.ResourceAttributes{
Group: "cert-manager.io",
Resource: "signers",
Verb: "reference",
Namespace: "hello",
Name: "world",
Version: "*",
},
},
},
},
existingIssuer: gen.Issuer("world", gen.SetIssuerNamespace("hello"),
gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "tls",
}),
gen.AddIssuerCondition(cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmmeta.ConditionTrue,
Reason: "IssuerReady",
Message: "Issuer ready message",
}),
),
wantErr: false,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
util.Clock = fixedClock
builder := &testpkg.Builder{
T: t,
Clock: fixedClock,
}
if test.existingIssuer != nil {
builder.CertManagerObjects = append(builder.CertManagerObjects, test.existingIssuer)
}
if test.existingCSR != nil {
builder.KubeObjects = append(builder.KubeObjects, test.existingCSR)
}
for i := range test.wantSARCreation {
builder.ExpectedActions = append(builder.ExpectedActions,
testpkg.NewAction(coretesting.NewCreateAction(
authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews"),
"",
test.wantSARCreation[i],
)),
)
}
builder.Init()
builder.FakeKubeClient().PrependReactor("create", "*", func(action coretesting.Action) (bool, runtime.Object, error) {
if action.GetResource() != authzv1.SchemeGroupVersion.WithResource("subjectaccessreviews") {
return false, nil, nil
}
return test.sarReaction(t)(action)
})
controller := New(test.signerType, test.signerImpl(t))
_, _, err := controller.Register(builder.Context)
if err != nil {
t.Fatal(err)
}
if test.wantConditions != nil {
if test.existingCSR == nil {
t.Fatal("cannot expect an Update operation if test.existingCSR is nil")
}
expectedCSR := test.existingCSR.DeepCopy()
expectedCSR.Status.Conditions = test.wantConditions
builder.ExpectedActions = append(builder.ExpectedActions,
testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
certificatesv1.SchemeGroupVersion.WithResource("certificatesigningrequests"),
"status",
"",
expectedCSR,
)),
)
}
if test.wantEvent != "" {
builder.ExpectedEvents = []string{test.wantEvent}
}
builder.Start()
defer builder.Stop()
key := test.key
if key == "" && test.existingCSR != nil {
key, err = controllerpkg.KeyFunc(test.existingCSR)
if err != nil {
t.Fatal(err)
}
}
gotErr := controller.ProcessItem(context.Background(), key)
if test.wantErr != (gotErr != nil) {
t.Errorf("got unexpected error, exp=%t got=%v",
test.wantErr, gotErr)
}
builder.CheckAndFinish()
})
}
}

View File

@ -0,0 +1,26 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = ["fake.go"],
importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/fake",
visibility = ["//visibility:public"],
deps = [
"//pkg/apis/certmanager/v1:go_default_library",
"@io_k8s_api//certificates/v1: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"],
)

View File

@ -0,0 +1,33 @@
/*
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 fake
import (
"context"
certificatesv1 "k8s.io/api/certificates/v1"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
)
type Signer struct {
FakeSign func(context.Context, *certificatesv1.CertificateSigningRequest, cmapi.GenericIssuer) error
}
func (s *Signer) Sign(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerObj cmapi.GenericIssuer) error {
return s.FakeSign(ctx, csr, issuerObj)
}

View File

@ -0,0 +1,197 @@
/*
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 certificatesigningrequests
import (
"context"
"fmt"
authzv1 "k8s.io/api/authorization/v1"
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiutil "github.com/jetstack/cert-manager/pkg/api/util"
"github.com/jetstack/cert-manager/pkg/apis/certmanager"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
"github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
func (c *Controller) Sync(ctx context.Context, csr *certificatesv1.CertificateSigningRequest) error {
log := logf.WithResource(logf.FromContext(ctx), csr).WithValues("signerName", csr.Spec.SignerName)
dbg := log.V(logf.DebugLevel)
ref, ok := util.SignerIssuerRefFromSignerName(csr.Spec.SignerName)
if !ok {
dbg.Info("certificate signing request has malformed signer name,", "signerName", csr.Spec.SignerName)
return nil
}
if ref.Group != certmanager.GroupName {
dbg.Info("certificate signing request signerName group does not match 'cert-manager.io' group so skipping processing")
return nil
}
if util.CertificateSigningRequestIsFailed(csr) {
dbg.Info("certificate signing request has failed so skipping processing")
return nil
}
if !util.CertificateSigningRequestIsApproved(csr) {
dbg.Info("certificate signing request is not approved so skipping processing")
return nil
}
if len(csr.Status.Certificate) > 0 {
dbg.Info("certificate field is already set in status so skipping processing")
return nil
}
var kind string
switch ref.Type {
case "issuers":
kind = cmapi.IssuerKind
break
case "clusterissuers":
kind = cmapi.ClusterIssuerKind
break
default:
dbg.Info("certificate signing request signerName type does not match 'issuers' or 'clusterissuers' so skipping processing")
return nil
}
issuerObj, err := c.helper.GetGenericIssuer(cmmeta.ObjectReference{
Name: ref.Name,
Kind: kind,
Group: ref.Group,
}, ref.Namespace)
if apierrors.IsNotFound(err) {
c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotFound", "Referenced %s %s/%s not found", kind, ref.Namespace, ref.Name)
return nil
}
if err != nil {
log.Error(err, "failed to get issuer")
return err
}
log = logf.WithRelatedResource(log, issuerObj)
dbg.Info("ensuring issuer type matches this controller")
signerType, err := apiutil.NameForIssuer(issuerObj)
if err != nil {
c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerTypeMissing", "Referenced %s %s/%s is missing type", kind, ref.Namespace, ref.Name)
return nil
}
// This CertificateSigningRequest is not meant for us, ignore
if signerType != c.signerType {
dbg.WithValues(logf.RelatedResourceKindKey, signerType).Info("signer reference type does not match controller resource kind, ignoring")
return nil
}
switch kind {
case cmapi.IssuerKind:
ok, err := c.userCanReferenceSigner(ctx, csr, ref.Namespace, ref.Name)
if err != nil {
return err
}
if !ok {
message := fmt.Sprintf("Requester may not reference Namespaced Issuer %s/%s", ref.Namespace, ref.Name)
c.recorder.Event(csr, corev1.EventTypeWarning, "DeniedReference", message)
util.CertificateSigningRequestSetFailed(csr, "DeniedReference", message)
if _, err := c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{}); err != nil {
return err
}
return nil
}
case cmapi.ClusterIssuerKind:
// Namespace not valid for a clusterissuer
if len(ref.Namespace) > 0 {
message := fmt.Sprintf("Signer clusterissuers may not be referenced with namespace (%s)", ref.Namespace)
c.recorder.Event(csr, corev1.EventTypeWarning, "BadSignerName", message)
util.CertificateSigningRequestSetFailed(csr, "BadSignerName", message)
if _, err := c.certClient.UpdateStatus(ctx, csr, metav1.UpdateOptions{}); err != nil {
return err
}
return nil
}
}
// check ready condition
if !apiutil.IssuerHasCondition(issuerObj, cmapi.IssuerCondition{
Type: cmapi.IssuerConditionReady,
Status: cmmeta.ConditionTrue,
}) {
c.recorder.Eventf(csr, corev1.EventTypeWarning, "IssuerNotReady", "Referenced %s %s/%s does not have a Ready status condition",
kind, issuerObj.GetNamespace(), issuerObj.GetName())
return nil
}
dbg.Info("invoking sign function as existing certificate does not exist")
return c.signer.Sign(ctx, csr, issuerObj)
}
// userCanReferenceSigner will return true if the CSR requester has a bound
// role that allows them to reference a given Namespaced signer. The user must
// have the permissions:
// group: cert-manager.io
// resource: signers
// verb: reference
// namespace: <referenced signer namespace>
// name: <either the name of the signer or '*' for all signer names in that namespace>
func (c *Controller) userCanReferenceSigner(ctx context.Context, csr *certificatesv1.CertificateSigningRequest, issuerNamespace, issuerName string) (bool, error) {
extra := make(map[string]authzv1.ExtraValue)
for k, v := range csr.Spec.Extra {
extra[k] = authzv1.ExtraValue(v)
}
for _, name := range []string{issuerName, "*"} {
resp, err := c.sarClient.Create(ctx, &authzv1.SubjectAccessReview{
Spec: authzv1.SubjectAccessReviewSpec{
User: csr.Spec.Username,
Groups: csr.Spec.Groups,
Extra: extra,
UID: csr.Spec.UID,
ResourceAttributes: &authzv1.ResourceAttributes{
Group: certmanager.GroupName,
Resource: "signers",
Verb: "reference",
Namespace: issuerNamespace,
Name: name,
Version: "*",
},
},
}, metav1.CreateOptions{})
if err != nil {
return false, err
}
if resp.Status.Allowed {
return true, nil
}
}
return false, nil
}

View File

@ -0,0 +1,38 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"conditions.go",
"signername.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/controller/certificatesigningrequests/util",
visibility = ["//visibility:public"],
deps = [
"//pkg/logs:go_default_library",
"@io_k8s_api//certificates/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_utils//clock:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["signername_test.go"],
embed = [":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"],
)

View File

@ -0,0 +1,65 @@
/*
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 util
import (
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/clock"
logf "github.com/jetstack/cert-manager/pkg/logs"
)
// Clock is defined as a package var so it can be stubbed out during tests.
var Clock clock.Clock = clock.RealClock{}
func CertificateSigningRequestIsApproved(csr *certificatesv1.CertificateSigningRequest) bool {
for _, cond := range csr.Status.Conditions {
if cond.Type == certificatesv1.CertificateApproved {
return true
}
}
return false
}
func CertificateSigningRequestIsFailed(csr *certificatesv1.CertificateSigningRequest) bool {
for _, cond := range csr.Status.Conditions {
if cond.Type == certificatesv1.CertificateFailed {
return true
}
}
return false
}
func CertificateSigningRequestSetFailed(csr *certificatesv1.CertificateSigningRequest, reason, message string) {
nowTime := metav1.NewTime(Clock.Now())
// Since we only ever set this condition once (enforced by the API), we
// needn't need to check whether the condition is already set.
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateFailed,
Status: corev1.ConditionTrue,
Reason: reason,
Message: message,
LastTransitionTime: nowTime,
LastUpdateTime: nowTime,
})
logf.V(logf.InfoLevel).Infof("Setting lastTransitionTime for CertificateSigningRequest %s/%s condition Failed to %v",
csr.Namespace, csr.Name, nowTime.Time)
}

View File

@ -0,0 +1,63 @@
/*
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 util
import (
"strings"
)
type SignerIssuerRef struct {
Namespace, Name string
Type, Group string
}
// SignerIssuerRefFromSignerName will return a SignerIssuerRef from a
// CertificateSigningRequests.SignerName
func SignerIssuerRefFromSignerName(name string) (SignerIssuerRef, bool) {
split := strings.Split(name, "/")
if len(split) != 2 {
return SignerIssuerRef{}, false
}
signerTypeSplit := strings.SplitN(split[0], ".", 2)
signerNameSplit := strings.Split(split[1], ".")
if len(signerTypeSplit) < 2 || signerNameSplit[0] == "" {
return SignerIssuerRef{}, false
}
switch len(signerNameSplit) {
case 1:
return SignerIssuerRef{
Namespace: "",
Name: signerNameSplit[0],
Type: signerTypeSplit[0],
Group: signerTypeSplit[1],
}, true
case 2:
return SignerIssuerRef{
Namespace: signerNameSplit[0],
Name: signerNameSplit[1],
Type: signerTypeSplit[0],
Group: signerTypeSplit[1],
}, true
default:
return SignerIssuerRef{}, false
}
}

View File

@ -0,0 +1,116 @@
/*
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 util
import (
"reflect"
"testing"
)
func TestIssuerRefFromSignerName(t *testing.T) {
tests := map[string]struct {
inpName string
expSignerIssuerRef SignerIssuerRef
expOK bool
}{
"an empty name should return false": {
inpName: "",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference without a name should return false": {
inpName: "foo.bar",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference with a '/' but no name should return false": {
inpName: "foo.bar/",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference with no host should return false": {
inpName: "/foo.bar",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference with only one domain should return false": {
inpName: "abc/hello-world",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference with too many names should return false": {
inpName: "foo.bar/hello.world.123",
expSignerIssuerRef: SignerIssuerRef{},
expOK: false,
},
"a reference with 2 domains and 2 names should return namespaced issuer": {
inpName: "foo.bar/hello.world",
expSignerIssuerRef: SignerIssuerRef{
Namespace: "hello",
Name: "world",
Type: "foo",
Group: "bar",
},
expOK: true,
},
"a reference with 4 domains and 2 names should return namespaced issuer": {
inpName: "foo.bar.abc.dbc/hello.world",
expSignerIssuerRef: SignerIssuerRef{
Namespace: "hello",
Name: "world",
Type: "foo",
Group: "bar.abc.dbc",
},
expOK: true,
},
"a reference with 2 domains and one name should return cluster issuer": {
inpName: "foo.bar/hello-world",
expSignerIssuerRef: SignerIssuerRef{
Namespace: "",
Name: "hello-world",
Type: "foo",
Group: "bar",
},
expOK: true,
},
"a reference with 4 domains and 1 name should return cluster issuer": {
inpName: "foo.bar.abc.dbc/hello-world",
expSignerIssuerRef: SignerIssuerRef{
Namespace: "",
Name: "hello-world",
Type: "foo",
Group: "bar.abc.dbc",
},
expOK: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ref, ok := SignerIssuerRefFromSignerName(test.inpName)
if ok != test.expOK {
t.Errorf("unexpected ok, exp=%t got=%t",
test.expOK, ok)
}
if !reflect.DeepEqual(ref, test.expSignerIssuerRef) {
t.Errorf("unexpected SignerIssuerRef, exp=%v got=%v",
test.expSignerIssuerRef, ref)
}
})
}
}

View File

@ -6,6 +6,7 @@ go_library(
"csr.go",
"generate.go",
"keyusage.go",
"kube.go",
"parse.go",
],
importpath = "github.com/jetstack/cert-manager/pkg/util/pki",
@ -13,7 +14,9 @@ go_library(
deps = [
"//pkg/api/util:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/experimental/v1alpha1:go_default_library",
"//pkg/util/errors:go_default_library",
"@io_k8s_api//certificates/v1:go_default_library",
],
)

View File

@ -5,6 +5,7 @@ go_library(
srcs = [
"certificate.go",
"certificaterequest.go",
"certificatesigningrequest.go",
"challenge.go",
"conditions.go",
"csr.go",
@ -19,8 +20,10 @@ go_library(
deps = [
"//pkg/apis/acme/v1:go_default_library",
"//pkg/apis/certmanager/v1:go_default_library",
"//pkg/apis/experimental/v1alpha1:go_default_library",
"//pkg/apis/meta/v1:go_default_library",
"//pkg/util/pki:go_default_library",
"@io_k8s_api//certificates/v1: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/types:go_default_library",

View File

@ -0,0 +1,142 @@
/*
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 gen
import (
"encoding/base64"
"strconv"
certificatesv1 "k8s.io/api/certificates/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
experimentalapi "github.com/jetstack/cert-manager/pkg/apis/experimental/v1alpha1"
)
type CertificateSigningRequestModifier func(*certificatesv1.CertificateSigningRequest)
func CertificateSigningRequest(name string, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest {
c := &certificatesv1.CertificateSigningRequest{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Annotations: make(map[string]string),
Labels: make(map[string]string),
},
}
for _, mod := range mods {
mod(c)
}
return c
}
func CertificateSigningRequestFrom(cr *certificatesv1.CertificateSigningRequest, mods ...CertificateSigningRequestModifier) *certificatesv1.CertificateSigningRequest {
cr = cr.DeepCopy()
for _, mod := range mods {
mod(cr)
}
return cr
}
func SetCertificateSigningRequestIsCA(isCA bool) CertificateSigningRequestModifier {
return AddCertificateSigningRequestAnnotations(map[string]string{
experimentalapi.CertificateSigningRequestIsCAAnnotationKey: strconv.FormatBool(isCA),
})
}
func SetCertificateSigningRequestRequest(request []byte) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.Request = request
}
}
func AddCertificateSigningRequestAnnotations(annotations map[string]string) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
// Make sure to do a merge here with new annotations overriding.
annotationsNew := csr.GetAnnotations()
if annotationsNew == nil {
annotationsNew = make(map[string]string)
}
for k, v := range annotations {
annotationsNew[k] = v
}
csr.SetAnnotations(annotationsNew)
}
}
func SetCertificateSigningRequestSignerName(signerName string) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.SignerName = signerName
}
}
func SetCertificateSigningRequestDuration(duration string) CertificateSigningRequestModifier {
return AddCertificateSigningRequestAnnotations(map[string]string{
experimentalapi.CertificateSigningRequestDurationAnnotationKey: duration,
})
}
func SetCertificateSigningRequestCertificate(cert []byte) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Status.Certificate = cert
}
}
func SetCertificateSigningRequestCA(ca []byte) CertificateSigningRequestModifier {
return AddCertificateSigningRequestAnnotations(map[string]string{
experimentalapi.CertificateSigningRequestCAAnnotationKey: base64.StdEncoding.EncodeToString(ca),
})
}
func SetCertificateSigningRequestStatusCondition(c certificatesv1.CertificateSigningRequestCondition) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
if len(csr.Status.Conditions) == 0 {
csr.Status.Conditions = []certificatesv1.CertificateSigningRequestCondition{c}
return
}
for i, existingC := range csr.Status.Conditions {
if existingC.Type == c.Type {
csr.Status.Conditions[i] = c
return
}
}
csr.Status.Conditions = append(csr.Status.Conditions, c)
}
}
func SetCertificateSigningRequestUsername(username string) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.Username = username
}
}
func SetCertificateSigningRequestGroups(groups []string) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.Groups = groups
}
}
func SetCertificateSigningRequestUID(uid string) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.UID = uid
}
}
func SetCertificateSigningRequestExtra(extra map[string]certificatesv1.ExtraValue) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.Extra = extra
}
}