client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  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. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  26. "github.com/external-secrets/external-secrets/pkg/find"
  27. )
  28. var errNoSecretForName = errors.New("no secret for this name")
  29. type client struct {
  30. api secretAPI
  31. projectID string
  32. cache cache
  33. }
  34. const (
  35. refTypeName = "name"
  36. refTypeID = "id"
  37. )
  38. type scwSecretRef struct {
  39. RefType string
  40. Value string
  41. }
  42. func (r scwSecretRef) String() string {
  43. return fmt.Sprintf("%s:%s", r.RefType, r.Value)
  44. }
  45. func decodeScwSecretRef(key string) (*scwSecretRef, error) {
  46. sepIndex := strings.IndexRune(key, ':')
  47. if sepIndex < 0 {
  48. return nil, fmt.Errorf("invalid secret reference: missing colon ':'")
  49. }
  50. return &scwSecretRef{
  51. RefType: key[:sepIndex],
  52. Value: key[sepIndex+1:],
  53. }, nil
  54. }
  55. func (c *client) getSecretByName(ctx context.Context, name string) (*smapi.Secret, error) {
  56. request := smapi.GetSecretByNameRequest{
  57. SecretName: name,
  58. }
  59. response, err := c.api.GetSecretByName(&request, scw.WithContext(ctx))
  60. if err != nil {
  61. //nolint:errorlint
  62. if _, isErrNotFound := err.(*scw.ResourceNotFoundError); isErrNotFound {
  63. return nil, errNoSecretForName
  64. }
  65. return nil, err
  66. }
  67. return response, nil
  68. }
  69. func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  70. scwRef, err := decodeScwSecretRef(ref.Key)
  71. if err != nil {
  72. return nil, err
  73. }
  74. versionSpec := "latest_enabled"
  75. if ref.Version != "" {
  76. versionSpec = ref.Version
  77. }
  78. value, err := c.accessSecretVersion(ctx, scwRef, versionSpec)
  79. if err != nil {
  80. //nolint:errorlint
  81. if _, isNotFoundErr := err.(*scw.ResourceNotFoundError); isNotFoundErr {
  82. return nil, esv1beta1.NoSecretError{}
  83. }
  84. return nil, err
  85. }
  86. if ref.Property != "" {
  87. extracted, err := extractJSONProperty(value, ref.Property)
  88. if err != nil {
  89. return nil, err
  90. }
  91. value = extracted
  92. }
  93. return value, nil
  94. }
  95. func (c *client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
  96. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  97. if err != nil {
  98. return err
  99. }
  100. if scwRef.RefType != refTypeName {
  101. return fmt.Errorf("secrets can only be pushed by name")
  102. }
  103. secretName := scwRef.Value
  104. // First, we do a GetSecretVersion() to resolve the secret id and the last revision number.
  105. var secretID string
  106. secretExists := false
  107. existingSecretVersion := int64(-1)
  108. secretVersion, err := c.api.GetSecretVersionByName(&smapi.GetSecretVersionByNameRequest{
  109. SecretName: secretName,
  110. Revision: "latest",
  111. }, scw.WithContext(ctx))
  112. if err != nil {
  113. //nolint:errorlint
  114. if notFoundErr, ok := err.(*scw.ResourceNotFoundError); ok {
  115. if notFoundErr.Resource == "secret_version" {
  116. secretExists = true
  117. }
  118. } else {
  119. return err
  120. }
  121. } else {
  122. secretExists = true
  123. existingSecretVersion = int64(secretVersion.Revision)
  124. }
  125. if secretExists {
  126. if existingSecretVersion != -1 {
  127. // If the secret exists, we can fetch its last value to see if we have any change to make.
  128. secretID = secretVersion.SecretID
  129. data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  130. if err != nil {
  131. return err
  132. }
  133. if bytes.Equal(data, value) {
  134. // No change to push.
  135. return nil
  136. }
  137. } else {
  138. // If the secret exists but has no versions, we need an additional GetSecret() to resolve the secret id.
  139. // This may happen if a push was interrupted.
  140. secret, err := c.api.GetSecretByName(&smapi.GetSecretByNameRequest{
  141. SecretName: secretName,
  142. }, scw.WithContext(ctx))
  143. if err != nil {
  144. return err
  145. }
  146. secretID = secret.ID
  147. }
  148. } else {
  149. // If the secret does not exist, we need to create it.
  150. secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
  151. ProjectID: c.projectID,
  152. Name: secretName,
  153. }, scw.WithContext(ctx))
  154. if err != nil {
  155. return err
  156. }
  157. secretID = secret.ID
  158. }
  159. // Finally, we push the new secret version.
  160. createSecretVersionRequest := smapi.CreateSecretVersionRequest{
  161. SecretID: secretID,
  162. Data: value,
  163. }
  164. createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
  165. if err != nil {
  166. return err
  167. }
  168. c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
  169. if secretExists && existingSecretVersion != -1 {
  170. _, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
  171. SecretID: secretID,
  172. Revision: fmt.Sprintf("%d", existingSecretVersion),
  173. })
  174. if err != nil {
  175. return err
  176. }
  177. }
  178. return nil
  179. }
  180. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
  181. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  182. if err != nil {
  183. return err
  184. }
  185. if scwRef.RefType != refTypeName {
  186. return fmt.Errorf("secrets can only be pushed by name")
  187. }
  188. secretName := scwRef.Value
  189. secret, err := c.getSecretByName(ctx, secretName)
  190. if err != nil {
  191. if errors.Is(err, errNoSecretForName) {
  192. return nil
  193. }
  194. return err
  195. }
  196. request := smapi.DeleteSecretRequest{
  197. SecretID: secret.ID,
  198. }
  199. err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
  200. if err != nil {
  201. return err
  202. }
  203. return nil
  204. }
  205. func (c *client) Validate() (esv1beta1.ValidationResult, error) {
  206. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  207. defer cancel()
  208. page := int32(1)
  209. pageSize := uint32(0)
  210. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  211. ProjectID: &c.projectID,
  212. Page: &page,
  213. PageSize: &pageSize,
  214. }, scw.WithContext(ctx))
  215. if err != nil {
  216. return esv1beta1.ValidationResultError, nil
  217. }
  218. return esv1beta1.ValidationResultReady, nil
  219. }
  220. func (c *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  221. rawData, err := c.GetSecret(ctx, ref)
  222. if err != nil {
  223. return nil, err
  224. }
  225. structuredData := make(map[string]json.RawMessage)
  226. err = json.Unmarshal(rawData, &structuredData)
  227. if err != nil {
  228. return nil, err
  229. }
  230. values := make(map[string][]byte)
  231. for key, value := range structuredData {
  232. values[key] = jsonToSecretData(value)
  233. }
  234. return values, nil
  235. }
  236. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  237. func (c *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  238. request := smapi.ListSecretsRequest{
  239. ProjectID: &c.projectID,
  240. Page: new(int32),
  241. PageSize: new(uint32),
  242. }
  243. *request.Page = 1
  244. *request.PageSize = 50
  245. if ref.Path != nil {
  246. return nil, fmt.Errorf("searching by path is not supported")
  247. }
  248. var nameMatcher *find.Matcher
  249. if ref.Name != nil {
  250. var err error
  251. nameMatcher, err = find.New(*ref.Name)
  252. if err != nil {
  253. return nil, err
  254. }
  255. }
  256. for tag := range ref.Tags {
  257. request.Tags = append(request.Tags, tag)
  258. }
  259. results := map[string][]byte{}
  260. for done := false; !done; {
  261. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  262. if err != nil {
  263. return nil, err
  264. }
  265. totalFetched := uint64(*request.Page-1)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  266. done = totalFetched == uint64(response.TotalCount)
  267. *request.Page++
  268. for _, secret := range response.Secrets {
  269. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  270. continue
  271. }
  272. accessReq := smapi.AccessSecretVersionRequest{
  273. Region: secret.Region,
  274. SecretID: secret.ID,
  275. Revision: "latest_enabled",
  276. }
  277. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  278. if err != nil {
  279. log.Error(err, "failed to access secret")
  280. continue
  281. }
  282. results[secret.Name] = accessResp.Data
  283. }
  284. }
  285. return results, nil
  286. }
  287. func (c *client) Close(context.Context) error {
  288. return nil
  289. }
  290. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  291. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  292. if secretRef.RefType == refTypeID && len(versionSpec) > 0 && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  293. secretID := secretRef.Value
  294. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  295. if err == nil {
  296. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  297. }
  298. }
  299. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  300. var secretID string
  301. var secretRevision uint32
  302. switch secretRef.RefType {
  303. case refTypeID:
  304. request := smapi.GetSecretVersionRequest{
  305. SecretID: secretRef.Value,
  306. Revision: versionSpec,
  307. }
  308. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  309. if err != nil {
  310. return nil, err
  311. }
  312. secretID = response.SecretID
  313. secretRevision = response.Revision
  314. case refTypeName:
  315. request := smapi.GetSecretVersionByNameRequest{
  316. SecretName: secretRef.Value,
  317. Revision: versionSpec,
  318. }
  319. response, err := c.api.GetSecretVersionByName(&request, scw.WithContext(ctx))
  320. if err != nil {
  321. return nil, err
  322. }
  323. secretID = response.SecretID
  324. secretRevision = response.Revision
  325. default:
  326. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  327. }
  328. return c.accessSpecificSecretVersion(ctx, secretID, secretRevision)
  329. }
  330. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  331. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  332. if cacheHit {
  333. return cachedValue, nil
  334. }
  335. request := smapi.AccessSecretVersionRequest{
  336. SecretID: secretID,
  337. Revision: fmt.Sprintf("%d", revision),
  338. }
  339. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  340. if err != nil {
  341. return nil, err
  342. }
  343. return response.Data, nil
  344. }
  345. func jsonToSecretData(value json.RawMessage) []byte {
  346. var stringValue string
  347. err := json.Unmarshal(value, &stringValue)
  348. if err == nil {
  349. return []byte(stringValue)
  350. }
  351. return []byte(strings.TrimSpace(string(value)))
  352. }
  353. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  354. result := gjson.Get(string(secretData), property)
  355. if !result.Exists() {
  356. return nil, esv1beta1.NoSecretError{}
  357. }
  358. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  359. }