client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package scaleway
  13. import (
  14. "bytes"
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "strconv"
  20. "strings"
  21. "time"
  22. smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1alpha1"
  23. "github.com/scaleway/scaleway-sdk-go/scw"
  24. "github.com/tidwall/gjson"
  25. corev1 "k8s.io/api/core/v1"
  26. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  27. "github.com/external-secrets/external-secrets/pkg/find"
  28. )
  29. var errNoSecretForName = errors.New("no secret for this name")
  30. type client struct {
  31. api secretAPI
  32. projectID string
  33. cache cache
  34. }
  35. const (
  36. refTypeName = "name"
  37. refTypeID = "id"
  38. refTypePath = "path"
  39. )
  40. type scwSecretRef struct {
  41. RefType string
  42. Value string
  43. }
  44. func (r scwSecretRef) String() string {
  45. return fmt.Sprintf("%s:%s", r.RefType, r.Value)
  46. }
  47. func decodeScwSecretRef(key string) (*scwSecretRef, error) {
  48. sepIndex := strings.IndexRune(key, ':')
  49. if sepIndex < 0 {
  50. return nil, fmt.Errorf("invalid secret reference: missing colon ':'")
  51. }
  52. return &scwSecretRef{
  53. RefType: key[:sepIndex],
  54. Value: key[sepIndex+1:],
  55. }, nil
  56. }
  57. func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  58. scwRef, err := decodeScwSecretRef(ref.Key)
  59. if err != nil {
  60. return nil, err
  61. }
  62. versionSpec := "latest_enabled"
  63. if ref.Version != "" {
  64. versionSpec = ref.Version
  65. }
  66. value, err := c.accessSecretVersion(ctx, scwRef, versionSpec)
  67. if err != nil {
  68. //nolint:errorlint
  69. if _, isNotFoundErr := err.(*scw.ResourceNotFoundError); isNotFoundErr {
  70. return nil, esv1beta1.NoSecretError{}
  71. } else if errors.Is(err, errNoSecretForName) {
  72. return nil, esv1beta1.NoSecretError{}
  73. }
  74. return nil, err
  75. }
  76. if ref.Property != "" {
  77. extracted, err := extractJSONProperty(value, ref.Property)
  78. if err != nil {
  79. return nil, err
  80. }
  81. value = extracted
  82. }
  83. return value, nil
  84. }
  85. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
  86. value := secret.Data[data.GetSecretKey()]
  87. scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
  88. if err != nil {
  89. return err
  90. }
  91. listSecretReq := &smapi.ListSecretsRequest{
  92. ProjectID: &c.projectID,
  93. Page: scw.Int32Ptr(1),
  94. PageSize: scw.Uint32Ptr(1),
  95. }
  96. var secretName string
  97. secretPath := "/"
  98. switch scwRef.RefType {
  99. case refTypeName:
  100. listSecretReq.Name = &scwRef.Value
  101. secretName = scwRef.Value
  102. case refTypePath:
  103. name, path, ok := splitNameAndPath(scwRef.Value)
  104. if !ok {
  105. return fmt.Errorf("ref is not a path")
  106. }
  107. listSecretReq.Name = &name
  108. listSecretReq.Path = &path
  109. secretName = name
  110. secretPath = path
  111. default:
  112. return fmt.Errorf("secrets can only be pushed by name or path")
  113. }
  114. var secretID string
  115. existingSecretVersion := int64(-1)
  116. // list secret by ref
  117. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  118. if err != nil {
  119. return err
  120. }
  121. // secret exists
  122. if len(listSecrets.Secrets) > 0 {
  123. secretID = listSecrets.Secrets[0].ID
  124. // get the latest version
  125. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  126. SecretID: secretID,
  127. Revision: "latest",
  128. }, scw.WithContext(ctx))
  129. if err != nil {
  130. var errNotNound *scw.ResourceNotFoundError
  131. if !errors.As(err, &errNotNound) {
  132. return err
  133. }
  134. } else {
  135. existingSecretVersion = int64(secretVersion.Revision)
  136. }
  137. if existingSecretVersion != -1 {
  138. data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  139. if err != nil {
  140. return err
  141. }
  142. if bytes.Equal(data, value) {
  143. // No change to push.
  144. return nil
  145. }
  146. }
  147. } else {
  148. secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
  149. ProjectID: c.projectID,
  150. Name: secretName,
  151. Path: &secretPath,
  152. }, scw.WithContext(ctx))
  153. if err != nil {
  154. return err
  155. }
  156. secretID = secret.ID
  157. }
  158. // Finally, we push the new secret version.
  159. createSecretVersionRequest := smapi.CreateSecretVersionRequest{
  160. SecretID: secretID,
  161. Data: value,
  162. }
  163. createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
  164. if err != nil {
  165. return err
  166. }
  167. c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
  168. if existingSecretVersion != -1 {
  169. _, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
  170. SecretID: secretID,
  171. Revision: fmt.Sprintf("%d", existingSecretVersion),
  172. })
  173. if err != nil {
  174. return err
  175. }
  176. }
  177. return nil
  178. }
  179. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
  180. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  181. if err != nil {
  182. return err
  183. }
  184. listSecretReq := &smapi.ListSecretsRequest{
  185. ProjectID: &c.projectID,
  186. Page: scw.Int32Ptr(1),
  187. PageSize: scw.Uint32Ptr(1),
  188. }
  189. switch scwRef.RefType {
  190. case refTypeName:
  191. listSecretReq.Name = &scwRef.Value
  192. case refTypePath:
  193. name, path, ok := splitNameAndPath(scwRef.Value)
  194. if !ok {
  195. return fmt.Errorf("ref is not a path")
  196. }
  197. listSecretReq.Name = &name
  198. listSecretReq.Path = &path
  199. default:
  200. return fmt.Errorf("secrets can only be deleted by name or path")
  201. }
  202. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  203. if err != nil {
  204. return err
  205. }
  206. if len(listSecrets.Secrets) == 0 {
  207. return nil
  208. }
  209. request := smapi.DeleteSecretRequest{
  210. SecretID: listSecrets.Secrets[0].ID,
  211. }
  212. err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
  213. if err != nil {
  214. return err
  215. }
  216. return nil
  217. }
  218. func (c *client) Validate() (esv1beta1.ValidationResult, error) {
  219. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  220. defer cancel()
  221. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  222. ProjectID: &c.projectID,
  223. Page: scw.Int32Ptr(1),
  224. PageSize: scw.Uint32Ptr(0),
  225. }, scw.WithContext(ctx))
  226. if err != nil {
  227. return esv1beta1.ValidationResultError, nil
  228. }
  229. return esv1beta1.ValidationResultReady, nil
  230. }
  231. func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  232. rawData, err := c.GetSecret(ctx, ref)
  233. if err != nil {
  234. return nil, err
  235. }
  236. structuredData := make(map[string]json.RawMessage)
  237. err = json.Unmarshal(rawData, &structuredData)
  238. if err != nil {
  239. return nil, err
  240. }
  241. values := make(map[string][]byte)
  242. for key, value := range structuredData {
  243. values[key] = jsonToSecretData(value)
  244. }
  245. return values, nil
  246. }
  247. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  248. func (c *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  249. request := smapi.ListSecretsRequest{
  250. ProjectID: &c.projectID,
  251. Page: scw.Int32Ptr(1),
  252. PageSize: scw.Uint32Ptr(50),
  253. }
  254. if ref.Path != nil {
  255. request.Path = ref.Path
  256. }
  257. var nameMatcher *find.Matcher
  258. if ref.Name != nil {
  259. var err error
  260. nameMatcher, err = find.New(*ref.Name)
  261. if err != nil {
  262. return nil, err
  263. }
  264. }
  265. for tag := range ref.Tags {
  266. request.Tags = append(request.Tags, tag)
  267. }
  268. results := map[string][]byte{}
  269. for done := false; !done; {
  270. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  271. if err != nil {
  272. return nil, err
  273. }
  274. totalFetched := uint64(*request.Page-1)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  275. done = totalFetched == uint64(response.TotalCount)
  276. *request.Page++
  277. for _, secret := range response.Secrets {
  278. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  279. continue
  280. }
  281. accessReq := smapi.AccessSecretVersionRequest{
  282. Region: secret.Region,
  283. SecretID: secret.ID,
  284. Revision: "latest_enabled",
  285. }
  286. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  287. if err != nil {
  288. log.Error(err, "failed to access secret")
  289. continue
  290. }
  291. results[secret.Name] = accessResp.Data
  292. }
  293. }
  294. return results, nil
  295. }
  296. func (c *client) Close(context.Context) error {
  297. return nil
  298. }
  299. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  300. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  301. if secretRef.RefType == refTypeID && len(versionSpec) > 0 && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  302. secretID := secretRef.Value
  303. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  304. if err == nil {
  305. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  306. }
  307. }
  308. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  309. request := &smapi.ListSecretsRequest{
  310. ProjectID: &c.projectID,
  311. Page: scw.Int32Ptr(1),
  312. PageSize: scw.Uint32Ptr(1),
  313. }
  314. switch secretRef.RefType {
  315. case refTypeID:
  316. request := smapi.GetSecretVersionRequest{
  317. SecretID: secretRef.Value,
  318. Revision: versionSpec,
  319. }
  320. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  321. if err != nil {
  322. return nil, err
  323. }
  324. return c.accessSpecificSecretVersion(ctx, response.SecretID, response.Revision)
  325. case refTypeName:
  326. request.Name = &secretRef.Value
  327. case refTypePath:
  328. name, path, ok := splitNameAndPath(secretRef.Value)
  329. if !ok {
  330. return nil, fmt.Errorf("ref is not a path")
  331. }
  332. request.Name = &name
  333. request.Path = &path
  334. default:
  335. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  336. }
  337. response, err := c.api.ListSecrets(request, scw.WithContext(ctx))
  338. if err != nil {
  339. return nil, err
  340. }
  341. if len(response.Secrets) == 0 {
  342. return nil, errNoSecretForName
  343. }
  344. secretID := response.Secrets[0].ID
  345. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  346. SecretID: secretID,
  347. Revision: versionSpec,
  348. }, scw.WithContext(ctx))
  349. if err != nil {
  350. return nil, err
  351. }
  352. return c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  353. }
  354. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  355. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  356. if cacheHit {
  357. return cachedValue, nil
  358. }
  359. request := smapi.AccessSecretVersionRequest{
  360. SecretID: secretID,
  361. Revision: fmt.Sprintf("%d", revision),
  362. }
  363. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  364. if err != nil {
  365. return nil, err
  366. }
  367. return response.Data, nil
  368. }
  369. func jsonToSecretData(value json.RawMessage) []byte {
  370. var stringValue string
  371. err := json.Unmarshal(value, &stringValue)
  372. if err == nil {
  373. return []byte(stringValue)
  374. }
  375. return []byte(strings.TrimSpace(string(value)))
  376. }
  377. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  378. result := gjson.Get(string(secretData), property)
  379. if !result.Exists() {
  380. return nil, esv1beta1.NoSecretError{}
  381. }
  382. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  383. }
  384. func splitNameAndPath(ref string) (name, path string, ok bool) {
  385. if !strings.HasPrefix(ref, "/") {
  386. return
  387. }
  388. s := strings.Split(ref, "/")
  389. name = s[len(s)-1]
  390. if len(s) == 2 {
  391. path = "/"
  392. } else {
  393. path = strings.Join(s[:len(s)-1], "/")
  394. }
  395. ok = true
  396. return
  397. }