webhookconfig.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // Package webhookconfig contains the controller for the WebhookConfig resource.
  14. package webhookconfig
  15. import (
  16. "context"
  17. "encoding/base64"
  18. "errors"
  19. "net/http"
  20. "strings"
  21. "sync"
  22. "time"
  23. "github.com/go-logr/logr"
  24. admissionregistration "k8s.io/api/admissionregistration/v1"
  25. v1 "k8s.io/api/core/v1"
  26. "k8s.io/apimachinery/pkg/api/equality"
  27. apierrors "k8s.io/apimachinery/pkg/api/errors"
  28. "k8s.io/apimachinery/pkg/runtime"
  29. "k8s.io/apimachinery/pkg/types"
  30. "k8s.io/client-go/tools/record"
  31. ctrl "sigs.k8s.io/controller-runtime"
  32. "sigs.k8s.io/controller-runtime/pkg/client"
  33. "sigs.k8s.io/controller-runtime/pkg/controller"
  34. "github.com/external-secrets/external-secrets/runtime/constants"
  35. "github.com/external-secrets/external-secrets/runtime/esutils"
  36. )
  37. // Reconciler reconciles a ValidatingWebhookConfiguration object
  38. // and updates it with the CA bundle from the given secret.
  39. type Reconciler struct {
  40. client.Client
  41. Log logr.Logger
  42. Scheme *runtime.Scheme
  43. recorder record.EventRecorder
  44. RequeueDuration time.Duration
  45. SvcName string
  46. SvcNamespace string
  47. SecretName string
  48. SecretNamespace string
  49. // store state for the readiness probe.
  50. // we're ready when we're not the leader or
  51. // if we've reconciled the webhook config when we're the leader.
  52. leaderChan <-chan struct{}
  53. leaderElected bool
  54. webhookReadyMu *sync.Mutex
  55. webhookReady bool
  56. }
  57. // Opts are the options for the webhookconfig controller Reconciler.
  58. type Opts struct {
  59. SvcName string
  60. SvcNamespace string
  61. SecretName string
  62. SecretNamespace string
  63. RequeueInterval time.Duration
  64. }
  65. // New returns a new Reconciler.
  66. // The controller will watch ValidatingWebhookConfiguration resources
  67. // and update them with the CA bundle from the given secret.
  68. func New(k8sClient client.Client, scheme *runtime.Scheme, leaderChan <-chan struct{}, log logr.Logger, opts Opts) *Reconciler {
  69. return &Reconciler{
  70. Client: k8sClient,
  71. Scheme: scheme,
  72. Log: log,
  73. RequeueDuration: opts.RequeueInterval,
  74. SvcName: opts.SvcName,
  75. SvcNamespace: opts.SvcNamespace,
  76. SecretName: opts.SecretName,
  77. SecretNamespace: opts.SecretNamespace,
  78. leaderChan: leaderChan,
  79. leaderElected: false,
  80. webhookReadyMu: &sync.Mutex{},
  81. webhookReady: false,
  82. }
  83. }
  84. const (
  85. // ReasonUpdateFailed is used when we fail to update the webhook config.
  86. ReasonUpdateFailed = "UpdateFailed"
  87. errWebhookNotReady = "webhook not ready"
  88. errCACertNotReady = "ca cert not yet ready"
  89. caCertName = "ca.crt"
  90. )
  91. // Reconcile is part of the main kubernetes reconciliation loop which aims to
  92. // move the current state of the cluster closer to the desired state.
  93. // In this case, we reconcile ValidatingWebhookConfiguration resources
  94. // that are labeled with the well-known label key and value.
  95. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  96. log := r.Log.WithValues("Webhookconfig", req.NamespacedName)
  97. var cfg admissionregistration.ValidatingWebhookConfiguration
  98. err := r.Get(ctx, req.NamespacedName, &cfg)
  99. if apierrors.IsNotFound(err) {
  100. return ctrl.Result{}, nil
  101. } else if err != nil {
  102. log.Error(err, "unable to get Webhookconfig")
  103. return ctrl.Result{}, err
  104. }
  105. if cfg.Labels[constants.WellKnownLabelKey] != constants.WellKnownLabelValueWebhook {
  106. log.Info("ignoring webhook due to missing labels", constants.WellKnownLabelKey, constants.WellKnownLabelValueWebhook)
  107. return ctrl.Result{}, nil
  108. }
  109. log.Info("updating webhook config")
  110. err = r.updateConfig(logr.NewContext(ctx, log), &cfg)
  111. if err != nil {
  112. log.Error(err, "could not update webhook config")
  113. r.recorder.Eventf(&cfg, v1.EventTypeWarning, ReasonUpdateFailed, err.Error())
  114. return ctrl.Result{
  115. RequeueAfter: time.Minute,
  116. }, err
  117. }
  118. // right now we only have one single
  119. // webhook config we care about
  120. r.webhookReadyMu.Lock()
  121. defer r.webhookReadyMu.Unlock()
  122. r.webhookReady = true
  123. return ctrl.Result{
  124. RequeueAfter: r.RequeueDuration,
  125. }, nil
  126. }
  127. // SetupWithManager sets up the controller with the Manager.
  128. // Also initializes the event recorder.
  129. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  130. r.recorder = mgr.GetEventRecorderFor("validating-webhook-configuration")
  131. return ctrl.NewControllerManagedBy(mgr).
  132. WithOptions(opts).
  133. For(&admissionregistration.ValidatingWebhookConfiguration{}).
  134. Complete(r)
  135. }
  136. // ReadyCheck does a readiness check for the webhook using the endpoint slices.
  137. func (r *Reconciler) ReadyCheck(_ *http.Request) error {
  138. // skip readiness check if we're not leader
  139. // as we depend on caches and being able to reconcile Webhooks
  140. if !r.leaderElected {
  141. select {
  142. case <-r.leaderChan:
  143. r.leaderElected = true
  144. default:
  145. return nil
  146. }
  147. }
  148. r.webhookReadyMu.Lock()
  149. defer r.webhookReadyMu.Unlock()
  150. if !r.webhookReady {
  151. return errors.New(errWebhookNotReady)
  152. }
  153. return esutils.CheckEndpointSlicesReady(context.TODO(), r.Client, r.SvcName, r.SvcNamespace)
  154. }
  155. // reads the ca cert and updates the webhook config.
  156. func (r *Reconciler) updateConfig(ctx context.Context, cfg *admissionregistration.ValidatingWebhookConfiguration) error {
  157. log := logr.FromContextOrDiscard(ctx)
  158. before := cfg.DeepCopyObject()
  159. secret := v1.Secret{}
  160. secretName := types.NamespacedName{
  161. Name: r.SecretName,
  162. Namespace: r.SecretNamespace,
  163. }
  164. err := r.Get(context.Background(), secretName, &secret)
  165. if err != nil {
  166. return err
  167. }
  168. crt, ok := secret.Data[caCertName]
  169. if !ok {
  170. return errors.New(errCACertNotReady)
  171. }
  172. r.inject(cfg, r.SvcName, r.SvcNamespace, crt)
  173. if !equality.Semantic.DeepEqual(before, cfg) {
  174. if err := r.Update(ctx, cfg); err != nil {
  175. return err
  176. }
  177. log.Info("updated webhook config")
  178. return nil
  179. }
  180. log.V(1).Info("webhook config unchanged")
  181. return nil
  182. }
  183. func (r *Reconciler) inject(cfg *admissionregistration.ValidatingWebhookConfiguration, svcName, svcNamespace string, certData []byte) {
  184. r.Log.Info("injecting ca certificate and service names", "cacrt", base64.StdEncoding.EncodeToString(certData), "name", cfg.Name)
  185. for idx, w := range cfg.Webhooks {
  186. if !strings.HasSuffix(w.Name, "external-secrets.io") {
  187. r.Log.Info("skipping webhook", "name", cfg.Name, "webhook-name", w.Name)
  188. continue
  189. }
  190. // we just patch the relevant fields
  191. cfg.Webhooks[idx].ClientConfig.Service.Name = svcName
  192. cfg.Webhooks[idx].ClientConfig.Service.Namespace = svcNamespace
  193. cfg.Webhooks[idx].ClientConfig.CABundle = certData
  194. }
  195. }