webhookconfig.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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/go-logr/logr"
  22. admissionregistration "k8s.io/api/admissionregistration/v1"
  23. v1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/api/equality"
  25. apierrors "k8s.io/apimachinery/pkg/api/errors"
  26. "k8s.io/apimachinery/pkg/runtime"
  27. "k8s.io/apimachinery/pkg/types"
  28. "k8s.io/client-go/tools/record"
  29. ctrl "sigs.k8s.io/controller-runtime"
  30. "sigs.k8s.io/controller-runtime/pkg/client"
  31. "sigs.k8s.io/controller-runtime/pkg/controller"
  32. "github.com/external-secrets/external-secrets/pkg/constants"
  33. )
  34. type Reconciler struct {
  35. client.Client
  36. Log logr.Logger
  37. Scheme *runtime.Scheme
  38. recorder record.EventRecorder
  39. RequeueDuration time.Duration
  40. SvcName string
  41. SvcNamespace string
  42. SecretName string
  43. SecretNamespace string
  44. // store state for the readiness probe.
  45. // we're ready when we're not the leader or
  46. // if we've reconciled the webhook config when we're the leader.
  47. leaderChan <-chan struct{}
  48. leaderElected bool
  49. webhookReadyMu *sync.Mutex
  50. webhookReady bool
  51. }
  52. type Opts struct {
  53. SvcName string
  54. SvcNamespace string
  55. SecretName string
  56. SecretNamespace string
  57. RequeueInterval time.Duration
  58. }
  59. func New(k8sClient client.Client, scheme *runtime.Scheme, leaderChan <-chan struct{}, log logr.Logger, opts Opts) *Reconciler {
  60. return &Reconciler{
  61. Client: k8sClient,
  62. Scheme: scheme,
  63. Log: log,
  64. RequeueDuration: opts.RequeueInterval,
  65. SvcName: opts.SvcName,
  66. SvcNamespace: opts.SvcNamespace,
  67. SecretName: opts.SecretName,
  68. SecretNamespace: opts.SecretNamespace,
  69. leaderChan: leaderChan,
  70. leaderElected: false,
  71. webhookReadyMu: &sync.Mutex{},
  72. webhookReady: false,
  73. }
  74. }
  75. const (
  76. ReasonUpdateFailed = "UpdateFailed"
  77. errWebhookNotReady = "webhook not ready"
  78. errSubsetsNotReady = "subsets not ready"
  79. errAddressesNotReady = "addresses not ready"
  80. errCACertNotReady = "ca cert not yet ready"
  81. caCertName = "ca.crt"
  82. )
  83. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  84. log := r.Log.WithValues("Webhookconfig", req.NamespacedName)
  85. var cfg admissionregistration.ValidatingWebhookConfiguration
  86. err := r.Get(ctx, req.NamespacedName, &cfg)
  87. if apierrors.IsNotFound(err) {
  88. return ctrl.Result{}, nil
  89. } else if err != nil {
  90. log.Error(err, "unable to get Webhookconfig")
  91. return ctrl.Result{}, err
  92. }
  93. if cfg.Labels[constants.WellKnownLabelKey] != constants.WellKnownLabelValueWebhook {
  94. log.Info("ignoring webhook due to missing labels", constants.WellKnownLabelKey, constants.WellKnownLabelValueWebhook)
  95. return ctrl.Result{}, nil
  96. }
  97. log.Info("updating webhook config")
  98. err = r.updateConfig(logr.NewContext(ctx, log), &cfg)
  99. if err != nil {
  100. log.Error(err, "could not update webhook config")
  101. r.recorder.Eventf(&cfg, v1.EventTypeWarning, ReasonUpdateFailed, err.Error())
  102. return ctrl.Result{
  103. RequeueAfter: time.Minute,
  104. }, err
  105. }
  106. // right now we only have one single
  107. // webhook config we care about
  108. r.webhookReadyMu.Lock()
  109. defer r.webhookReadyMu.Unlock()
  110. r.webhookReady = true
  111. return ctrl.Result{
  112. RequeueAfter: r.RequeueDuration,
  113. }, nil
  114. }
  115. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  116. r.recorder = mgr.GetEventRecorderFor("validating-webhook-configuration")
  117. return ctrl.NewControllerManagedBy(mgr).
  118. WithOptions(opts).
  119. For(&admissionregistration.ValidatingWebhookConfiguration{}).
  120. Complete(r)
  121. }
  122. func (r *Reconciler) ReadyCheck(_ *http.Request) error {
  123. // skip readiness check if we're not leader
  124. // as we depend on caches and being able to reconcile Webhooks
  125. if !r.leaderElected {
  126. select {
  127. case <-r.leaderChan:
  128. r.leaderElected = true
  129. default:
  130. return nil
  131. }
  132. }
  133. r.webhookReadyMu.Lock()
  134. defer r.webhookReadyMu.Unlock()
  135. if !r.webhookReady {
  136. return errors.New(errWebhookNotReady)
  137. }
  138. var eps v1.Endpoints
  139. err := r.Get(context.TODO(), types.NamespacedName{
  140. Name: r.SvcName,
  141. Namespace: r.SvcNamespace,
  142. }, &eps)
  143. if err != nil {
  144. return err
  145. }
  146. if len(eps.Subsets) == 0 {
  147. return errors.New(errSubsetsNotReady)
  148. }
  149. if len(eps.Subsets[0].Addresses) == 0 {
  150. return errors.New(errAddressesNotReady)
  151. }
  152. return nil
  153. }
  154. // reads the ca cert and updates the webhook config.
  155. func (r *Reconciler) updateConfig(ctx context.Context, cfg *admissionregistration.ValidatingWebhookConfiguration) error {
  156. log := logr.FromContextOrDiscard(ctx)
  157. before := cfg.DeepCopyObject()
  158. secret := v1.Secret{}
  159. secretName := types.NamespacedName{
  160. Name: r.SecretName,
  161. Namespace: r.SecretNamespace,
  162. }
  163. err := r.Get(context.Background(), secretName, &secret)
  164. if err != nil {
  165. return err
  166. }
  167. crt, ok := secret.Data[caCertName]
  168. if !ok {
  169. return errors.New(errCACertNotReady)
  170. }
  171. r.inject(cfg, r.SvcName, r.SvcNamespace, crt)
  172. if !equality.Semantic.DeepEqual(before, cfg) {
  173. if err := r.Update(ctx, cfg); err != nil {
  174. return err
  175. }
  176. log.Info("updated webhook config")
  177. return nil
  178. }
  179. log.V(1).Info("webhook config unchanged")
  180. return nil
  181. }
  182. func (r *Reconciler) inject(cfg *admissionregistration.ValidatingWebhookConfiguration, svcName, svcNamespace string, certData []byte) {
  183. r.Log.Info("injecting ca certificate and service names", "cacrt", base64.StdEncoding.EncodeToString(certData), "name", cfg.Name)
  184. for idx, w := range cfg.Webhooks {
  185. if !strings.HasSuffix(w.Name, "external-secrets.io") {
  186. r.Log.Info("skipping webhook", "name", cfg.Name, "webhook-name", w.Name)
  187. continue
  188. }
  189. // we just patch the relevant fields
  190. cfg.Webhooks[idx].ClientConfig.Service.Name = svcName
  191. cfg.Webhooks[idx].ClientConfig.Service.Namespace = svcNamespace
  192. cfg.Webhooks[idx].ClientConfig.CABundle = certData
  193. }
  194. }