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:
parent
9c3a04432c
commit
2bd50de5b5
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: cert-manager.io/v1
|
||||
kind: ClusterIssuer
|
||||
metadata:
|
||||
name: selfsigned
|
||||
spec:
|
||||
selfSigned: {}
|
||||
15
go.mod
15
go.mod
|
|
@ -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
23
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue