grafana.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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 generator
  14. import (
  15. "os"
  16. "strings"
  17. "time"
  18. //nolint
  19. . "github.com/onsi/gomega"
  20. "sigs.k8s.io/controller-runtime/pkg/client"
  21. // nolint
  22. . "github.com/onsi/ginkgo/v2"
  23. v1 "k8s.io/api/core/v1"
  24. "k8s.io/utils/ptr"
  25. "github.com/external-secrets/external-secrets-e2e/framework"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  28. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  29. grafanaclient "github.com/grafana/grafana-openapi-client-go/client"
  30. grafanasearch "github.com/grafana/grafana-openapi-client-go/client/search"
  31. grafanasa "github.com/grafana/grafana-openapi-client-go/client/service_accounts"
  32. )
  33. var _ = Describe("grafana generator", Label("grafana"), func() {
  34. f := framework.New("grafana")
  35. const grafanaCredsSecretName = "grafana-creds"
  36. grafanaClient := newGrafanaClient()
  37. BeforeEach(func() {
  38. // grafana instance may need to load for a bit
  39. // we'll wake it up here and wait for it to be ready
  40. Eventually(func() error {
  41. _, err := grafanaClient.Search.Search(&grafanasearch.SearchParams{})
  42. return err
  43. }).WithPolling(time.Second * 15).WithTimeout(time.Minute * 5).ShouldNot(HaveOccurred())
  44. })
  45. AfterEach(func() {
  46. // ESO does clean up tokens, but not the service accounts.
  47. accounts, err := grafanaClient.ServiceAccounts.SearchOrgServiceAccountsWithPaging(&grafanasa.SearchOrgServiceAccountsWithPagingParams{
  48. Perpage: ptr.To(int64(100)),
  49. Page: ptr.To(int64(1)),
  50. Query: ptr.To(f.Namespace.Name),
  51. })
  52. Expect(err).ToNot(HaveOccurred())
  53. if accounts.GetPayload().ServiceAccounts != nil && len(accounts.GetPayload().ServiceAccounts) > 0 {
  54. for _, sa := range accounts.GetPayload().ServiceAccounts {
  55. _, err := grafanaClient.ServiceAccounts.DeleteServiceAccount(sa.ID)
  56. Expect(err).ToNot(HaveOccurred())
  57. }
  58. }
  59. })
  60. setupGenerator := func(tc *testCase) {
  61. err := f.CRClient.Create(GinkgoT().Context(), &v1.Secret{
  62. ObjectMeta: metav1.ObjectMeta{
  63. Name: grafanaCredsSecretName,
  64. Namespace: f.Namespace.Name,
  65. },
  66. Data: map[string][]byte{
  67. "grafana-token": []byte(os.Getenv("GRAFANA_TOKEN")),
  68. },
  69. })
  70. Expect(err).ToNot(HaveOccurred())
  71. tc.Generator = &genv1alpha1.Grafana{
  72. TypeMeta: metav1.TypeMeta{
  73. APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
  74. Kind: genv1alpha1.GrafanaKind,
  75. },
  76. ObjectMeta: metav1.ObjectMeta{
  77. Name: generatorName,
  78. Namespace: f.Namespace.Name,
  79. },
  80. Spec: genv1alpha1.GrafanaSpec{
  81. URL: os.Getenv("GRAFANA_URL"),
  82. ServiceAccount: genv1alpha1.GrafanaServiceAccount{
  83. Name: f.Namespace.Name,
  84. Role: "Viewer",
  85. },
  86. Auth: genv1alpha1.GrafanaAuth{
  87. Token: &genv1alpha1.SecretKeySelector{
  88. Name: grafanaCredsSecretName,
  89. Key: "grafana-token",
  90. },
  91. },
  92. },
  93. }
  94. tc.ExternalSecret.Spec.DataFrom = []esv1.ExternalSecretDataFromRemoteRef{
  95. {
  96. SourceRef: &esv1.StoreGeneratorSourceRef{
  97. GeneratorRef: &esv1.GeneratorRef{
  98. Kind: "Grafana",
  99. Name: generatorName,
  100. },
  101. },
  102. },
  103. }
  104. }
  105. ensureExternalSecretPurgesGeneratorState := func(tc *testCase) {
  106. // delete ES to trigger cleanup of generator state
  107. err := f.CRClient.Delete(GinkgoT().Context(), tc.ExternalSecret)
  108. Expect(err).ToNot(HaveOccurred())
  109. By("waiting for generator state to be cleaned up")
  110. // wait for generator state to be cleaned up
  111. Eventually(func() int {
  112. generatorStates := &genv1alpha1.GeneratorStateList{}
  113. err := f.CRClient.List(GinkgoT().Context(), generatorStates, client.InNamespace(f.Namespace.Name))
  114. if err != nil {
  115. return -1
  116. }
  117. GinkgoLogr.Info("found generator states", "states", generatorStates.Items)
  118. return len(generatorStates.Items)
  119. }).WithPolling(time.Second * 1).WithTimeout(time.Minute * 2).Should(BeZero())
  120. }
  121. tokenIsUsable := func(tc *testCase) {
  122. tc.AfterSync = func(secret *v1.Secret) {
  123. // ensure token exists and is usable
  124. Expect(string(secret.Data["token"])).ToNot(BeEmpty())
  125. _, err := grafanaClient.Search.Search(&grafanasearch.SearchParams{
  126. Query: ptr.To(""),
  127. })
  128. Expect(err).ToNot(HaveOccurred())
  129. ensureExternalSecretPurgesGeneratorState(tc)
  130. }
  131. }
  132. cleanupServiceAccountsAfterDeletion := func(tc *testCase) {
  133. tc.ExternalSecret.Spec.RefreshInterval = &metav1.Duration{Duration: time.Second * 10}
  134. tc.AfterSync = func(secret *v1.Secret) {
  135. // Wait for ES to be rotated a couple of times,
  136. // this should create a couple of service accounts.
  137. // This allows us to verify that the service accounts are cleaned up
  138. // after the generator is deleted.
  139. Eventually(func() bool {
  140. generatorStates := &genv1alpha1.GeneratorStateList{}
  141. err := f.CRClient.List(GinkgoT().Context(), generatorStates, client.InNamespace(f.Namespace.Name))
  142. Expect(err).ToNot(HaveOccurred())
  143. GinkgoLogr.Info("generator states", "states", generatorStates.Items)
  144. return len(generatorStates.Items) > 2
  145. }).WithPolling(time.Second * 10).WithTimeout(time.Minute * 5).Should(BeTrue())
  146. ensureExternalSecretPurgesGeneratorState(tc)
  147. // ensure service accounts are cleaned up
  148. saList, err := grafanaClient.ServiceAccounts.SearchOrgServiceAccountsWithPaging(&grafanasa.SearchOrgServiceAccountsWithPagingParams{
  149. Perpage: ptr.To(int64(100)),
  150. Page: ptr.To(int64(1)),
  151. Query: ptr.To(f.Namespace.Name),
  152. })
  153. Expect(err).ToNot(HaveOccurred())
  154. Expect(saList.GetPayload().ServiceAccounts).To(HaveLen(1))
  155. tokens, err := grafanaClient.ServiceAccounts.ListTokensWithParams(&grafanasa.ListTokensParams{
  156. ServiceAccountID: saList.GetPayload().ServiceAccounts[0].ID,
  157. })
  158. Expect(err).ToNot(HaveOccurred())
  159. Expect(tokens.GetPayload()).To(BeEmpty())
  160. }
  161. }
  162. DescribeTable("generate secrets with grafana generator", generatorTableFunc,
  163. Entry("should generate a token that can be used to access the API", f, setupGenerator, tokenIsUsable),
  164. Entry("deleting a generator should cleanup the generated service accounts", f, setupGenerator, cleanupServiceAccountsAfterDeletion),
  165. )
  166. })
  167. func newGrafanaClient() *grafanaclient.GrafanaHTTPAPI {
  168. url := strings.TrimPrefix(os.Getenv("GRAFANA_URL"), "https://")
  169. return grafanaclient.NewHTTPClientWithConfig(nil, &grafanaclient.TransportConfig{
  170. Host: url,
  171. BasePath: "/api",
  172. Schemes: []string{"https"},
  173. APIKey: os.Getenv("GRAFANA_TOKEN"),
  174. NumRetries: 15,
  175. RetryTimeout: time.Second * 6,
  176. })
  177. }