client.go 9.5 KB

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