webhookconfig.go 5.9 KB

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