client.go 11 KB

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