client.go 11 KB

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