webhookconfig.go 5.9 KB

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