client.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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 ngrok
  14. import (
  15. "context"
  16. "crypto/sha256"
  17. "encoding/hex"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "sync"
  22. "time"
  23. "github.com/ngrok/ngrok-api-go/v7"
  24. corev1 "k8s.io/api/core/v1"
  25. v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  26. "k8s.io/utils/ptr"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/pkg/utils/metadata"
  29. )
  30. const (
  31. defaultDescription = "Managed by External Secrets Operator"
  32. defaultListTimeout = 1 * time.Minute
  33. )
  34. var (
  35. errWriteOnlyOperations = errors.New("not implemented - the ngrok provider only supports write operations")
  36. errVaultDoesNotExist = errors.New("vault does not exist")
  37. errVaultSecretDoesNotExist = errors.New("vault secret does not exist")
  38. )
  39. type PushSecretMetadataSpec struct {
  40. // The description of the secret in the ngrok API.
  41. Description string `json:"description,omitempty"`
  42. // Custom metadata to be merged with generated metadata for the secret in the ngrok API.
  43. // This metadata is different from Kubernetes metadata.
  44. Metadata map[string]string `json:"metadata,omitempty"`
  45. }
  46. type VaultClient interface {
  47. Create(context.Context, *ngrok.VaultCreate) (*ngrok.Vault, error)
  48. Get(context.Context, string) (*ngrok.Vault, error)
  49. GetSecretsByVault(string, *ngrok.Paging) ngrok.Iter[*ngrok.Secret]
  50. List(*ngrok.Paging) ngrok.Iter[*ngrok.Vault]
  51. }
  52. type SecretsClient interface {
  53. Create(context.Context, *ngrok.SecretCreate) (*ngrok.Secret, error)
  54. Delete(context.Context, string) error
  55. Get(context.Context, string) (*ngrok.Secret, error)
  56. List(*ngrok.Paging) ngrok.Iter[*ngrok.Secret]
  57. Update(context.Context, *ngrok.SecretUpdate) (*ngrok.Secret, error)
  58. }
  59. type client struct {
  60. vaultClient VaultClient
  61. secretsClient SecretsClient
  62. vaultName string
  63. vaultID string
  64. vaultIDMu sync.RWMutex
  65. }
  66. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  67. // First, make sure the vault name still matches the ID we have stored. If not, we have to look it up again.
  68. err := c.verifyVaultNameStillMatchesID(ctx)
  69. if err != nil {
  70. return fmt.Errorf("failed to verify vault name still matches ID: %w", err)
  71. }
  72. // Prepare the secret data for pushing
  73. var value []byte
  74. // If key is specified, get the value from the secret data
  75. if data.GetSecretKey() != "" {
  76. var ok bool
  77. value, ok = secret.Data[data.GetSecretKey()]
  78. if !ok {
  79. return fmt.Errorf("key %s not found in secret", data.GetSecretKey())
  80. }
  81. } else { // otherwise, marshal the entire secret data as JSON
  82. value, err = json.Marshal(secret.Data)
  83. if err != nil {
  84. return fmt.Errorf("json.Marshal failed with error: %w", err)
  85. }
  86. }
  87. // Calculate the checksum of the value to add to metadata
  88. valueChecksum := sha256.Sum256(value)
  89. psmd, err := parseAndDefaultMetadata(data.GetMetadata())
  90. if err != nil {
  91. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  92. }
  93. psmd.Metadata["_sha256"] = hex.EncodeToString(valueChecksum[:])
  94. metadataJSON, err := json.Marshal(psmd.Metadata)
  95. if err != nil {
  96. return fmt.Errorf("failed to marshal metadata for ngrok api: %w", err)
  97. }
  98. // Check if the secret already exists in the vault
  99. existingSecret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), data.GetRemoteKey())
  100. if err != nil {
  101. if !errors.Is(err, errVaultSecretDoesNotExist) {
  102. return fmt.Errorf("failed to get secret: %w", err)
  103. }
  104. // If the secret does not exist, create it
  105. _, err = c.secretsClient.Create(ctx, &ngrok.SecretCreate{
  106. VaultID: c.getVaultID(),
  107. Name: data.GetRemoteKey(),
  108. Value: string(value),
  109. Metadata: string(metadataJSON),
  110. Description: psmd.Description,
  111. })
  112. return err
  113. }
  114. // If the secret exists, update it
  115. _, err = c.secretsClient.Update(ctx, &ngrok.SecretUpdate{
  116. ID: existingSecret.ID,
  117. Value: ptr.To(string(value)),
  118. Metadata: ptr.To(string(metadataJSON)),
  119. Description: ptr.To(psmd.Description),
  120. })
  121. return err
  122. }
  123. func (c *client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  124. err := c.verifyVaultNameStillMatchesID(ctx)
  125. if errors.Is(err, errVaultDoesNotExist) {
  126. return false, nil
  127. }
  128. if err != nil {
  129. return false, err
  130. }
  131. // Implementation for checking if a secret exists in ngrok
  132. secret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), ref.GetRemoteKey())
  133. if errors.Is(err, errVaultDoesNotExist) || errors.Is(err, errVaultSecretDoesNotExist) {
  134. return false, nil
  135. }
  136. if err != nil {
  137. return false, fmt.Errorf("error fetching secret: %w", err)
  138. }
  139. return (secret != nil), nil
  140. }
  141. // DeleteSecret deletes a secret from ngrok by its reference.
  142. func (c *client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  143. err := c.verifyVaultNameStillMatchesID(ctx)
  144. if errors.Is(err, errVaultDoesNotExist) {
  145. return nil
  146. } else if err != nil {
  147. return err
  148. }
  149. secret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), ref.GetRemoteKey())
  150. if errors.Is(err, errVaultDoesNotExist) || errors.Is(err, errVaultSecretDoesNotExist) {
  151. // If the secret or vault do not exist, we can consider it deleted.
  152. return nil
  153. }
  154. if err != nil {
  155. return err
  156. }
  157. if secret == nil {
  158. return nil
  159. }
  160. return c.secretsClient.Delete(ctx, secret.ID)
  161. }
  162. func (c *client) Validate() (esv1.ValidationResult, error) {
  163. // Validate the client can list secrets with a timeout. If we
  164. // can list secrets, we assume the client is valid(API keys, URL, etc.)
  165. iter := c.secretsClient.List(nil)
  166. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  167. defer cancel()
  168. for iter.Next(ctx) {
  169. return esv1.ValidationResultReady, nil
  170. }
  171. if iter.Err() != nil {
  172. return esv1.ValidationResultError, fmt.Errorf("store is not allowed to list secrets: %w", iter.Err())
  173. }
  174. return esv1.ValidationResultReady, nil
  175. }
  176. func (c *client) GetSecret(ctx context.Context, _ esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  177. // Implementation for getting a secret from ngrok
  178. return nil, errWriteOnlyOperations
  179. }
  180. func (c *client) GetSecretMap(ctx context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  181. // Implementation for getting a map of secrets from ngrok
  182. return nil, errWriteOnlyOperations
  183. }
  184. func (c *client) GetAllSecrets(ctx context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  185. // Implementation for getting all secrets from ngrok
  186. return nil, errWriteOnlyOperations
  187. }
  188. func (c *client) Close(_ context.Context) error {
  189. return nil
  190. }
  191. func (c *client) verifyVaultNameStillMatchesID(ctx context.Context) error {
  192. vaultID := c.getVaultID()
  193. if vaultID == "" {
  194. return c.refreshVaultID(ctx)
  195. }
  196. vault, err := c.vaultClient.Get(ctx, vaultID)
  197. if err != nil || vault.Name != c.vaultName {
  198. return c.refreshVaultID(ctx)
  199. }
  200. return nil
  201. }
  202. // getVaultID safely retrieves the current vault ID.
  203. func (c *client) getVaultID() string {
  204. c.vaultIDMu.RLock()
  205. defer c.vaultIDMu.RUnlock()
  206. return c.vaultID
  207. }
  208. // setVaultID safely sets the vault ID.
  209. func (c *client) setVaultID(vaultID string) {
  210. c.vaultIDMu.Lock()
  211. defer c.vaultIDMu.Unlock()
  212. c.vaultID = vaultID
  213. }
  214. func (c *client) refreshVaultID(ctx context.Context) error {
  215. v, err := c.getVaultByName(ctx, c.vaultName)
  216. if err != nil {
  217. return fmt.Errorf("failed to refresh vault ID: %w", err)
  218. }
  219. c.setVaultID(v.ID)
  220. return nil
  221. }
  222. func (c *client) getVaultByName(ctx context.Context, name string) (*ngrok.Vault, error) {
  223. listCtx, cancel := context.WithTimeout(ctx, defaultListTimeout)
  224. defer cancel()
  225. iter := c.vaultClient.List(nil)
  226. for iter.Next(listCtx) {
  227. vault := iter.Item()
  228. if vault.Name == name {
  229. return vault, nil
  230. }
  231. }
  232. if iter.Err() != nil {
  233. return nil, iter.Err()
  234. }
  235. return nil, errVaultDoesNotExist
  236. }
  237. // getSecretByVaultIDAndName retrieves a secret by its vault ID and secret name.
  238. func (c *client) getSecretByVaultIDAndName(ctx context.Context, vaultID, name string) (*ngrok.Secret, error) {
  239. iter := c.vaultClient.GetSecretsByVault(vaultID, nil)
  240. for iter.Next(ctx) {
  241. secret := iter.Item()
  242. if secret.Name == name {
  243. return secret, nil
  244. }
  245. }
  246. if iter.Err() != nil {
  247. return nil, iter.Err()
  248. }
  249. return nil, fmt.Errorf("secret '%s' does not exist: %w", name, errVaultSecretDoesNotExist)
  250. }
  251. func parseAndDefaultMetadata(data *v1.JSON) (PushSecretMetadataSpec, error) {
  252. def := PushSecretMetadataSpec{
  253. Description: defaultDescription,
  254. Metadata: make(map[string]string),
  255. }
  256. res, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](data)
  257. if err != nil {
  258. return def, err
  259. }
  260. if res == nil {
  261. return def, nil
  262. }
  263. if res.Spec.Description != "" {
  264. def.Description = res.Spec.Description
  265. }
  266. if res.Spec.Metadata != nil {
  267. def.Metadata = res.Spec.Metadata
  268. }
  269. return def, nil
  270. }