Add cert-manager interface function implementation

This commit will implement the interface functions required for cert-manager
certificate provider.

Signed-off-by: ArkaSaha30 <arkasaha30@gmail.com>
This commit is contained in:
ArkaSaha30 2025-05-14 12:33:39 +05:30
parent 9c3a04432c
commit 2bd50de5b5
No known key found for this signature in database
GPG Key ID: C5FF37943E5BC363
7 changed files with 451 additions and 22 deletions

View File

@ -0,0 +1,6 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned
spec:
selfSigned: {}

15
go.mod
View File

@ -5,6 +5,7 @@ go 1.24.0
toolchain go1.24.4
require (
github.com/cert-manager/cert-manager v1.17.2
github.com/go-logr/logr v1.4.3
github.com/stretchr/testify v1.10.0
go.etcd.io/etcd/api/v3 v3.6.1
@ -43,15 +44,15 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/emicklei/go-restful/v3 v3.12.1 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
@ -66,7 +67,7 @@ require (
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@ -97,9 +98,9 @@ require (
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
@ -120,7 +121,7 @@ require (
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/e2e-framework v0.6.0
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

23
go.sum
View File

@ -19,7 +19,6 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -34,8 +33,8 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -45,12 +44,10 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@ -102,8 +99,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@ -266,8 +261,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -318,8 +313,10 @@ sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytI
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM=
sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/gateway-api v1.1.0 h1:DsLDXCi6jR+Xz8/xd0Z1PYl2Pn0TyaFMOPPZIj4inDM=
sigs.k8s.io/gateway-api v1.1.0/go.mod h1:ZH4lHrL2sDi0FHZ9jjneb8kKnGzFWyrTya35sWUTrRs=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

View File

@ -0,0 +1,385 @@
package cert_manager
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"log"
"net"
"strings"
"time"
certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
interfaces "go.etcd.io/etcd-operator/pkg/certificate/interfaces"
)
const (
IssuerNameKey = "issuerName"
IssuerKindKey = "issuerKind"
)
type CertManagerProvider struct {
client.Client
}
var _ interfaces.Provider = (*CertManagerProvider)(nil)
func New(c client.Client) interfaces.Provider {
return &CertManagerProvider{
c,
}
}
func (cm *CertManagerProvider) EnsureCertificateSecret(ctx context.Context, secretName, namespace string,
cfg *interfaces.Config) error {
cmCertificate := &certmanagerv1.Certificate{}
err := cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, cmCertificate)
if err != nil {
if k8serrors.IsNotFound(err) {
valErr := cm.validateCertificateConfig(ctx, namespace, cfg)
if valErr != nil {
return valErr
}
err := cm.createCertificate(ctx, secretName, namespace, cfg)
if err != nil {
return err
}
} else {
return err
}
}
log.Printf("Valid certificate: %s present in namespace: %s, checking certificate status...", secretName, namespace)
err = cm.checkCertificateStatus(secretName, namespace, ctx)
if err != nil && !errors.Is(err, interfaces.ErrUnknown) {
for try := range interfaces.MaxRetries {
// Wait for the certificate to be in "Ready" state
// Reference: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers
log.Printf("Certificate Status: retry attempt %v, after %v, error: %v", try, interfaces.RetryInterval, err)
time.Sleep(interfaces.RetryInterval)
err = cm.checkCertificateStatus(secretName, namespace, ctx)
if err == nil {
break
}
}
if err != nil {
return err
}
} else {
return err
}
log.Printf("Certificate Status: %s ready in namespace: %s, validating associated secret...", secretName, namespace)
err = cm.ValidateCertificateSecret(ctx, secretName, namespace, cfg)
if err != nil {
if k8serrors.IsNotFound(err) {
return err
} else {
return fmt.Errorf("invalid certificate secret: %s present in namespace: %s, please delete and try again.\nError: %s",
secretName, namespace, err)
}
}
log.Printf("Valid certificate secret: %s already present in namespace: %s", secretName, namespace)
return nil
}
func (cm *CertManagerProvider) ValidateCertificateSecret(ctx context.Context, secretName, namespace string,
_ *interfaces.Config) error {
var err error
secret := &corev1.Secret{}
err = cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
if err != nil && k8serrors.IsNotFound(err) {
for try := range interfaces.MaxRetries {
// Wait for cert-manager reconciler to create the associated certificate secret
// Reference: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers
log.Printf("Valid certificate secret: retry attempt %v, after %v, error: %v", try+1, interfaces.RetryInterval, err)
time.Sleep(interfaces.RetryInterval)
err = cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
if err == nil {
break
}
}
if err != nil {
return err
}
} else {
return err
}
certificateData, exists := secret.Data["tls.crt"]
if !exists {
return interfaces.ErrTLSCert
}
decodeCertificatePem, _ := pem.Decode(certificateData)
if decodeCertificatePem == nil {
return interfaces.ErrDecodeCert
}
privateKeyData, keyExists := secret.Data["tls.key"]
if !keyExists {
return interfaces.ErrTLSKey
}
parseCert, err := x509.ParseCertificate(decodeCertificatePem.Bytes)
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
}
if parseCert.NotAfter.Before(time.Now()) {
return interfaces.ErrCertExpired
}
privateKey, err := parsePrivateKey(privateKeyData)
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
}
if checkKeyPairErr := checkKeyPair(parseCert, privateKey); checkKeyPairErr != nil {
return fmt.Errorf("private key does not match certificate: %w", checkKeyPairErr)
}
return nil
}
func (cm *CertManagerProvider) DeleteCertificateSecret(ctx context.Context, secretName, namespace string) error {
cmCertificate := &certmanagerv1.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
}
certStatusErr := cm.checkCertificateStatus(secretName, namespace, ctx)
if certStatusErr != nil {
log.Printf("Certificate associated not ready yet, try again later.")
return certStatusErr
}
dErr := cm.Delete(ctx, cmCertificate)
if dErr != nil {
return dErr
}
// By default, cert-manager Certificate deletion does not delete the associated secret.
// Existing secret will allow to services relying on that Certificate, so additionally delete it
// More info: https://cert-manager.io/docs/usage/certificate/#cleaning-up-secrets-when-certificates-are-deleted
cmSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
}
dSecretErr := cm.Delete(ctx, cmSecret)
if dSecretErr != nil {
if k8serrors.IsNotFound(dSecretErr) {
fmt.Println("Certificate secret not found, maybe already deleted")
} else {
return dSecretErr
}
}
return nil
}
// RevokeCertificate is not supported, certificates can only be deleted which is handled by DeleteCertificateSecret
// as per official documentation: https://cert-manager.io/docs/usage/certificate/#inner-workings-diagram-for-developers
func (cm *CertManagerProvider) RevokeCertificate(ctx context.Context, secretName string, namespace string) error {
return nil
}
func (cm *CertManagerProvider) GetCertificateConfig(ctx context.Context,
secretName, namespace string) (*interfaces.Config, error) {
cmCertificate := &certmanagerv1.Certificate{}
err := cm.Get(ctx, client.ObjectKey{Name: secretName, Namespace: namespace}, cmCertificate)
if err != nil {
return nil, fmt.Errorf("failed to get certificate: %w", err)
}
var ipAddresses []net.IP
if len(cmCertificate.Spec.IPAddresses) != 0 {
ipAddresses = make([]net.IP, len(cmCertificate.Spec.IPAddresses))
} else {
ipAddresses = nil
}
cfg := &interfaces.Config{
CommonName: cmCertificate.Spec.CommonName,
Organization: cmCertificate.Spec.Subject.Organizations,
AltNames: interfaces.AltNames{
DNSNames: cmCertificate.Spec.DNSNames,
IPs: ipAddresses,
},
ValidityDuration: cmCertificate.Spec.Duration.Duration,
ExtraConfig: map[string]any{
IssuerNameKey: cmCertificate.Spec.IssuerRef.Name,
IssuerKindKey: cmCertificate.Spec.IssuerRef.Kind,
},
}
return cfg, nil
}
// checkCertificateStatus returns the current status of the certificate creation
func (cm *CertManagerProvider) checkCertificateStatus(certificateName, namespace string, ctx context.Context) error {
cmCertificate := &certmanagerv1.Certificate{}
err := cm.Get(ctx, client.ObjectKey{Name: certificateName, Namespace: namespace}, cmCertificate)
if err != nil {
return err
}
cmStatus := cmCertificate.Status.Conditions
for _, condition := range cmStatus {
switch condition.Type {
case certmanagerv1.CertificateConditionReady:
log.Printf("Certificate Ready: %v (Reason: %s, Message: %s)\n",
condition.Status, condition.Reason, condition.Message)
return nil
case certmanagerv1.CertificateConditionIssuing:
return fmt.Errorf("certificate Issuing: %v (Reason: %s, Message: %s), error: %w \n",
condition.Status, condition.Reason, condition.Message, interfaces.ErrPending)
default:
return fmt.Errorf("certificate status unknown: %v (Reason: %s, Message: %s), error: %w\n",
condition.Status, condition.Reason, condition.Message, interfaces.ErrUnknown)
}
}
return nil
}
// checkIssuerExists checks for if the provided issuer is present in the namespace/cluster
func (cm *CertManagerProvider) checkIssuerExists(issuerName, issuerKind, namespace string, ctx context.Context) error {
switch issuerKind {
case "Issuer":
issuer := &certmanagerv1.Issuer{}
err := cm.Get(ctx, client.ObjectKey{Name: issuerName, Namespace: namespace}, issuer)
if k8serrors.IsNotFound(err) {
return fmt.Errorf("issuer %s not found in namespace %s", issuerName, namespace)
}
case "ClusterIssuer":
clusterIssuer := &certmanagerv1.ClusterIssuer{}
err := cm.Get(ctx, client.ObjectKey{Name: issuerName}, clusterIssuer)
if k8serrors.IsNotFound(err) {
return fmt.Errorf("clusterIssuer %s not found", issuerName)
}
default:
return fmt.Errorf("unsupported issuer kind: %s", issuerKind)
}
return nil
}
// validateCertificateConfig checks if the config passed is valid
func (cm *CertManagerProvider) validateCertificateConfig(ctx context.Context, namespace string,
cfg *interfaces.Config) error {
issuerName, isValid := cfg.ExtraConfig[IssuerNameKey].(string)
if !isValid {
return fmt.Errorf("value for %s not correctly provided, try again", IssuerNameKey)
}
issuerKind, isValid := cfg.ExtraConfig[IssuerKindKey].(string)
if !isValid {
return fmt.Errorf("value for %s not correctly provided, try again", IssuerKindKey)
}
checkIssuerExist := cm.checkIssuerExists(issuerName, issuerKind, namespace, ctx)
if checkIssuerExist != nil {
return checkIssuerExist
}
return nil
}
// createCertificate creates a cert-manager Certificate resource in the specified namespace.
// DNSNames and IPAddresses if not user-defined, will be set to default value in runtime:
// fmt.Sprintf("%s-%d.%s.%s.svc.cluster.local", ec.Name, index, ec.Name, ec.Namespace)
// returns an error if the Certificate resource cannot be created.
func (cm *CertManagerProvider) createCertificate(ctx context.Context, secretName, namespace string,
cfg *interfaces.Config) error {
issuerName, isValid := cfg.ExtraConfig[IssuerNameKey].(string)
if !isValid {
return fmt.Errorf("value for %s not correctly provided, try again", IssuerNameKey)
}
issuerKind, isValid := cfg.ExtraConfig[IssuerKindKey].(string)
if !isValid {
return fmt.Errorf("value for %s not correctly provided, try again", IssuerKindKey)
}
certificateResource := &certmanagerv1.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Spec: certmanagerv1.CertificateSpec{
CommonName: cfg.CommonName,
Subject: &certmanagerv1.X509Subject{
Organizations: cfg.Organization,
},
SecretName: secretName,
DNSNames: cfg.AltNames.DNSNames,
IPAddresses: strings.Fields(strings.Trim(fmt.Sprint(cfg.AltNames.IPs), "[]")),
IssuerRef: cmmeta.ObjectReference{
Name: issuerName,
Kind: issuerKind,
},
Duration: &metav1.Duration{Duration: cfg.ValidityDuration},
},
}
return cm.Create(ctx, certificateResource)
}
// parsePrivateKey parses the private key from the PEM-encoded data.
func parsePrivateKey(privateKeyData []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(privateKeyData)
if block == nil {
return nil, errors.New("failed to decode private key: invalid PEM")
}
// Parse the private key from the PEM block
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
// Parse the private key in another format (e.g., RSA)
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
}
return privateKey, nil
}
// checkKeyPair checks if the private key matches the certificate by validating the public key
func checkKeyPair(cert *x509.Certificate, privateKey crypto.PrivateKey) error {
switch key := privateKey.(type) {
case *rsa.PrivateKey:
pub, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok || !key.PublicKey.Equal(pub) {
return interfaces.ErrRSAKeyPair
}
case *ecdsa.PrivateKey:
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok || !key.PublicKey.Equal(pub) {
return interfaces.ErrECDSAKeyPair
}
case *ed25519.PrivateKey:
pub, ok := cert.PublicKey.(ed25519.PublicKey)
if !ok || !bytes.Equal(key.Public().(ed25519.PublicKey), pub) {
return interfaces.ErrED25519KeyPair
}
default:
return fmt.Errorf("unsupported private key type: %T", key)
}
return nil
}

View File

@ -3,6 +3,9 @@ package certificate
import (
"fmt"
"sigs.k8s.io/controller-runtime/pkg/client"
certManager "go.etcd.io/etcd-operator/pkg/certificate/cert_manager"
certInterface "go.etcd.io/etcd-operator/pkg/certificate/interfaces"
)
@ -14,12 +17,12 @@ const (
// add more ...
)
func NewProvider(pt ProviderType) (certInterface.Provider, error) {
func NewProvider(pt ProviderType, c client.Client) (certInterface.Provider, error) {
switch pt {
case Auto:
return nil, nil // change me later
case CertManager:
return nil, nil // change me later
return certManager.New(c), nil
}
return nil, fmt.Errorf("unknown provider type: %s", pt)

View File

@ -2,10 +2,47 @@ package certificate
import (
"context"
"errors"
"net"
"time"
)
var (
// ErrPending is returned when the Certificate is not in "Ready" state
ErrPending = errors.New("certificate creation pending")
// ErrUnknown is returned when the Certificate status does not match the provider defined states
ErrUnknown = errors.New("certificate status unknown")
// ErrTLSKey is returned when private key not found in Certificate secret
ErrTLSKey = errors.New("private key not found in secret")
// ErrTLSCert is returned when private key certificate not found in Certificate secret
ErrTLSCert = errors.New("certificate not found in secret")
// ErrDecodeCert is returned when failed to decode PEM block of tls.crt of Certificate secret
ErrDecodeCert = errors.New("failed to decode PEM block")
// ErrCertExpired is returned when certificate has expired
ErrCertExpired = errors.New("certificate has expired")
// ErrRSAKeyPair is returned when private key(RSA) does not match the public key in the Certificate secret
ErrRSAKeyPair = errors.New("private key(RSA) does not match the public key in the certificate")
// ErrECDSAKeyPair is returned when private key(ECDSA) does not match the public key in the Certificate secret
ErrECDSAKeyPair = errors.New("private key(ECDSA) does not match the public key in the certificate")
// ErrED25519KeyPair is returned when private key(ED25519) does not match the public key in the Certificate secret
ErrED25519KeyPair = errors.New("private key(ED25519) does not match the public key in the certificate")
)
const (
// MaxRetries is the maximum number of retry attempts for EnsureCertificateSecret, ValidateCertificateSecret
// with a delay of RetryInterval between consecutive retries
MaxRetries = 36
RetryInterval = 5 * time.Second
)
// AltNames contains the domain names and IP addresses that will be added
// to the x509 certificate SubAltNames fields. The values will be passed
// directly to the x509.Certificate object.