client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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/v1beta1"
  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, errors.New("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 errors.New("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 errors.New("ref is not a path")
  109. }
  110. listSecretReq.Name = &name
  111. listSecretReq.Path = &path
  112. secretName = name
  113. secretPath = path
  114. default:
  115. return errors.New("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 errors.New("ref is not a path")
  199. }
  200. listSecretReq.Name = &name
  201. listSecretReq.Path = &path
  202. default:
  203. return errors.New("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) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
  222. return false, errors.New("not implemented")
  223. }
  224. func (c *client) Validate() (esv1beta1.ValidationResult, error) {
  225. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  226. defer cancel()
  227. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  228. ProjectID: &c.projectID,
  229. Page: scw.Int32Ptr(1),
  230. PageSize: scw.Uint32Ptr(0),
  231. }, scw.WithContext(ctx))
  232. if err != nil {
  233. return esv1beta1.ValidationResultError, nil
  234. }
  235. return esv1beta1.ValidationResultReady, nil
  236. }
  237. func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  238. rawData, err := c.GetSecret(ctx, ref)
  239. if err != nil {
  240. return nil, err
  241. }
  242. structuredData := make(map[string]json.RawMessage)
  243. err = json.Unmarshal(rawData, &structuredData)
  244. if err != nil {
  245. return nil, err
  246. }
  247. values := make(map[string][]byte)
  248. for key, value := range structuredData {
  249. values[key] = jsonToSecretData(value)
  250. }
  251. return values, nil
  252. }
  253. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  254. func (c *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  255. request := smapi.ListSecretsRequest{
  256. ProjectID: &c.projectID,
  257. Page: scw.Int32Ptr(1),
  258. PageSize: scw.Uint32Ptr(50),
  259. }
  260. if ref.Path != nil {
  261. request.Path = ref.Path
  262. }
  263. var nameMatcher *find.Matcher
  264. if ref.Name != nil {
  265. var err error
  266. nameMatcher, err = find.New(*ref.Name)
  267. if err != nil {
  268. return nil, err
  269. }
  270. }
  271. for tag := range ref.Tags {
  272. request.Tags = append(request.Tags, tag)
  273. }
  274. results := map[string][]byte{}
  275. for done := false; !done; {
  276. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  277. if err != nil {
  278. return nil, err
  279. }
  280. totalFetched := uint64(*request.Page-1)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  281. done = totalFetched == response.TotalCount
  282. *request.Page++
  283. for _, secret := range response.Secrets {
  284. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  285. continue
  286. }
  287. accessReq := smapi.AccessSecretVersionRequest{
  288. Region: secret.Region,
  289. SecretID: secret.ID,
  290. Revision: "latest_enabled",
  291. }
  292. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  293. if err != nil {
  294. log.Error(err, "failed to access secret")
  295. continue
  296. }
  297. results[secret.Name] = accessResp.Data
  298. }
  299. }
  300. return results, nil
  301. }
  302. func (c *client) Close(context.Context) error {
  303. return nil
  304. }
  305. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  306. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  307. if secretRef.RefType == refTypeID && versionSpec != "" && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  308. secretID := secretRef.Value
  309. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  310. if err == nil {
  311. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  312. }
  313. }
  314. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  315. request := &smapi.ListSecretsRequest{
  316. ProjectID: &c.projectID,
  317. Page: scw.Int32Ptr(1),
  318. PageSize: scw.Uint32Ptr(1),
  319. }
  320. switch secretRef.RefType {
  321. case refTypeID:
  322. request := smapi.GetSecretVersionRequest{
  323. SecretID: secretRef.Value,
  324. Revision: versionSpec,
  325. }
  326. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  327. if err != nil {
  328. return nil, err
  329. }
  330. return c.accessSpecificSecretVersion(ctx, response.SecretID, response.Revision)
  331. case refTypeName:
  332. request.Name = &secretRef.Value
  333. case refTypePath:
  334. name, path, ok := splitNameAndPath(secretRef.Value)
  335. if !ok {
  336. return nil, errors.New("ref is not a path")
  337. }
  338. request.Name = &name
  339. request.Path = &path
  340. default:
  341. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  342. }
  343. response, err := c.api.ListSecrets(request, scw.WithContext(ctx))
  344. if err != nil {
  345. return nil, err
  346. }
  347. if len(response.Secrets) == 0 {
  348. return nil, errNoSecretForName
  349. }
  350. secretID := response.Secrets[0].ID
  351. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  352. SecretID: secretID,
  353. Revision: versionSpec,
  354. }, scw.WithContext(ctx))
  355. if err != nil {
  356. return nil, err
  357. }
  358. return c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  359. }
  360. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  361. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  362. if cacheHit {
  363. return cachedValue, nil
  364. }
  365. request := smapi.AccessSecretVersionRequest{
  366. SecretID: secretID,
  367. Revision: fmt.Sprintf("%d", revision),
  368. }
  369. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  370. if err != nil {
  371. return nil, err
  372. }
  373. return response.Data, nil
  374. }
  375. func jsonToSecretData(value json.RawMessage) []byte {
  376. var stringValue string
  377. err := json.Unmarshal(value, &stringValue)
  378. if err == nil {
  379. return []byte(stringValue)
  380. }
  381. return []byte(strings.TrimSpace(string(value)))
  382. }
  383. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  384. result := gjson.Get(string(secretData), property)
  385. if !result.Exists() {
  386. return nil, esv1beta1.NoSecretError{}
  387. }
  388. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  389. }
  390. func splitNameAndPath(ref string) (name, path string, ok bool) {
  391. if !strings.HasPrefix(ref, "/") {
  392. return
  393. }
  394. s := strings.Split(ref, "/")
  395. name = s[len(s)-1]
  396. if len(s) == 2 {
  397. path = "/"
  398. } else {
  399. path = strings.Join(s[:len(s)-1], "/")
  400. }
  401. ok = true
  402. return
  403. }