push.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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 infisical
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "net/http"
  19. "time"
  20. infisicalSdk "github.com/infisical/go-sdk"
  21. sdkErrors "github.com/infisical/go-sdk/packages/errors"
  22. //nolint
  23. . "github.com/onsi/ginkgo/v2"
  24. //nolint
  25. . "github.com/onsi/gomega"
  26. v1 "k8s.io/api/core/v1"
  27. apierrors "k8s.io/apimachinery/pkg/api/errors"
  28. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  29. "k8s.io/apimachinery/pkg/types"
  30. "k8s.io/apimachinery/pkg/util/wait"
  31. "github.com/external-secrets/external-secrets-e2e/framework"
  32. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  33. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  34. )
  35. // pushSecretValue pushes a single value through the provider's PushSecret
  36. // implementation, then reads it back with an ExternalSecret to confirm the
  37. // round-trip. The remote key is namespaced so parallel specs sharing the e2e
  38. // project do not collide.
  39. func pushSecretValue(prov *infisicalProvider) func(*framework.Framework) (string, func(*framework.TestCase)) {
  40. return func(f *framework.Framework) (string, func(*framework.TestCase)) {
  41. return "[infisical] should push a secret and read it back", func(tc *framework.TestCase) {
  42. sourceName := fmt.Sprintf("%s-src", f.Namespace.Name)
  43. remoteKey := fmt.Sprintf("%s-pushed", f.Namespace.Name)
  44. value := "pushed-value"
  45. tc.PushSecretSource = &v1.Secret{
  46. ObjectMeta: metav1.ObjectMeta{
  47. Name: sourceName,
  48. Namespace: f.Namespace.Name,
  49. },
  50. Type: v1.SecretTypeOpaque,
  51. Data: map[string][]byte{"credential": []byte(value)},
  52. }
  53. tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
  54. Secret: &esv1alpha1.PushSecretSecret{Name: sourceName},
  55. }
  56. tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{
  57. {
  58. Match: esv1alpha1.PushSecretMatch{
  59. SecretKey: "credential",
  60. RemoteRef: esv1alpha1.PushSecretRemoteRef{RemoteKey: remoteKey},
  61. },
  62. },
  63. }
  64. tc.VerifyPushSecretOutcome = func(_ *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
  65. // Remove the pushed secret from the shared project so re-runs
  66. // start clean, regardless of controller teardown ordering.
  67. DeferCleanup(func() { prov.deleteRemote(remoteKey) })
  68. Eventually(func() bool {
  69. ps := &esv1alpha1.PushSecret{}
  70. err := f.CRClient.Get(GinkgoT().Context(), types.NamespacedName{
  71. Name: tc.PushSecret.Name,
  72. Namespace: tc.PushSecret.Namespace,
  73. }, ps)
  74. Expect(err).ToNot(HaveOccurred())
  75. for i := range ps.Status.Conditions {
  76. c := ps.Status.Conditions[i]
  77. if c.Type == esv1alpha1.PushSecretReady && c.Status == v1.ConditionTrue {
  78. return true
  79. }
  80. }
  81. return false
  82. }, time.Minute*2, time.Second*5).Should(BeTrue())
  83. // Read the pushed value back through an ExternalSecret.
  84. const target = "push-readback"
  85. es := &esv1.ExternalSecret{
  86. ObjectMeta: metav1.ObjectMeta{
  87. Name: "e2e-push-es",
  88. Namespace: f.Namespace.Name,
  89. },
  90. Spec: esv1.ExternalSecretSpec{
  91. RefreshInterval: &metav1.Duration{Duration: time.Second * 5},
  92. SecretStoreRef: esv1.SecretStoreRef{Name: f.Namespace.Name},
  93. Target: esv1.ExternalSecretTarget{Name: target},
  94. Data: []esv1.ExternalSecretData{
  95. {
  96. SecretKey: target,
  97. RemoteRef: esv1.ExternalSecretDataRemoteRef{Key: remoteKey},
  98. },
  99. },
  100. },
  101. }
  102. Expect(f.CRClient.Create(GinkgoT().Context(), es)).ToNot(HaveOccurred())
  103. readBack := &v1.Secret{}
  104. err := wait.PollUntilContextTimeout(GinkgoT().Context(), time.Second*5, time.Minute*2, true, func(ctx context.Context) (bool, error) {
  105. gerr := f.CRClient.Get(ctx, types.NamespacedName{Namespace: f.Namespace.Name, Name: target}, readBack)
  106. if apierrors.IsNotFound(gerr) {
  107. return false, nil
  108. }
  109. return gerr == nil, gerr
  110. })
  111. Expect(err).ToNot(HaveOccurred())
  112. Expect(string(readBack.Data[target])).To(Equal(value))
  113. }
  114. }
  115. }
  116. }
  117. // pushSecretDeletesOnPolicy pushes a secret with deletionPolicy: Delete, then
  118. // deletes the PushSecret and confirms the operator removed the secret from
  119. // Infisical through the provider's DeleteSecret.
  120. func pushSecretDeletesOnPolicy(prov *infisicalProvider) func(*framework.Framework) (string, func(*framework.TestCase)) {
  121. return func(f *framework.Framework) (string, func(*framework.TestCase)) {
  122. return "[infisical] should delete the remote secret when the PushSecret is deleted", func(tc *framework.TestCase) {
  123. sourceName := fmt.Sprintf("%s-del-src", f.Namespace.Name)
  124. remoteKey := fmt.Sprintf("%s-del", f.Namespace.Name)
  125. value := "delete-me"
  126. tc.PushSecretSource = &v1.Secret{
  127. ObjectMeta: metav1.ObjectMeta{Name: sourceName, Namespace: f.Namespace.Name},
  128. Type: v1.SecretTypeOpaque,
  129. Data: map[string][]byte{"credential": []byte(value)},
  130. }
  131. tc.PushSecret.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyDelete
  132. tc.PushSecret.Spec.Selector = esv1alpha1.PushSecretSelector{
  133. Secret: &esv1alpha1.PushSecretSecret{Name: sourceName},
  134. }
  135. tc.PushSecret.Spec.Data = []esv1alpha1.PushSecretData{
  136. {
  137. Match: esv1alpha1.PushSecretMatch{
  138. SecretKey: "credential",
  139. RemoteRef: esv1alpha1.PushSecretRemoteRef{RemoteKey: remoteKey},
  140. },
  141. },
  142. }
  143. tc.VerifyPushSecretOutcome = func(ps *esv1alpha1.PushSecret, _ esv1.SecretsClient) {
  144. // Best-effort guard in case the deletion assertion below fails.
  145. DeferCleanup(func() { prov.deleteRemote(remoteKey) })
  146. // The push lands the secret in Infisical.
  147. Eventually(func() error {
  148. _, err := prov.remoteSecretValue(remoteKey)
  149. return err
  150. }, time.Minute*2, time.Second*5).Should(Succeed())
  151. // Deleting the PushSecret must drive the provider's DeleteSecret
  152. // (deletionPolicy: Delete), removing the secret from Infisical.
  153. Expect(f.CRClient.Delete(GinkgoT().Context(), ps)).To(Succeed())
  154. // Wait until the secret is confirmed absent. Using
  155. // Should(Succeed()) on a helper that returns nil only on
  156. // "not found" avoids a false pass on transient API errors
  157. // (which ShouldNot(Succeed()) would also accept).
  158. Eventually(func() error {
  159. _, err := prov.remoteSecretValue(remoteKey)
  160. if err == nil {
  161. return fmt.Errorf("secret %q still exists in Infisical", remoteKey)
  162. }
  163. if isInfisicalNotFound(err) {
  164. return nil
  165. }
  166. return err
  167. }, time.Minute*2, time.Second*5).Should(Succeed())
  168. }
  169. }
  170. }
  171. }
  172. // isInfisicalNotFound reports whether err is a 404 from the Infisical API,
  173. // meaning the secret is definitively absent rather than transiently unavailable.
  174. func isInfisicalNotFound(err error) bool {
  175. var apiErr *sdkErrors.APIError
  176. return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
  177. }
  178. // remoteSecretValue reads a secret from the e2e project by slug via the addon
  179. // SDK; it returns an error when the secret is absent.
  180. func (s *infisicalProvider) remoteSecretValue(key string) (string, error) {
  181. secretPath, name := secretAddress(scopePath, key)
  182. secret, err := s.addon.SDKClient.Secrets().Retrieve(infisicalSdk.RetrieveSecretOptions{
  183. ProjectSlug: s.addon.ProjectSlug,
  184. Environment: s.addon.EnvironmentSlug,
  185. SecretKey: name,
  186. SecretPath: secretPath,
  187. })
  188. if err != nil {
  189. return "", err
  190. }
  191. return secret.SecretValue, nil
  192. }
  193. // deleteRemote best-effort removes a secret from the shared e2e project via the
  194. // addon SDK, used to clean up after a push spec.
  195. func (s *infisicalProvider) deleteRemote(key string) {
  196. secretPath, name := secretAddress(scopePath, key)
  197. _, _ = s.addon.SDKClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
  198. ProjectID: s.addon.ProjectID,
  199. Environment: s.addon.EnvironmentSlug,
  200. SecretPath: secretPath,
  201. SecretKey: name,
  202. })
  203. }