client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  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. if data.GetSecretKey() == "" {
  87. return fmt.Errorf("pushing the whole secret is not yet implemented")
  88. }
  89. value := secret.Data[data.GetSecretKey()]
  90. scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
  91. if err != nil {
  92. return err
  93. }
  94. listSecretReq := &smapi.ListSecretsRequest{
  95. ProjectID: &c.projectID,
  96. Page: scw.Int32Ptr(1),
  97. PageSize: scw.Uint32Ptr(1),
  98. }
  99. var secretName string
  100. secretPath := "/"
  101. switch scwRef.RefType {
  102. case refTypeName:
  103. listSecretReq.Name = &scwRef.Value
  104. secretName = scwRef.Value
  105. case refTypePath:
  106. name, path, ok := splitNameAndPath(scwRef.Value)
  107. if !ok {
  108. return fmt.Errorf("ref is not a path")
  109. }
  110. listSecretReq.Name = &name
  111. listSecretReq.Path = &path
  112. secretName = name
  113. secretPath = path
  114. default:
  115. return fmt.Errorf("secrets can only be pushed by name or path")
  116. }
  117. var secretID string
  118. existingSecretVersion := int64(-1)
  119. // list secret by ref
  120. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  121. if err != nil {
  122. return err
  123. }
  124. // secret exists
  125. if len(listSecrets.Secrets) > 0 {
  126. secretID = listSecrets.Secrets[0].ID
  127. // get the latest version
  128. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  129. SecretID: secretID,
  130. Revision: "latest",
  131. }, scw.WithContext(ctx))
  132. if err != nil {
  133. var errNotNound *scw.ResourceNotFoundError
  134. if !errors.As(err, &errNotNound) {
  135. return err
  136. }
  137. } else {
  138. existingSecretVersion = int64(secretVersion.Revision)
  139. }
  140. if existingSecretVersion != -1 {
  141. data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  142. if err != nil {
  143. return err
  144. }
  145. if bytes.Equal(data, value) {
  146. // No change to push.
  147. return nil
  148. }
  149. }
  150. } else {
  151. secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
  152. ProjectID: c.projectID,
  153. Name: secretName,
  154. Path: &secretPath,
  155. }, scw.WithContext(ctx))
  156. if err != nil {
  157. return err
  158. }
  159. secretID = secret.ID
  160. }
  161. // Finally, we push the new secret version.
  162. createSecretVersionRequest := smapi.CreateSecretVersionRequest{
  163. SecretID: secretID,
  164. Data: value,
  165. }
  166. createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
  167. if err != nil {
  168. return err
  169. }
  170. c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
  171. if existingSecretVersion != -1 {
  172. _, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
  173. SecretID: secretID,
  174. Revision: fmt.Sprintf("%d", existingSecretVersion),
  175. })
  176. if err != nil {
  177. return err
  178. }
  179. }
  180. return nil
  181. }
  182. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
  183. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  184. if err != nil {
  185. return err
  186. }
  187. listSecretReq := &smapi.ListSecretsRequest{
  188. ProjectID: &c.projectID,
  189. Page: scw.Int32Ptr(1),
  190. PageSize: scw.Uint32Ptr(1),
  191. }
  192. switch scwRef.RefType {
  193. case refTypeName:
  194. listSecretReq.Name = &scwRef.Value
  195. case refTypePath:
  196. name, path, ok := splitNameAndPath(scwRef.Value)
  197. if !ok {
  198. return fmt.Errorf("ref is not a path")
  199. }
  200. listSecretReq.Name = &name
  201. listSecretReq.Path = &path
  202. default:
  203. return fmt.Errorf("secrets can only be deleted by name or path")
  204. }
  205. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  206. if err != nil {
  207. return err
  208. }
  209. if len(listSecrets.Secrets) == 0 {
  210. return nil
  211. }
  212. request := smapi.DeleteSecretRequest{
  213. SecretID: listSecrets.Secrets[0].ID,
  214. }
  215. err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
  216. if err != nil {
  217. return err
  218. }
  219. return nil
  220. }
  221. func (c *client) Validate() (esv1beta1.ValidationResult, error) {
  222. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  223. defer cancel()
  224. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  225. ProjectID: &c.projectID,
  226. Page: scw.Int32Ptr(1),
  227. PageSize: scw.Uint32Ptr(0),
  228. }, scw.WithContext(ctx))
  229. if err != nil {
  230. return esv1beta1.ValidationResultError, nil
  231. }
  232. return esv1beta1.ValidationResultReady, nil
  233. }
  234. func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  235. rawData, err := c.GetSecret(ctx, ref)
  236. if err != nil {
  237. return nil, err
  238. }
  239. structuredData := make(map[string]json.RawMessage)
  240. err = json.Unmarshal(rawData, &structuredData)
  241. if err != nil {
  242. return nil, err
  243. }
  244. values := make(map[string][]byte)
  245. for key, value := range structuredData {
  246. values[key] = jsonToSecretData(value)
  247. }
  248. return values, nil
  249. }
  250. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  251. func (c *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  252. request := smapi.ListSecretsRequest{
  253. ProjectID: &c.projectID,
  254. Page: scw.Int32Ptr(1),
  255. PageSize: scw.Uint32Ptr(50),
  256. }
  257. if ref.Path != nil {
  258. request.Path = ref.Path
  259. }
  260. var nameMatcher *find.Matcher
  261. if ref.Name != nil {
  262. var err error
  263. nameMatcher, err = find.New(*ref.Name)
  264. if err != nil {
  265. return nil, err
  266. }
  267. }
  268. for tag := range ref.Tags {
  269. request.Tags = append(request.Tags, tag)
  270. }
  271. results := map[string][]byte{}
  272. for done := false; !done; {
  273. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  274. if err != nil {
  275. return nil, err
  276. }
  277. totalFetched := uint64(*request.Page-1)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  278. done = totalFetched == uint64(response.TotalCount)
  279. *request.Page++
  280. for _, secret := range response.Secrets {
  281. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  282. continue
  283. }
  284. accessReq := smapi.AccessSecretVersionRequest{
  285. Region: secret.Region,
  286. SecretID: secret.ID,
  287. Revision: "latest_enabled",
  288. }
  289. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  290. if err != nil {
  291. log.Error(err, "failed to access secret")
  292. continue
  293. }
  294. results[secret.Name] = accessResp.Data
  295. }
  296. }
  297. return results, nil
  298. }
  299. func (c *client) Close(context.Context) error {
  300. return nil
  301. }
  302. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  303. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  304. if secretRef.RefType == refTypeID && len(versionSpec) > 0 && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  305. secretID := secretRef.Value
  306. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  307. if err == nil {
  308. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  309. }
  310. }
  311. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  312. request := &smapi.ListSecretsRequest{
  313. ProjectID: &c.projectID,
  314. Page: scw.Int32Ptr(1),
  315. PageSize: scw.Uint32Ptr(1),
  316. }
  317. switch secretRef.RefType {
  318. case refTypeID:
  319. request := smapi.GetSecretVersionRequest{
  320. SecretID: secretRef.Value,
  321. Revision: versionSpec,
  322. }
  323. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  324. if err != nil {
  325. return nil, err
  326. }
  327. return c.accessSpecificSecretVersion(ctx, response.SecretID, response.Revision)
  328. case refTypeName:
  329. request.Name = &secretRef.Value
  330. case refTypePath:
  331. name, path, ok := splitNameAndPath(secretRef.Value)
  332. if !ok {
  333. return nil, fmt.Errorf("ref is not a path")
  334. }
  335. request.Name = &name
  336. request.Path = &path
  337. default:
  338. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  339. }
  340. response, err := c.api.ListSecrets(request, scw.WithContext(ctx))
  341. if err != nil {
  342. return nil, err
  343. }
  344. if len(response.Secrets) == 0 {
  345. return nil, errNoSecretForName
  346. }
  347. secretID := response.Secrets[0].ID
  348. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  349. SecretID: secretID,
  350. Revision: versionSpec,
  351. }, scw.WithContext(ctx))
  352. if err != nil {
  353. return nil, err
  354. }
  355. return c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  356. }
  357. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  358. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  359. if cacheHit {
  360. return cachedValue, nil
  361. }
  362. request := smapi.AccessSecretVersionRequest{
  363. SecretID: secretID,
  364. Revision: fmt.Sprintf("%d", revision),
  365. }
  366. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  367. if err != nil {
  368. return nil, err
  369. }
  370. return response.Data, nil
  371. }
  372. func jsonToSecretData(value json.RawMessage) []byte {
  373. var stringValue string
  374. err := json.Unmarshal(value, &stringValue)
  375. if err == nil {
  376. return []byte(stringValue)
  377. }
  378. return []byte(strings.TrimSpace(string(value)))
  379. }
  380. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  381. result := gjson.Get(string(secretData), property)
  382. if !result.Exists() {
  383. return nil, esv1beta1.NoSecretError{}
  384. }
  385. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  386. }
  387. func splitNameAndPath(ref string) (name, path string, ok bool) {
  388. if !strings.HasPrefix(ref, "/") {
  389. return
  390. }
  391. s := strings.Split(ref, "/")
  392. name = s[len(s)-1]
  393. if len(s) == 2 {
  394. path = "/"
  395. } else {
  396. path = strings.Join(s[:len(s)-1], "/")
  397. }
  398. ok = true
  399. return
  400. }