controller.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /*
  2. Copyright © The ESO Authors
  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 clusterprovider implements the controller for ClusterProvider resources.
  14. package clusterprovider
  15. import (
  16. "context"
  17. "fmt"
  18. "time"
  19. "github.com/go-logr/logr"
  20. corev1 "k8s.io/api/core/v1"
  21. apierrors "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime"
  24. ctrl "sigs.k8s.io/controller-runtime"
  25. "sigs.k8s.io/controller-runtime/pkg/client"
  26. "sigs.k8s.io/controller-runtime/pkg/controller"
  27. "sigs.k8s.io/controller-runtime/pkg/handler"
  28. ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
  29. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  30. k8sv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/kubernetes/v2alpha1"
  31. pb "github.com/external-secrets/external-secrets/proto/provider"
  32. "github.com/external-secrets/external-secrets/providers/v2/common/grpc"
  33. )
  34. // Reconciler reconciles a ClusterProvider object.
  35. type Reconciler struct {
  36. client.Client
  37. Log logr.Logger
  38. Scheme *runtime.Scheme
  39. RequeueInterval time.Duration
  40. }
  41. // Reconcile validates the ClusterProvider and updates its status.
  42. func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
  43. log := r.Log.WithValues("ClusterProvider", req.NamespacedName)
  44. start := time.Now()
  45. defer func() {
  46. duration := time.Since(start)
  47. if gaugeVec := GetGaugeVec(ClusterProviderReconcileDurationKey); gaugeVec != nil {
  48. gaugeVec.WithLabelValues(req.Name).Set(duration.Seconds())
  49. }
  50. }()
  51. log.Info("reconciling ClusterProvider")
  52. var store esv1.ClusterProvider
  53. if err := r.Get(ctx, req.NamespacedName, &store); err != nil {
  54. if apierrors.IsNotFound(err) {
  55. RemoveMetrics(req.Name)
  56. return ctrl.Result{}, nil
  57. }
  58. log.Error(err, "unable to get ClusterProvider")
  59. return ctrl.Result{}, err
  60. }
  61. // Validate provider config and get capabilities
  62. capabilities, err := r.validateStoreAndGetCapabilities(ctx, &store)
  63. if err != nil {
  64. log.Error(err, "validation failed")
  65. r.setNotReadyCondition(&store, "ValidationFailed", err.Error())
  66. store.Status.Capabilities = ""
  67. if updateErr := r.Status().Update(ctx, &store); updateErr != nil {
  68. log.Error(updateErr, "failed to update status")
  69. return ctrl.Result{}, updateErr
  70. }
  71. // Requeue after interval to retry
  72. return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
  73. }
  74. // Set ready condition and capabilities
  75. r.setReadyCondition(&store)
  76. store.Status.Capabilities = capabilities
  77. if err := r.Status().Update(ctx, &store); err != nil {
  78. log.Error(err, "failed to update status")
  79. return ctrl.Result{}, err
  80. }
  81. log.Info("ClusterProvider is ready", "capabilities", capabilities)
  82. return ctrl.Result{RequeueAfter: r.RequeueInterval}, nil
  83. }
  84. // validateStoreAndGetCapabilities validates the ClusterProvider configuration and retrieves capabilities by:
  85. // 1. Creating a gRPC client to the provider
  86. // 2. Calling Validate() on the provider with the ProviderReference
  87. // 3. Calling Capabilities() to get the provider's capabilities.
  88. func (r *Reconciler) validateStoreAndGetCapabilities(ctx context.Context, store *esv1.ClusterProvider) (esv1.ProviderCapabilities, error) {
  89. // Get provider address
  90. address := store.Spec.Config.Address
  91. if address == "" {
  92. return "", fmt.Errorf("provider address is required")
  93. }
  94. tlsSecretNamespace := grpc.ResolveTLSSecretNamespace(
  95. store.Spec.Config.Address,
  96. "",
  97. "",
  98. store.Spec.Config.ProviderRef.Namespace,
  99. )
  100. // Load TLS configuration
  101. tlsConfig, err := grpc.LoadClientTLSConfig(ctx, r.Client, store.Spec.Config.Address, tlsSecretNamespace)
  102. if err != nil {
  103. return "", fmt.Errorf("failed to load TLS config: %w", err)
  104. }
  105. // Create gRPC client with TLS
  106. client, err := grpc.NewClient(address, tlsConfig)
  107. if err != nil {
  108. return "", fmt.Errorf("failed to create gRPC client: %w", err)
  109. }
  110. defer func() { _ = client.Close(ctx) }()
  111. // Convert ProviderReference to protobuf format
  112. providerRef := &pb.ProviderReference{
  113. ApiVersion: store.Spec.Config.ProviderRef.APIVersion,
  114. Kind: store.Spec.Config.ProviderRef.Kind,
  115. Name: store.Spec.Config.ProviderRef.Name,
  116. Namespace: store.Spec.Config.ProviderRef.Namespace,
  117. StoreRefKind: esv1.ClusterProviderKindStr,
  118. }
  119. // For ClusterProvider validation, we need to be more lenient since:
  120. // - ManifestNamespace scope: we don't have a manifest namespace at validation time
  121. // - ProviderNamespace scope: validation namespace may not have RBAC set up yet
  122. // So we skip validation and rely on runtime errors instead
  123. // This is acceptable since the provider will be validated when actually used by an ExternalSecret
  124. // Get provider capabilities (using empty namespace for ClusterProvider)
  125. caps, err := client.Capabilities(ctx, providerRef, "")
  126. if err != nil {
  127. r.Log.Error(err, "failed to get capabilities")
  128. // Don't fail validation if capabilities check fails, just log and default to ReadOnly
  129. return esv1.ProviderReadOnly, nil
  130. }
  131. // Map gRPC capabilities to our API type
  132. var capabilities esv1.ProviderCapabilities
  133. switch caps {
  134. case pb.SecretStoreCapabilities_READ_ONLY:
  135. capabilities = esv1.ProviderReadOnly
  136. case pb.SecretStoreCapabilities_WRITE_ONLY:
  137. capabilities = esv1.ProviderWriteOnly
  138. case pb.SecretStoreCapabilities_READ_WRITE:
  139. capabilities = esv1.ProviderReadWrite
  140. default:
  141. capabilities = esv1.ProviderReadOnly
  142. }
  143. return capabilities, nil
  144. }
  145. // setReadyCondition sets the Ready condition to True.
  146. func (r *Reconciler) setReadyCondition(store *esv1.ClusterProvider) {
  147. condition := esv1.ProviderCondition{
  148. Type: esv1.ProviderReady,
  149. Status: metav1.ConditionTrue,
  150. LastTransitionTime: metav1.Now(),
  151. Reason: "Validated",
  152. Message: "ClusterProvider is ready",
  153. }
  154. r.setCondition(store, condition)
  155. }
  156. // setNotReadyCondition sets the Ready condition to False.
  157. func (r *Reconciler) setNotReadyCondition(store *esv1.ClusterProvider, reason, message string) {
  158. condition := esv1.ProviderCondition{
  159. Type: esv1.ProviderReady,
  160. Status: metav1.ConditionFalse,
  161. LastTransitionTime: metav1.Now(),
  162. Reason: reason,
  163. Message: message,
  164. }
  165. r.setCondition(store, condition)
  166. }
  167. // setCondition updates or adds a condition to the ClusterProvider status.
  168. func (r *Reconciler) setCondition(store *esv1.ClusterProvider, newCondition esv1.ProviderCondition) {
  169. // Find existing condition
  170. for i, condition := range store.Status.Conditions {
  171. if condition.Type == newCondition.Type {
  172. // Preserve LastTransitionTime unless the condition status actually changes.
  173. if condition.Status == newCondition.Status {
  174. newCondition.LastTransitionTime = condition.LastTransitionTime
  175. }
  176. if condition.Status != newCondition.Status ||
  177. condition.Reason != newCondition.Reason ||
  178. condition.Message != newCondition.Message {
  179. store.Status.Conditions[i] = newCondition
  180. }
  181. // Update metrics
  182. UpdateStatusCondition(store, newCondition)
  183. return
  184. }
  185. }
  186. // Add new condition
  187. store.Status.Conditions = append(store.Status.Conditions, newCondition)
  188. // Update metrics
  189. UpdateStatusCondition(store, newCondition)
  190. }
  191. func findClusterProvidersForKubernetesProvider(ctx context.Context, kubeClient client.Client, obj client.Object) []ctrlreconcile.Request {
  192. kubernetesProvider, ok := obj.(*k8sv2alpha1.Kubernetes)
  193. if !ok {
  194. return nil
  195. }
  196. var providers esv1.ClusterProviderList
  197. if err := kubeClient.List(ctx, &providers); err != nil {
  198. return nil
  199. }
  200. requests := make([]ctrlreconcile.Request, 0, len(providers.Items))
  201. for i := range providers.Items {
  202. provider := providers.Items[i]
  203. ref := provider.Spec.Config.ProviderRef
  204. if ref.APIVersion != k8sv2alpha1.GroupVersion.String() ||
  205. ref.Kind != k8sv2alpha1.Kind ||
  206. ref.Name != kubernetesProvider.Name ||
  207. ref.Namespace != kubernetesProvider.Namespace {
  208. continue
  209. }
  210. requests = append(requests, ctrlreconcile.Request{
  211. NamespacedName: client.ObjectKeyFromObject(&provider),
  212. })
  213. }
  214. return requests
  215. }
  216. // SetupWithManager sets up the controller with the Manager.
  217. func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
  218. return ctrl.NewControllerManagedBy(mgr).
  219. WithOptions(opts).
  220. For(&esv1.ClusterProvider{}).
  221. Watches(
  222. &k8sv2alpha1.Kubernetes{},
  223. handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrlreconcile.Request {
  224. return findClusterProvidersForKubernetesProvider(ctx, r.Client, obj)
  225. }),
  226. ).
  227. Owns(&corev1.Secret{}). // Watch secrets that might be used for auth
  228. Complete(r)
  229. }