cert-manager-webhook-example/solver/solver.go
2026-02-14 23:54:45 +01:00

104 lines
3.9 KiB
Go

package solver
import (
"context"
"encoding/json"
"fmt"
acme "github.com/cert-manager/cert-manager/pkg/acme/webhook/apis/acme/v1alpha1"
v1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
"github.com/nrdcg/desec"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// Configuration for the DeSEC DNS-01 challenge solver
type DeSECDNSProviderSolverConfig struct {
// Reference to the kubernetes secret containing the API token for deSEC
APIKeySecretRef v1.SecretKeySelector `json:"apiKeySecretRef"`
// Reference to the kubernetes namespace containing the secret
APIKeySecretRefNamespace string `json:"apiKeySecretRefNamespace"`
}
// A DNS-01 challenge solver for the DeSEC DNS Provider
type DeSECDNSProviderSolver struct {
// Client to communicate with the kubernetes API
k8s *kubernetes.Clientset
}
// Returns the name of the DNS solver
func (s *DeSECDNSProviderSolver) Name() string {
return "deSEC"
}
// Initializes a new client
func (s *DeSECDNSProviderSolver) getClient(config *apiextensionsv1.JSON, namespace string) (*desec.Client, error) {
if config == nil {
return nil, fmt.Errorf("missing configuration in issuer found; webhook configuration requires apiKeySecretRef containing deSEC API token")
}
// Initialize the configuration object and unmarshal json
solverConfig := DeSECDNSProviderSolverConfig{}
if err := json.Unmarshal(config.Raw, &solverConfig); err != nil {
return nil, fmt.Errorf("invalid configuration in issuer found; webhook configuration requires apiKeySecretRef containing deSEC API token")
}
// Check if the namespace has been provided within the configuration
// Otherwise use the namespace from the request
if solverConfig.APIKeySecretRefNamespace != "" {
fmt.Sprintf("k8s secret namespace has been overwritten in webhook configuration apiKeySecretRefNamespace from %s to %s", namespace, solverConfig.APIKeySecretRefNamespace)
namespace = solverConfig.APIKeySecretRefNamespace
}
// Check if the k8s client has been initialized
// This should never happen as cert-manager calls s.Initialize() which assigns the k8s client
if s.k8s == nil {
return nil, fmt.Errorf("k8s client has not been initialized by cert-manager; this should never happen")
}
// Read the secret from k8s
secret, err := s.k8s.CoreV1().Secrets(namespace).Get(context.Background(), solverConfig.APIKeySecretRef.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("k8s secret %s not found in namespace %s", solverConfig.APIKeySecretRef.Name, namespace)
}
token, ok := secret.Data[solverConfig.APIKeySecretRef.Key]
if !ok {
return nil, fmt.Errorf("k8s secret key %s not found in secret %s in namespace %s", solverConfig.APIKeySecretRef.Key, solverConfig.APIKeySecretRef.Name, namespace)
}
// Finally assign the client
client := desec.New(string(token), desec.NewDefaultClientOptions())
// Return the client (reuse if initialized)
return client, nil
}
// Present presents the TXT DNS entry after completion of the ACME DNS-01 challenge
func (s *DeSECDNSProviderSolver) Present(req *acme.ChallengeRequest) error {
// Create or reuse the API client
apiClient, err := s.getClient(req.Config, req.ResourceNamespace)
if err != nil {
return err
}
return nil
}
// Cleanup removes the TXT DNS entry after completion of the ACME DNS-01 challenge
func (s *DeSECDNSProviderSolver) CleanUp(req *acme.ChallengeRequest) error {
// Create or reuse the API client
apiClient, err := s.getClient(req.Config, req.ResourceNamespace)
if err != nil {
return err
}
return nil
}
// Initializes the solver
func (s *DeSECDNSProviderSolver) Initialize(kubeClientConfig *rest.Config, stopCh <-chan struct{}) error {
// Create the k8s client
k8s, err := kubernetes.NewForConfig(kubeClientConfig)
if err != nil {
return err
}
// Assign the k8s client to the solver
s.k8s = k8s
return nil
}