164 lines
4.1 KiB
Go
164 lines
4.1 KiB
Go
package approver
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
|
|
pve "git.giftfish.de/ston1th/pve-go"
|
|
|
|
"github.com/go-logr/logr"
|
|
certificatesv1 "k8s.io/api/certificates/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
typedcertificatesv1 "k8s.io/client-go/kubernetes/typed/certificates/v1"
|
|
ctrl "sigs.k8s.io/controller-runtime"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
|
)
|
|
|
|
var ErrNoKubeletServingCSR = errors.New("CSR is no kubelet serving certificate")
|
|
|
|
type CSRReconciler struct {
|
|
client.Client
|
|
Log logr.Logger
|
|
Scheme *runtime.Scheme
|
|
PVEClient *pve.Client
|
|
CSRClient *typedcertificatesv1.CertificatesV1Client
|
|
}
|
|
|
|
func (r *CSRReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
|
|
return ctrl.NewControllerManagedBy(mgr).
|
|
WithOptions(options).
|
|
For(&certificatesv1.CertificateSigningRequest{}).
|
|
Complete(r)
|
|
}
|
|
|
|
func (r *CSRReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
|
|
log := r.Log
|
|
csr := &certificatesv1.CertificateSigningRequest{}
|
|
if err = r.Get(ctx, req.NamespacedName, csr); err != nil {
|
|
if apierrors.IsNotFound(err) {
|
|
err = nil
|
|
return
|
|
}
|
|
return
|
|
}
|
|
if !csr.DeletionTimestamp.IsZero() {
|
|
return
|
|
}
|
|
err = r.approve(ctx, log, csr)
|
|
return
|
|
}
|
|
|
|
var conditionv1 = certificatesv1.CertificateSigningRequestCondition{
|
|
Type: certificatesv1.CertificateApproved,
|
|
Reason: "AutoApproved",
|
|
Message: "Auto approving kubelet client certificate in PVE Cluster",
|
|
}
|
|
|
|
func (r *CSRReconciler) approve(ctx context.Context, log logr.Logger, request *certificatesv1.CertificateSigningRequest) error {
|
|
if len(request.Status.Conditions) > 0 {
|
|
return nil
|
|
}
|
|
client := r.CSRClient.CertificateSigningRequests()
|
|
err := checkCSR(ctx, r.PVEClient, request)
|
|
if err == ErrNoKubeletServingCSR {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
if len(request.Status.Conditions) > 0 {
|
|
return nil
|
|
}
|
|
request.Status.Conditions = append(request.Status.Conditions, conditionv1)
|
|
// Submit the updated CSR.
|
|
if _, err := client.UpdateApproval(ctx, request.Name, request, metav1.UpdateOptions{}); err != nil {
|
|
if strings.Contains(err.Error(), "the object has been modified") {
|
|
// The CSR might have been updated by a third-party, retry until we
|
|
// succeed.
|
|
request, err = client.Get(ctx, request.ObjectMeta.Name, metav1.GetOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
log.Info("csr approval successful", "name", request.ObjectMeta.Name)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func checkCSR(ctx context.Context, pveClient *pve.Client, request *certificatesv1.CertificateSigningRequest) error {
|
|
req, err := ParseCSR(request.Spec.Request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
usages := make([]string, len(request.Spec.Usages))
|
|
for i, v := range request.Spec.Usages {
|
|
usages[i] = string(v)
|
|
}
|
|
if !IsKubeletServingCSR(req, sets.NewString(usages...)) {
|
|
return ErrNoKubeletServingCSR
|
|
}
|
|
hostname := strings.TrimPrefix(req.Subject.CommonName, "system:node:")
|
|
ref, err := pveClient.Pool.FindServerByName(ctx, "", hostname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ref.Name != hostname {
|
|
return errors.New("CSR hostname and PVE server name do not match")
|
|
}
|
|
ok := false
|
|
for _, dns := range req.DNSNames {
|
|
if dns == hostname {
|
|
ok = true
|
|
}
|
|
}
|
|
if len(req.DNSNames) == 0 {
|
|
ok = true
|
|
}
|
|
if !ok {
|
|
return errors.New("req.DNSNames does not contain hostname")
|
|
}
|
|
srv, err := pveClient.Server.GetByRef(ctx, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ok = false
|
|
for _, ip := range req.IPAddresses {
|
|
for _, ipc := range srv.IPConfig {
|
|
ipv4, _, err := net.ParseCIDR(ipc.IPv4CIDR)
|
|
if err == nil {
|
|
if ip.String() == ipv4.String() {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
ipv6, _, err := net.ParseCIDR(ipc.IPv6CIDR)
|
|
if err == nil {
|
|
if ip.String() == ipv6.String() {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if ok {
|
|
break
|
|
}
|
|
}
|
|
if len(req.IPAddresses) == 0 {
|
|
ok = true
|
|
}
|
|
if !ok {
|
|
return errors.New("req.IPAddresses does not contain a valid IP")
|
|
}
|
|
return nil
|
|
}
|