webhookconfig.go 5.3 KB

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