Switch cert manager to v1 CSR API by default, falling back to v1beta1

Kubernetes-commit: a298c14f18d4973a9ceaf21f1e0dc4e39b4c5bfb
This commit is contained in:
Jordan Liggitt 2020-06-03 22:40:02 -04:00 committed by Kubernetes Publisher
parent 0adb702ae4
commit 3ab7d09ea9
4 changed files with 426 additions and 176 deletions

View File

@ -31,12 +31,12 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
certificates "k8s.io/api/certificates/v1beta1" certificates "k8s.io/api/certificates/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/cert" "k8s.io/client-go/util/cert"
"k8s.io/client-go/util/certificate/csr" "k8s.io/client-go/util/certificate/csr"
"k8s.io/client-go/util/keyutil" "k8s.io/client-go/util/keyutil"
@ -68,11 +68,11 @@ type Manager interface {
// Config is the set of configuration parameters available for a new Manager. // Config is the set of configuration parameters available for a new Manager.
type Config struct { type Config struct {
// ClientFn will be used to create a client for // ClientsetFn will be used to create a clientset for
// signing new certificate requests generated when a key rotation occurs. // creating/fetching new certificate requests generated when a key rotation occurs.
// It must be set at initialization. The function will never be invoked // The function will never be invoked in parallel.
// in parallel. It is passed the current client certificate if one exists. // It is passed the current client certificate if one exists.
ClientFn CSRClientFunc ClientsetFn ClientsetFunc
// Template is the CertificateRequest that will be used as a template for // Template is the CertificateRequest that will be used as a template for
// generating certificate signing requests for all new keys generated as // generating certificate signing requests for all new keys generated as
// part of rotation. It follows the same rules as the template parameter of // part of rotation. It follows the same rules as the template parameter of
@ -162,9 +162,9 @@ type Counter interface {
// NoCertKeyError indicates there is no cert/key currently available. // NoCertKeyError indicates there is no cert/key currently available.
type NoCertKeyError string type NoCertKeyError string
// CSRClientFunc returns a new client for requesting CSRs. It passes the // ClientsetFunc returns a new clientset for discovering CSR API availability and requesting CSRs.
// current certificate if one is available and valid. // It is passed the current certificate if one is available and valid.
type CSRClientFunc func(current *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) type ClientsetFunc func(current *tls.Certificate) (clientset.Interface, error)
func (e *NoCertKeyError) Error() string { return string(*e) } func (e *NoCertKeyError) Error() string { return string(*e) }
@ -193,7 +193,7 @@ type manager struct {
// the clientFn must only be accessed under the clientAccessLock // the clientFn must only be accessed under the clientAccessLock
clientAccessLock sync.Mutex clientAccessLock sync.Mutex
clientFn CSRClientFunc clientsetFn ClientsetFunc
stopCh chan struct{} stopCh chan struct{}
stopped bool stopped bool
@ -220,7 +220,7 @@ func NewManager(config *Config) (Manager, error) {
m := manager{ m := manager{
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
clientFn: config.ClientFn, clientsetFn: config.ClientsetFn,
getTemplate: getTemplate, getTemplate: getTemplate,
dynamicTemplate: config.GetTemplate != nil, dynamicTemplate: config.GetTemplate != nil,
signerName: config.SignerName, signerName: config.SignerName,
@ -274,7 +274,7 @@ func (m *manager) Start() {
// Certificate rotation depends on access to the API server certificate // Certificate rotation depends on access to the API server certificate
// signing API, so don't start the certificate manager if we don't have a // signing API, so don't start the certificate manager if we don't have a
// client. // client.
if m.clientFn == nil { if m.clientsetFn == nil {
klog.V(2).Infof("Certificate rotation is not enabled, no connection to the apiserver.") klog.V(2).Infof("Certificate rotation is not enabled, no connection to the apiserver.")
return return
} }
@ -388,11 +388,11 @@ func getCurrentCertificateOrBootstrap(
return &bootstrapCert, true, nil return &bootstrapCert, true, nil
} }
func (m *manager) getClient() (certificatesclient.CertificateSigningRequestInterface, error) { func (m *manager) getClientset() (clientset.Interface, error) {
current := m.Current() current := m.Current()
m.clientAccessLock.Lock() m.clientAccessLock.Lock()
defer m.clientAccessLock.Unlock() defer m.clientAccessLock.Unlock()
return m.clientFn(current) return m.clientsetFn(current)
} }
// RotateCerts is exposed for testing only and is not a part of the public interface. // RotateCerts is exposed for testing only and is not a part of the public interface.
@ -421,7 +421,7 @@ func (m *manager) rotateCerts() (bool, error) {
} }
// request the client each time // request the client each time
client, err := m.getClient() clientSet, err := m.getClientset()
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("Unable to load a client to request certificates: %v", err)) utilruntime.HandleError(fmt.Errorf("Unable to load a client to request certificates: %v", err))
if m.certificateRenewFailure != nil { if m.certificateRenewFailure != nil {
@ -432,7 +432,7 @@ func (m *manager) rotateCerts() (bool, error) {
// Call the Certificate Signing Request API to get a certificate for the // Call the Certificate Signing Request API to get a certificate for the
// new private key. // new private key.
req, err := csr.RequestCertificate(client, csrPEM, "", m.signerName, m.usages, privateKey) reqName, reqUID, err := csr.RequestCertificate(clientSet, csrPEM, "", m.signerName, m.usages, privateKey)
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err)) utilruntime.HandleError(fmt.Errorf("Failed while requesting a signed certificate from the master: %v", err))
if m.certificateRenewFailure != nil { if m.certificateRenewFailure != nil {
@ -449,7 +449,7 @@ func (m *manager) rotateCerts() (bool, error) {
// Wait for the certificate to be signed. This interface and internal timout // Wait for the certificate to be signed. This interface and internal timout
// is a remainder after the old design using raw watch wrapped with backoff. // is a remainder after the old design using raw watch wrapped with backoff.
crtPEM, err := csr.WaitForCertificate(ctx, client, req) crtPEM, err := csr.WaitForCertificate(ctx, clientSet, reqName, reqUID)
if err != nil { if err != nil {
utilruntime.HandleError(fmt.Errorf("certificate request was not signed: %v", err)) utilruntime.HandleError(fmt.Errorf("certificate request was not signed: %v", err))
if m.certificateRenewFailure != nil { if m.certificateRenewFailure != nil {

View File

@ -18,7 +18,6 @@ package certificate
import ( import (
"bytes" "bytes"
"context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
@ -28,12 +27,19 @@ import (
"testing" "testing"
"time" "time"
certificates "k8s.io/api/certificates/v1beta1" certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
watch "k8s.io/apimachinery/pkg/watch" watch "k8s.io/apimachinery/pkg/watch"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1"
clienttesting "k8s.io/client-go/testing"
) )
var storeCertData = newCertificateData(`-----BEGIN CERTIFICATE----- var storeCertData = newCertificateData(`-----BEGIN CERTIFICATE-----
@ -219,7 +225,7 @@ func TestNewManagerNoRotation(t *testing.T) {
} }
if _, err := NewManager(&Config{ if _, err := NewManager(&Config{
Template: &x509.CertificateRequest{}, Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{}, Usages: []certificatesv1.KeyUsage{},
CertificateStore: store, CertificateStore: store,
}); err != nil { }); err != nil {
t.Fatalf("Failed to initialize the certificate manager: %v", err) t.Fatalf("Failed to initialize the certificate manager: %v", err)
@ -271,7 +277,7 @@ func TestSetRotationDeadline(t *testing.T) {
}, },
}, },
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
usages: []certificates.KeyUsage{}, usages: []certificatesv1.KeyUsage{},
now: func() time.Time { return now }, now: func() time.Time { return now },
} }
jitteryDuration = func(float64) time.Duration { return time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.7) } jitteryDuration = func(float64) time.Duration { return time.Duration(float64(tc.notAfter.Sub(tc.notBefore)) * 0.7) }
@ -465,9 +471,9 @@ func TestRotateCertCreateCSRError(t *testing.T) {
}, },
}, },
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
usages: []certificates.KeyUsage{}, usages: []certificatesv1.KeyUsage{},
clientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { clientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return fakeClient{failureType: createError}, nil return newClientset(fakeClient{failureType: createError}), nil
}, },
now: func() time.Time { return now }, now: func() time.Time { return now },
} }
@ -489,9 +495,9 @@ func TestRotateCertWaitingForResultError(t *testing.T) {
}, },
}, },
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
usages: []certificates.KeyUsage{}, usages: []certificatesv1.KeyUsage{},
clientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { clientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return fakeClient{failureType: watchError}, nil return newClientset(fakeClient{failureType: watchError}), nil
}, },
now: func() time.Time { return now }, now: func() time.Time { return now },
} }
@ -511,7 +517,7 @@ func TestNewManagerBootstrap(t *testing.T) {
var cm Manager var cm Manager
cm, err := NewManager(&Config{ cm, err := NewManager(&Config{
Template: &x509.CertificateRequest{}, Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{}, Usages: []certificatesv1.KeyUsage{},
CertificateStore: store, CertificateStore: store,
BootstrapCertificatePEM: bootstrapCertData.certificatePEM, BootstrapCertificatePEM: bootstrapCertData.certificatePEM,
BootstrapKeyPEM: bootstrapCertData.keyPEM, BootstrapKeyPEM: bootstrapCertData.keyPEM,
@ -548,7 +554,7 @@ func TestNewManagerNoBootstrap(t *testing.T) {
cm, err := NewManager(&Config{ cm, err := NewManager(&Config{
Template: &x509.CertificateRequest{}, Template: &x509.CertificateRequest{},
Usages: []certificates.KeyUsage{}, Usages: []certificatesv1.KeyUsage{},
CertificateStore: store, CertificateStore: store,
BootstrapCertificatePEM: bootstrapCertData.certificatePEM, BootstrapCertificatePEM: bootstrapCertData.certificatePEM,
BootstrapKeyPEM: bootstrapCertData.keyPEM, BootstrapKeyPEM: bootstrapCertData.keyPEM,
@ -644,6 +650,8 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
storeCert *certificateData storeCert *certificateData
bootstrapCert *certificateData bootstrapCert *certificateData
apiCert *certificateData apiCert *certificateData
noV1 bool
noV1beta1 bool
expectedCertBeforeStart *certificateData expectedCertBeforeStart *certificateData
expectedCertAfterStart *certificateData expectedCertAfterStart *certificateData
}{ }{
@ -655,6 +663,24 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
expectedCertBeforeStart: nilCertificate, expectedCertBeforeStart: nilCertificate,
expectedCertAfterStart: apiServerCertData, expectedCertAfterStart: apiServerCertData,
}, },
{
description: "No current certificate, no bootstrap certificate, no v1 API",
storeCert: nilCertificate,
bootstrapCert: nilCertificate,
apiCert: apiServerCertData,
expectedCertBeforeStart: nilCertificate,
expectedCertAfterStart: apiServerCertData,
noV1: true,
},
{
description: "No current certificate, no bootstrap certificate, no v1beta1 API",
storeCert: nilCertificate,
bootstrapCert: nilCertificate,
apiCert: apiServerCertData,
expectedCertBeforeStart: nilCertificate,
expectedCertAfterStart: apiServerCertData,
noV1beta1: true,
},
{ {
description: "No current certificate, bootstrap certificate", description: "No current certificate, bootstrap certificate",
storeCert: nilCertificate, storeCert: nilCertificate,
@ -702,18 +728,21 @@ func TestInitializeCertificateSigningRequestClient(t *testing.T) {
CommonName: "system:node:fake-node-name", CommonName: "system:node:fake-node-name",
}, },
}, },
Usages: []certificates.KeyUsage{ SignerName: certificatesv1.KubeAPIServerClientSignerName,
certificates.UsageDigitalSignature, Usages: []certificatesv1.KeyUsage{
certificates.UsageKeyEncipherment, certificatesv1.UsageDigitalSignature,
certificates.UsageClientAuth, certificatesv1.UsageKeyEncipherment,
certificatesv1.UsageClientAuth,
}, },
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { ClientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return &fakeClient{ return newClientset(fakeClient{
noV1: tc.noV1,
noV1beta1: tc.noV1beta1,
certificatePEM: tc.apiCert.certificatePEM, certificatePEM: tc.apiCert.certificatePEM,
}, nil }), nil
}, },
}) })
if err != nil { if err != nil {
@ -814,18 +843,18 @@ func TestInitializeOtherRESTClients(t *testing.T) {
CommonName: "system:node:fake-node-name", CommonName: "system:node:fake-node-name",
}, },
}, },
Usages: []certificates.KeyUsage{ Usages: []certificatesv1.KeyUsage{
certificates.UsageDigitalSignature, certificatesv1.UsageDigitalSignature,
certificates.UsageKeyEncipherment, certificatesv1.UsageKeyEncipherment,
certificates.UsageClientAuth, certificatesv1.UsageClientAuth,
}, },
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { ClientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return &fakeClient{ return newClientset(fakeClient{
certificatePEM: tc.apiCert.certificatePEM, certificatePEM: tc.apiCert.certificatePEM,
}, nil }), nil
}, },
}) })
if err != nil { if err != nil {
@ -959,20 +988,20 @@ func TestServerHealth(t *testing.T) {
CommonName: "system:node:fake-node-name", CommonName: "system:node:fake-node-name",
}, },
}, },
Usages: []certificates.KeyUsage{ Usages: []certificatesv1.KeyUsage{
certificates.UsageDigitalSignature, certificatesv1.UsageDigitalSignature,
certificates.UsageKeyEncipherment, certificatesv1.UsageKeyEncipherment,
certificates.UsageClientAuth, certificatesv1.UsageClientAuth,
}, },
CertificateStore: certificateStore, CertificateStore: certificateStore,
BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM, BootstrapCertificatePEM: tc.bootstrapCert.certificatePEM,
BootstrapKeyPEM: tc.bootstrapCert.keyPEM, BootstrapKeyPEM: tc.bootstrapCert.keyPEM,
ClientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { ClientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return &fakeClient{ return newClientset(fakeClient{
certificatePEM: tc.apiCert.certificatePEM, certificatePEM: tc.apiCert.certificatePEM,
failureType: tc.failureType, failureType: tc.failureType,
err: tc.clientErr, err: tc.clientErr,
}, nil }), nil
}, },
}) })
if err != nil { if err != nil {
@ -1022,10 +1051,10 @@ func TestRotationLogsDuration(t *testing.T) {
}, },
certStore: &fakeStore{cert: expiredStoreCertData.certificate}, certStore: &fakeStore{cert: expiredStoreCertData.certificate},
getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} }, getTemplate: func() *x509.CertificateRequest { return &x509.CertificateRequest{} },
clientFn: func(_ *tls.Certificate) (certificatesclient.CertificateSigningRequestInterface, error) { clientsetFn: func(_ *tls.Certificate) (clientset.Interface, error) {
return &fakeClient{ return newClientset(fakeClient{
certificatePEM: apiServerCertData.certificatePEM, certificatePEM: apiServerCertData.certificatePEM,
}, nil }), nil
}, },
certificateRotation: &h, certificateRotation: &h,
now: func() time.Time { return now }, now: func() time.Time { return now },
@ -1053,53 +1082,101 @@ const (
) )
type fakeClient struct { type fakeClient struct {
noV1 bool
noV1beta1 bool
certificatesclient.CertificateSigningRequestInterface certificatesclient.CertificateSigningRequestInterface
failureType fakeClientFailureType failureType fakeClientFailureType
certificatePEM []byte certificatePEM []byte
err error err error
} }
func (c fakeClient) List(_ context.Context, opts v1.ListOptions) (*certificates.CertificateSigningRequestList, error) { func newClientset(opts fakeClient) *fake.Clientset {
if c.failureType == watchError { f := fake.NewSimpleClientset()
if c.err != nil { switch opts.failureType {
return nil, c.err case createError:
f.PrependReactor("create", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
if opts.err != nil {
return true, nil, opts.err
} }
return nil, fmt.Errorf("Watch error") return true, nil, fmt.Errorf("create error")
})
case watchError:
f.PrependReactor("list", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
if opts.err != nil {
return true, nil, opts.err
} }
csrReply := certificates.CertificateSigningRequestList{ return true, nil, fmt.Errorf("watch error")
Items: []certificates.CertificateSigningRequest{ })
{ObjectMeta: v1.ObjectMeta{UID: "fake-uid"}}, f.PrependWatchReactor("certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
}, if opts.err != nil {
return true, nil, opts.err
} }
return &csrReply, nil return true, nil, fmt.Errorf("watch error")
})
default:
f.PrependReactor("create", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
switch action.GetResource().Version {
case "v1":
if opts.noV1 {
return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
} }
return true, &certificatesv1.CertificateSigningRequest{ObjectMeta: metav1.ObjectMeta{UID: "fake-uid"}}, nil
func (c fakeClient) Create(context.Context, *certificates.CertificateSigningRequest, v1.CreateOptions) (*certificates.CertificateSigningRequest, error) { case "v1beta1":
if c.failureType == createError { if opts.noV1beta1 {
if c.err != nil { return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
return nil, c.err
} }
return nil, fmt.Errorf("create error") return true, &certificatesv1beta1.CertificateSigningRequest{ObjectMeta: metav1.ObjectMeta{UID: "fake-uid"}}, nil
default:
return false, nil, nil
} }
csrReply := certificates.CertificateSigningRequest{} })
csrReply.UID = "fake-uid" f.PrependReactor("list", "certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return &csrReply, nil switch action.GetResource().Version {
case "v1":
if opts.noV1 {
return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
} }
return true, &certificatesv1.CertificateSigningRequestList{Items: []certificatesv1.CertificateSigningRequest{{ObjectMeta: v1.ObjectMeta{UID: "fake-uid"}}}}, nil
func (c fakeClient) Watch(_ context.Context, opts v1.ListOptions) (watch.Interface, error) { case "v1beta1":
if c.failureType == watchError { if opts.noV1beta1 {
if c.err != nil { return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
return nil, c.err
} }
return nil, fmt.Errorf("watch error") return true, &certificatesv1beta1.CertificateSigningRequestList{Items: []certificatesv1beta1.CertificateSigningRequest{{ObjectMeta: v1.ObjectMeta{UID: "fake-uid"}}}}, nil
default:
return false, nil, nil
} }
return &fakeWatch{ })
failureType: c.failureType, f.PrependWatchReactor("certificatesigningrequests", func(action clienttesting.Action) (handled bool, ret watch.Interface, err error) {
certificatePEM: c.certificatePEM, switch action.GetResource().Version {
case "v1":
if opts.noV1 {
return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
}
return true, &fakeWatch{
version: action.GetResource().Version,
failureType: opts.failureType,
certificatePEM: opts.certificatePEM,
}, nil }, nil
case "v1beta1":
if opts.noV1beta1 {
return true, nil, apierrors.NewNotFound(certificatesv1.Resource("certificatesigningrequests"), "")
}
return true, &fakeWatch{
version: action.GetResource().Version,
failureType: opts.failureType,
certificatePEM: opts.certificatePEM,
}, nil
default:
return false, nil, nil
}
})
}
return f
} }
type fakeWatch struct { type fakeWatch struct {
version string
failureType fakeClientFailureType failureType fakeClientFailureType
certificatePEM []byte certificatePEM []byte
} }
@ -1108,31 +1185,58 @@ func (w *fakeWatch) Stop() {
} }
func (w *fakeWatch) ResultChan() <-chan watch.Event { func (w *fakeWatch) ResultChan() <-chan watch.Event {
var condition certificates.CertificateSigningRequestCondition var csr runtime.Object
switch w.version {
case "v1":
var condition certificatesv1.CertificateSigningRequestCondition
if w.failureType == certificateSigningRequestDenied { if w.failureType == certificateSigningRequestDenied {
condition = certificates.CertificateSigningRequestCondition{ condition = certificatesv1.CertificateSigningRequestCondition{
Type: certificates.CertificateDenied, Type: certificatesv1.CertificateDenied,
} }
} else { } else {
condition = certificates.CertificateSigningRequestCondition{ condition = certificatesv1.CertificateSigningRequestCondition{
Type: certificates.CertificateApproved, Type: certificatesv1.CertificateApproved,
} }
} }
csr := certificates.CertificateSigningRequest{ csr = &certificatesv1.CertificateSigningRequest{
Status: certificates.CertificateSigningRequestStatus{ ObjectMeta: metav1.ObjectMeta{UID: "fake-uid"},
Conditions: []certificates.CertificateSigningRequestCondition{ Status: certificatesv1.CertificateSigningRequestStatus{
Conditions: []certificatesv1.CertificateSigningRequestCondition{
condition, condition,
}, },
Certificate: []byte(w.certificatePEM), Certificate: []byte(w.certificatePEM),
}, },
} }
csr.UID = "fake-uid"
case "v1beta1":
var condition certificatesv1beta1.CertificateSigningRequestCondition
if w.failureType == certificateSigningRequestDenied {
condition = certificatesv1beta1.CertificateSigningRequestCondition{
Type: certificatesv1beta1.CertificateDenied,
}
} else {
condition = certificatesv1beta1.CertificateSigningRequestCondition{
Type: certificatesv1beta1.CertificateApproved,
}
}
csr = &certificatesv1beta1.CertificateSigningRequest{
ObjectMeta: metav1.ObjectMeta{UID: "fake-uid"},
Status: certificatesv1beta1.CertificateSigningRequestStatus{
Conditions: []certificatesv1beta1.CertificateSigningRequestCondition{
condition,
},
Certificate: []byte(w.certificatePEM),
},
}
}
c := make(chan watch.Event, 1) c := make(chan watch.Event, 1)
c <- watch.Event{ c <- watch.Event{
Type: watch.Added, Type: watch.Added,
Object: &csr, Object: csr,
} }
return c return c
} }

View File

@ -27,14 +27,17 @@ import (
"k8s.io/klog/v2" "k8s.io/klog/v2"
certificates "k8s.io/api/certificates/v1beta1" certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
certificatesclient "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch" watchtools "k8s.io/client-go/tools/watch"
certutil "k8s.io/client-go/util/cert" certutil "k8s.io/client-go/util/cert"
@ -42,95 +45,239 @@ import (
// RequestCertificate will either use an existing (if this process has run // RequestCertificate will either use an existing (if this process has run
// before but not to completion) or create a certificate signing request using the // before but not to completion) or create a certificate signing request using the
// PEM encoded CSR and send it to API server, then it will watch the object's // PEM encoded CSR and send it to API server.
// status, once approved by API server, it will return the API server's issued func RequestCertificate(client clientset.Interface, csrData []byte, name string, signerName string, usages []certificatesv1.KeyUsage, privateKey interface{}) (reqName string, reqUID types.UID, err error) {
// certificate (pem-encoded). If there is any errors, or the watch timeouts, it csr := &certificatesv1.CertificateSigningRequest{
// will return an error.
func RequestCertificate(client certificatesclient.CertificateSigningRequestInterface, csrData []byte, name string, signerName string, usages []certificates.KeyUsage, privateKey interface{}) (req *certificates.CertificateSigningRequest, err error) {
csr := &certificates.CertificateSigningRequest{
// Username, UID, Groups will be injected by API server. // Username, UID, Groups will be injected by API server.
TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"}, TypeMeta: metav1.TypeMeta{Kind: "CertificateSigningRequest"},
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
}, },
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificatesv1.CertificateSigningRequestSpec{
Request: csrData, Request: csrData,
Usages: usages, Usages: usages,
SignerName: &signerName, SignerName: signerName,
}, },
} }
if len(csr.Name) == 0 { if len(csr.Name) == 0 {
csr.GenerateName = "csr-" csr.GenerateName = "csr-"
} }
req, err = client.Create(context.TODO(), csr, metav1.CreateOptions{}) reqName, reqUID, err = create(client, csr)
switch { switch {
case err == nil: case err == nil:
return reqName, reqUID, err
case errors.IsAlreadyExists(err) && len(name) > 0: case errors.IsAlreadyExists(err) && len(name) > 0:
klog.Infof("csr for this node already exists, reusing") klog.Infof("csr for this node already exists, reusing")
req, err = client.Get(context.TODO(), name, metav1.GetOptions{}) req, err := get(client, name)
if err != nil { if err != nil {
return nil, formatError("cannot retrieve certificate signing request: %v", err) return "", "", formatError("cannot retrieve certificate signing request: %v", err)
} }
if err := ensureCompatible(req, csr, privateKey); err != nil { if err := ensureCompatible(req, csr, privateKey); err != nil {
return nil, fmt.Errorf("retrieved csr is not compatible: %v", err) return "", "", fmt.Errorf("retrieved csr is not compatible: %v", err)
} }
klog.Infof("csr for this node is still valid") klog.Infof("csr for this node is still valid")
return req.Name, req.UID, nil
default: default:
return nil, formatError("cannot create certificate signing request: %v", err) return "", "", formatError("cannot create certificate signing request: %v", err)
} }
return req, nil }
func get(client clientset.Interface, name string) (*certificatesv1.CertificateSigningRequest, error) {
v1req, v1err := client.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
if v1err == nil || !apierrors.IsNotFound(v1err) {
return v1req, v1err
}
v1beta1req, v1beta1err := client.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), name, metav1.GetOptions{})
if v1beta1err != nil {
return nil, v1beta1err
}
v1req = &certificatesv1.CertificateSigningRequest{
ObjectMeta: v1beta1req.ObjectMeta,
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: v1beta1req.Spec.Request,
},
}
if v1beta1req.Spec.SignerName != nil {
v1req.Spec.SignerName = *v1beta1req.Spec.SignerName
}
for _, usage := range v1beta1req.Spec.Usages {
v1req.Spec.Usages = append(v1req.Spec.Usages, certificatesv1.KeyUsage(usage))
}
return v1req, nil
}
func create(client clientset.Interface, csr *certificatesv1.CertificateSigningRequest) (reqName string, reqUID types.UID, err error) {
// only attempt a create via v1 if we specified signerName and usages and are not using the legacy unknown signerName
if len(csr.Spec.Usages) > 0 && len(csr.Spec.SignerName) > 0 && csr.Spec.SignerName != "kubernetes.io/legacy-unknown" {
v1req, v1err := client.CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csr, metav1.CreateOptions{})
switch {
case v1err != nil && apierrors.IsNotFound(v1err):
// v1 CSR API was not found, continue to try v1beta1
case v1err != nil:
// other creation error
return "", "", v1err
default:
// success
return v1req.Name, v1req.UID, v1err
}
}
// convert relevant bits to v1beta1
v1beta1csr := &certificatesv1beta1.CertificateSigningRequest{
ObjectMeta: csr.ObjectMeta,
Spec: certificatesv1beta1.CertificateSigningRequestSpec{
SignerName: &csr.Spec.SignerName,
Request: csr.Spec.Request,
},
}
for _, usage := range csr.Spec.Usages {
v1beta1csr.Spec.Usages = append(v1beta1csr.Spec.Usages, certificatesv1beta1.KeyUsage(usage))
}
// create v1beta1
v1beta1req, v1beta1err := client.CertificatesV1beta1().CertificateSigningRequests().Create(context.TODO(), v1beta1csr, metav1.CreateOptions{})
if v1beta1err != nil {
return "", "", v1beta1err
}
return v1beta1req.Name, v1beta1req.UID, nil
} }
// WaitForCertificate waits for a certificate to be issued until timeout, or returns an error. // WaitForCertificate waits for a certificate to be issued until timeout, or returns an error.
func WaitForCertificate(ctx context.Context, client certificatesclient.CertificateSigningRequestInterface, req *certificates.CertificateSigningRequest) (certData []byte, err error) { func WaitForCertificate(ctx context.Context, client clientset.Interface, reqName string, reqUID types.UID) (certData []byte, err error) {
fieldSelector := fields.OneTermEqualSelector("metadata.name", req.Name).String() fieldSelector := fields.OneTermEqualSelector("metadata.name", reqName).String()
lw := &cache.ListWatch{
var lw *cache.ListWatch
var obj runtime.Object
for {
// see if the v1 API is available
if _, err := client.CertificatesV1().CertificateSigningRequests().List(ctx, metav1.ListOptions{FieldSelector: fieldSelector}); err == nil {
// watch v1 objects
obj = &certificatesv1.CertificateSigningRequest{}
lw = &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = fieldSelector options.FieldSelector = fieldSelector
return client.List(context.TODO(), options) return client.CertificatesV1().CertificateSigningRequests().List(ctx, options)
}, },
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = fieldSelector options.FieldSelector = fieldSelector
return client.Watch(context.TODO(), options) return client.CertificatesV1().CertificateSigningRequests().Watch(ctx, options)
}, },
} }
event, err := watchtools.UntilWithSync( break
} else {
klog.V(2).Infof("error fetching v1 certificate signing request: %v", err)
}
// return if we've timed out
if err := ctx.Err(); err != nil {
return nil, wait.ErrWaitTimeout
}
// see if the v1beta1 API is available
if _, err := client.CertificatesV1beta1().CertificateSigningRequests().List(ctx, metav1.ListOptions{FieldSelector: fieldSelector}); err == nil {
// watch v1beta1 objects
obj = &certificatesv1beta1.CertificateSigningRequest{}
lw = &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1beta1().CertificateSigningRequests().List(ctx, options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
options.FieldSelector = fieldSelector
return client.CertificatesV1beta1().CertificateSigningRequests().Watch(ctx, options)
},
}
break
} else {
klog.V(2).Infof("error fetching v1beta1 certificate signing request: %v", err)
}
// return if we've timed out
if err := ctx.Err(); err != nil {
return nil, wait.ErrWaitTimeout
}
// wait and try again
time.Sleep(time.Second)
}
var issuedCertificate []byte
_, err = watchtools.UntilWithSync(
ctx, ctx,
lw, lw,
&certificates.CertificateSigningRequest{}, obj,
nil, nil,
func(event watch.Event) (bool, error) { func(event watch.Event) (bool, error) {
switch event.Type { switch event.Type {
case watch.Modified, watch.Added: case watch.Modified, watch.Added:
case watch.Deleted: case watch.Deleted:
return false, fmt.Errorf("csr %q was deleted", req.Name) return false, fmt.Errorf("csr %q was deleted", reqName)
default: default:
return false, nil return false, nil
} }
csr := event.Object.(*certificates.CertificateSigningRequest)
if csr.UID != req.UID { switch csr := event.Object.(type) {
case *certificatesv1.CertificateSigningRequest:
if csr.UID != reqUID {
return false, fmt.Errorf("csr %q changed UIDs", csr.Name) return false, fmt.Errorf("csr %q changed UIDs", csr.Name)
} }
approved := false approved := false
for _, c := range csr.Status.Conditions { for _, c := range csr.Status.Conditions {
if c.Type == certificates.CertificateDenied { if c.Type == certificatesv1.CertificateDenied {
return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message) return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message)
} }
if c.Type == certificates.CertificateFailed { if c.Type == certificatesv1.CertificateFailed {
return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message) return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message)
} }
if c.Type == certificates.CertificateApproved { if c.Type == certificatesv1.CertificateApproved {
approved = true approved = true
} }
} }
if approved { if approved {
if len(csr.Status.Certificate) > 0 { if len(csr.Status.Certificate) > 0 {
klog.V(2).Infof("certificate signing request %s is issued", csr.Name) klog.V(2).Infof("certificate signing request %s is issued", csr.Name)
issuedCertificate = csr.Status.Certificate
return true, nil return true, nil
} }
klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name) klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name)
} }
case *certificatesv1beta1.CertificateSigningRequest:
if csr.UID != reqUID {
return false, fmt.Errorf("csr %q changed UIDs", csr.Name)
}
approved := false
for _, c := range csr.Status.Conditions {
if c.Type == certificatesv1beta1.CertificateDenied {
return false, fmt.Errorf("certificate signing request is denied, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1beta1.CertificateFailed {
return false, fmt.Errorf("certificate signing request failed, reason: %v, message: %v", c.Reason, c.Message)
}
if c.Type == certificatesv1beta1.CertificateApproved {
approved = true
}
}
if approved {
if len(csr.Status.Certificate) > 0 {
klog.V(2).Infof("certificate signing request %s is issued", csr.Name)
issuedCertificate = csr.Status.Certificate
return true, nil
}
klog.V(2).Infof("certificate signing request %s is approved, waiting to be issued", csr.Name)
}
default:
return false, fmt.Errorf("unexpected type received: %T", event.Object)
}
return false, nil return false, nil
}, },
) )
@ -141,24 +288,24 @@ func WaitForCertificate(ctx context.Context, client certificatesclient.Certifica
return nil, formatError("cannot watch on the certificate signing request: %v", err) return nil, formatError("cannot watch on the certificate signing request: %v", err)
} }
return event.Object.(*certificates.CertificateSigningRequest).Status.Certificate, nil return issuedCertificate, nil
} }
// ensureCompatible ensures that a CSR object is compatible with an original CSR // ensureCompatible ensures that a CSR object is compatible with an original CSR
func ensureCompatible(new, orig *certificates.CertificateSigningRequest, privateKey interface{}) error { func ensureCompatible(new, orig *certificatesv1.CertificateSigningRequest, privateKey interface{}) error {
newCSR, err := parseCSR(new) newCSR, err := parseCSR(new.Spec.Request)
if err != nil { if err != nil {
return fmt.Errorf("unable to parse new csr: %v", err) return fmt.Errorf("unable to parse new csr: %v", err)
} }
origCSR, err := parseCSR(orig) origCSR, err := parseCSR(orig.Spec.Request)
if err != nil { if err != nil {
return fmt.Errorf("unable to parse original csr: %v", err) return fmt.Errorf("unable to parse original csr: %v", err)
} }
if !reflect.DeepEqual(newCSR.Subject, origCSR.Subject) { if !reflect.DeepEqual(newCSR.Subject, origCSR.Subject) {
return fmt.Errorf("csr subjects differ: new: %#v, orig: %#v", newCSR.Subject, origCSR.Subject) return fmt.Errorf("csr subjects differ: new: %#v, orig: %#v", newCSR.Subject, origCSR.Subject)
} }
if new.Spec.SignerName != nil && orig.Spec.SignerName != nil && *new.Spec.SignerName != *orig.Spec.SignerName { if len(new.Spec.SignerName) > 0 && len(orig.Spec.SignerName) > 0 && new.Spec.SignerName != orig.Spec.SignerName {
return fmt.Errorf("csr signerNames differ: new %q, orig: %q", *new.Spec.SignerName, *orig.Spec.SignerName) return fmt.Errorf("csr signerNames differ: new %q, orig: %q", new.Spec.SignerName, orig.Spec.SignerName)
} }
signer, ok := privateKey.(crypto.Signer) signer, ok := privateKey.(crypto.Signer)
if !ok { if !ok {
@ -195,9 +342,9 @@ func formatError(format string, err error) error {
} }
// parseCSR extracts the CSR from the API object and decodes it. // parseCSR extracts the CSR from the API object and decodes it.
func parseCSR(obj *certificates.CertificateSigningRequest) (*x509.CertificateRequest, error) { func parseCSR(pemData []byte) (*x509.CertificateRequest, error) {
// extract PEM from request object // extract PEM from request object
block, _ := pem.Decode(obj.Spec.Request) block, _ := pem.Decode(pemData)
if block == nil || block.Type != "CERTIFICATE REQUEST" { if block == nil || block.Type != "CERTIFICATE REQUEST" {
return nil, fmt.Errorf("PEM block type must be CERTIFICATE REQUEST") return nil, fmt.Errorf("PEM block type must be CERTIFICATE REQUEST")
} }

View File

@ -25,8 +25,7 @@ import (
"encoding/pem" "encoding/pem"
"testing" "testing"
certificates "k8s.io/api/certificates/v1beta1" certificates "k8s.io/api/certificates/v1"
"k8s.io/utils/pointer"
) )
func TestEnsureCompatible(t *testing.T) { func TestEnsureCompatible(t *testing.T) {
@ -50,7 +49,7 @@ func TestEnsureCompatible(t *testing.T) {
orig: &certificates.CertificateSigningRequest{ orig: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/test"), SignerName: "example.com/test",
}, },
}, },
privateKey: privateKey, privateKey: privateKey,
@ -59,7 +58,7 @@ func TestEnsureCompatible(t *testing.T) {
new: &certificates.CertificateSigningRequest{ new: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/test"), SignerName: "example.com/test",
}, },
}, },
orig: &certificates.CertificateSigningRequest{ orig: &certificates.CertificateSigningRequest{
@ -73,13 +72,13 @@ func TestEnsureCompatible(t *testing.T) {
new: &certificates.CertificateSigningRequest{ new: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/test"), SignerName: "example.com/test",
}, },
}, },
orig: &certificates.CertificateSigningRequest{ orig: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/test"), SignerName: "example.com/test",
}, },
}, },
privateKey: privateKey, privateKey: privateKey,
@ -88,13 +87,13 @@ func TestEnsureCompatible(t *testing.T) {
new: &certificates.CertificateSigningRequest{ new: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/test"), SignerName: "example.com/test",
}, },
}, },
orig: &certificates.CertificateSigningRequest{ orig: &certificates.CertificateSigningRequest{
Spec: certificates.CertificateSigningRequestSpec{ Spec: certificates.CertificateSigningRequestSpec{
Request: req, Request: req,
SignerName: pointer.StringPtr("example.com/not-test"), SignerName: "example.com/not-test",
}, },
}, },
privateKey: privateKey, privateKey: privateKey,