grafana.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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 grafana provides functionality for generating Grafana service account tokens.
  14. package grafana
  15. import (
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "net/url"
  20. "strings"
  21. "github.com/google/uuid"
  22. grafanaclient "github.com/grafana/grafana-openapi-client-go/client"
  23. grafanasa "github.com/grafana/grafana-openapi-client-go/client/service_accounts"
  24. "github.com/grafana/grafana-openapi-client-go/models"
  25. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  26. "k8s.io/utils/ptr"
  27. "sigs.k8s.io/controller-runtime/pkg/client"
  28. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  29. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  30. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  31. )
  32. // Grafana implements token generation for Grafana service accounts.
  33. type Grafana struct{}
  34. // Generate creates a new Grafana service account token using the provided configuration.
  35. func (w *Grafana) Generate(ctx context.Context, jsonSpec *apiextensions.JSON, kclient client.Client, ns string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
  36. gen, err := parseSpec(jsonSpec.Raw)
  37. if err != nil {
  38. return nil, nil, err
  39. }
  40. cl, err := newClient(ctx, gen, kclient, ns)
  41. if err != nil {
  42. return nil, nil, err
  43. }
  44. state, err := createOrGetServiceAccount(cl, gen)
  45. if err != nil {
  46. return nil, nil, err
  47. }
  48. // create new token
  49. res, err := cl.ServiceAccounts.CreateToken(&grafanasa.CreateTokenParams{
  50. ServiceAccountID: *state.ServiceAccount.ServiceAccountID,
  51. Body: &models.AddServiceAccountTokenCommand{
  52. Name: uuid.New().String(),
  53. },
  54. }, nil)
  55. if err != nil {
  56. return nil, nil, err
  57. }
  58. state.ServiceAccount.ServiceAccountTokenID = ptr.To(res.Payload.ID)
  59. return tokenResponse(state, res.Payload.Key)
  60. }
  61. // Cleanup handles any necessary cleanup after token generation.
  62. func (w *Grafana) Cleanup(ctx context.Context, jsonSpec *apiextensions.JSON, previousStatus genv1alpha1.GeneratorProviderState, kclient client.Client, ns string) error {
  63. if previousStatus == nil {
  64. return fmt.Errorf("missing previous status")
  65. }
  66. status, err := parseStatus(previousStatus.Raw)
  67. if err != nil {
  68. return err
  69. }
  70. gen, err := parseSpec(jsonSpec.Raw)
  71. if err != nil {
  72. return err
  73. }
  74. cl, err := newClient(ctx, gen, kclient, ns)
  75. if err != nil {
  76. return err
  77. }
  78. _, err = cl.ServiceAccounts.DeleteToken(*status.ServiceAccount.ServiceAccountTokenID, *status.ServiceAccount.ServiceAccountID)
  79. if err != nil && !strings.Contains(err.Error(), "service account token not found") {
  80. return err
  81. }
  82. return nil
  83. }
  84. func newClient(ctx context.Context, gen *genv1alpha1.Grafana, kclient client.Client, ns string) (*grafanaclient.GrafanaHTTPAPI, error) {
  85. parsedURL, err := url.Parse(gen.Spec.URL)
  86. if err != nil {
  87. return nil, err
  88. }
  89. cfg := &grafanaclient.TransportConfig{
  90. Host: parsedURL.Host,
  91. BasePath: parsedURL.JoinPath("/api").Path,
  92. Schemes: []string{parsedURL.Scheme},
  93. }
  94. if err := setGrafanaClientCredentials(ctx, gen, kclient, ns, cfg); err != nil {
  95. return nil, err
  96. }
  97. return grafanaclient.NewHTTPClientWithConfig(nil, cfg), nil
  98. }
  99. func setGrafanaClientCredentials(ctx context.Context, gen *genv1alpha1.Grafana, kclient client.Client, ns string, cfg *grafanaclient.TransportConfig) error {
  100. // First try to use service account auth
  101. if gen.Spec.Auth.Token != nil {
  102. serviceAccountAPIKey, err := resolvers.SecretKeyRef(ctx, kclient, resolvers.EmptyStoreKind, ns, &esmeta.SecretKeySelector{
  103. Namespace: &ns,
  104. Name: gen.Spec.Auth.Token.Name,
  105. Key: gen.Spec.Auth.Token.Key,
  106. })
  107. if err != nil {
  108. return err
  109. }
  110. cfg.APIKey = serviceAccountAPIKey
  111. return nil
  112. }
  113. // Next try to use basic auth
  114. if gen.Spec.Auth.Basic != nil {
  115. basicAuthPassword, err := resolvers.SecretKeyRef(ctx, kclient, resolvers.EmptyStoreKind, ns, &esmeta.SecretKeySelector{
  116. Namespace: &ns,
  117. Name: gen.Spec.Auth.Basic.Password.Name,
  118. Key: gen.Spec.Auth.Basic.Password.Key,
  119. })
  120. if err != nil {
  121. return err
  122. }
  123. cfg.BasicAuth = url.UserPassword(gen.Spec.Auth.Basic.Username, basicAuthPassword)
  124. return nil
  125. }
  126. // No auth found, fail
  127. return fmt.Errorf("no auth configuration found")
  128. }
  129. func createOrGetServiceAccount(cl *grafanaclient.GrafanaHTTPAPI, gen *genv1alpha1.Grafana) (*genv1alpha1.GrafanaServiceAccountTokenState, error) {
  130. saList, err := cl.ServiceAccounts.SearchOrgServiceAccountsWithPaging(&grafanasa.SearchOrgServiceAccountsWithPagingParams{
  131. Query: ptr.To(gen.Spec.ServiceAccount.Name),
  132. })
  133. if err != nil {
  134. return nil, err
  135. }
  136. for _, sa := range saList.Payload.ServiceAccounts {
  137. if sa.Name == gen.Spec.ServiceAccount.Name {
  138. return &genv1alpha1.GrafanaServiceAccountTokenState{
  139. ServiceAccount: genv1alpha1.GrafanaStateServiceAccount{
  140. ServiceAccountID: &sa.ID,
  141. ServiceAccountLogin: &sa.Login,
  142. },
  143. }, nil
  144. }
  145. }
  146. res, err := cl.ServiceAccounts.CreateServiceAccount(&grafanasa.CreateServiceAccountParams{
  147. Body: &models.CreateServiceAccountForm{
  148. Name: gen.Spec.ServiceAccount.Name,
  149. Role: gen.Spec.ServiceAccount.Role,
  150. },
  151. }, nil)
  152. if err != nil {
  153. return nil, err
  154. }
  155. return &genv1alpha1.GrafanaServiceAccountTokenState{
  156. ServiceAccount: genv1alpha1.GrafanaStateServiceAccount{
  157. ServiceAccountID: ptr.To(res.Payload.ID),
  158. ServiceAccountLogin: &res.Payload.Login,
  159. },
  160. }, nil
  161. }
  162. func tokenResponse(state *genv1alpha1.GrafanaServiceAccountTokenState, token string) (map[string][]byte, genv1alpha1.GeneratorProviderState, error) {
  163. newStateJSON, err := json.Marshal(state)
  164. if err != nil {
  165. return nil, nil, err
  166. }
  167. return map[string][]byte{
  168. "login": []byte(*state.ServiceAccount.ServiceAccountLogin),
  169. "token": []byte(token),
  170. }, &apiextensions.JSON{Raw: newStateJSON}, nil
  171. }
  172. func parseSpec(data []byte) (*genv1alpha1.Grafana, error) {
  173. var spec genv1alpha1.Grafana
  174. err := json.Unmarshal(data, &spec)
  175. return &spec, err
  176. }
  177. func parseStatus(data []byte) (*genv1alpha1.GrafanaServiceAccountTokenState, error) {
  178. var state genv1alpha1.GrafanaServiceAccountTokenState
  179. err := json.Unmarshal(data, &state)
  180. if err != nil {
  181. return nil, err
  182. }
  183. return &state, err
  184. }
  185. // NewGenerator creates a new Generator instance.
  186. func NewGenerator() genv1alpha1.Generator {
  187. return &Grafana{}
  188. }
  189. // Kind returns the generator kind.
  190. func Kind() string {
  191. return string(genv1alpha1.GeneratorKindGrafana)
  192. }