client_push_secret.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  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 ovh
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "reflect"
  20. "github.com/google/uuid"
  21. "github.com/ovh/okms-sdk-go/types"
  22. corev1 "k8s.io/api/core/v1"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. )
  25. const pushSecretError = "failed to push secret at path"
  26. // PushSecret pushes a secret to the Secret Manager according to the updatePolicy
  27. // defined in the PushSecret (create or update).
  28. func (cl *ovhClient) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  29. remoteKey := data.GetRemoteKey()
  30. if secret == nil {
  31. return newPushSecretValidationError(remoteKey, "provided secret is nil")
  32. }
  33. if len(secret.Data) == 0 {
  34. return newPushSecretValidationError(remoteKey, "provided secret is empty")
  35. }
  36. // Check if the secret already exists.
  37. // This determines which method to use: create or update.
  38. remoteSecret, currentVersion, err := cl.getSecretWithOvhSDK(ctx, cl.okmsID, esv1.ExternalSecretDataRemoteRef{
  39. Key: remoteKey,
  40. })
  41. noSecretErr := errors.Is(err, esv1.NoSecretErr)
  42. if err != nil && !noSecretErr {
  43. return wrapPushSecretError(remoteKey, err)
  44. }
  45. secretExists := !noSecretErr
  46. // Build the secret to be pushed.
  47. secretToPush, err := buildSecretToPush(secret, data)
  48. if err != nil {
  49. return wrapPushSecretError(remoteKey, err)
  50. }
  51. // Compare the data of secretToPush with that of remoteSecret.
  52. equal, err := compareSecretsData(secretToPush, remoteSecret)
  53. if err != nil {
  54. return wrapPushSecretError(remoteKey, err)
  55. }
  56. if equal {
  57. return nil
  58. }
  59. // Set cas according to client configuration
  60. if !cl.cas {
  61. currentVersion = nil
  62. }
  63. // Push the secret.
  64. err = pushNewSecret(ctx, cl.okmsClient, cl.okmsID, secretToPush, remoteKey, currentVersion, secretExists)
  65. if err != nil {
  66. return wrapPushSecretError(remoteKey, err)
  67. }
  68. return nil
  69. }
  70. func wrapPushSecretError(remoteKey string, err error) error {
  71. return fmt.Errorf("%s %q: %w", pushSecretError, remoteKey, err)
  72. }
  73. func newPushSecretValidationError(remoteKey, msg string) error {
  74. return fmt.Errorf("%s %q: %s", pushSecretError, remoteKey, msg)
  75. }
  76. // Compare the secret to push with the remote secret.
  77. // If they are equal, do not push the secret.
  78. func compareSecretsData(secretToPush map[string]any, remoteSecret []byte) (bool, error) {
  79. if len(remoteSecret) == 0 {
  80. return false, nil
  81. }
  82. localBytes, err := json.Marshal(secretToPush)
  83. if err != nil {
  84. return false, fmt.Errorf("could not compare remote secret with secret to push: %w", err)
  85. }
  86. var localSecretMap, remoteSecretMap any
  87. if err := json.Unmarshal(localBytes, &localSecretMap); err != nil {
  88. return false, fmt.Errorf("could not normalize local secret for comparison: %w", err)
  89. }
  90. if err := json.Unmarshal(remoteSecret, &remoteSecretMap); err != nil {
  91. return false, fmt.Errorf("could not normalize remote secret for comparison: %w", err)
  92. }
  93. return reflect.DeepEqual(localSecretMap, remoteSecretMap), nil
  94. }
  95. // Build the secret to be pushed.
  96. //
  97. // If remoteProperty is defined, it will be used as the key to store the secret value.
  98. // If secretKey is not defined, the entire secret value will be pushed.
  99. // Otherwise, only the value of the specified secretKey will be pushed.
  100. func buildSecretToPush(secret *corev1.Secret, data esv1.PushSecretData) (map[string]any, error) {
  101. // Retrieve the secret value to push based on secretKey.
  102. var secretValueToPush map[string]any
  103. var err error
  104. secretValueToPush, err = extractSecretValue(secret.Data, data.GetSecretKey())
  105. if err != nil {
  106. return map[string]any{}, err
  107. }
  108. // Build the secret to push using remoteProperty.
  109. secretToPush := make(map[string]any)
  110. property := data.GetProperty()
  111. if property == "" {
  112. secretToPush = secretValueToPush
  113. } else {
  114. secretToPush[property] = secretValueToPush
  115. }
  116. return secretToPush, nil
  117. }
  118. func extractSecretValue(data map[string][]byte, secretKey string) (map[string]any, error) {
  119. var err error
  120. secretValueToPush := make(map[string]any)
  121. if secretKey != "" {
  122. err = extractSecretKeyValue(data, secretValueToPush, secretKey)
  123. return secretValueToPush, err
  124. }
  125. for key := range data {
  126. err = extractSecretKeyValue(data, secretValueToPush, key)
  127. if err != nil {
  128. return nil, err
  129. }
  130. }
  131. return secretValueToPush, nil
  132. }
  133. func extractSecretKeyValue(data map[string][]byte, secretValueToPush map[string]any, secretKey string) error {
  134. value, ok := data[secretKey]
  135. if !ok {
  136. return fmt.Errorf(
  137. "could not extract secret key value to push: secretKey %q not found in secret data", secretKey,
  138. )
  139. }
  140. var decoded any
  141. if json.Unmarshal(value, &decoded) != nil {
  142. secretValueToPush[secretKey] = string(value)
  143. } else {
  144. secretValueToPush[secretKey] = json.RawMessage(value)
  145. }
  146. return nil
  147. }
  148. // This pushes the created/updated secret.
  149. func pushNewSecret(ctx context.Context, okmsClient OkmsClient, okmsID uuid.UUID, secretToPush map[string]any, path string, cas *uint32, secretExists bool) error {
  150. var err error
  151. if !secretExists {
  152. // Create a secret.
  153. _, err = okmsClient.PostSecretV2(ctx, okmsID, types.PostSecretV2Request{
  154. Path: path,
  155. Version: types.SecretV2VersionShort{
  156. Data: &secretToPush,
  157. },
  158. })
  159. if err != nil {
  160. return fmt.Errorf("could not create remote secret: %w", err)
  161. }
  162. return nil
  163. }
  164. // Update a secret.
  165. _, err = okmsClient.PutSecretV2(ctx, okmsID, path, cas, types.PutSecretV2Request{
  166. Version: &types.SecretV2VersionShort{
  167. Data: &secretToPush,
  168. },
  169. })
  170. if err != nil {
  171. return fmt.Errorf("could not update remote secret: %w", err)
  172. }
  173. return nil
  174. }