webhook_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 webhook
  13. import (
  14. "bytes"
  15. "context"
  16. "errors"
  17. "io"
  18. "net/http"
  19. "net/http/httptest"
  20. "strings"
  21. "testing"
  22. "time"
  23. "gopkg.in/yaml.v3"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  26. )
  27. type testCase struct {
  28. Case string `json:"case,omitempty"`
  29. Args args `json:"args"`
  30. Want want `json:"want"`
  31. }
  32. type args struct {
  33. URL string `json:"url,omitempty"`
  34. Body string `json:"body,omitempty"`
  35. Timeout string `json:"timeout,omitempty"`
  36. Key string `json:"key,omitempty"`
  37. Version string `json:"version,omitempty"`
  38. JSONPath string `json:"jsonpath,omitempty"`
  39. Response string `json:"response,omitempty"`
  40. StatusCode int `json:"statuscode,omitempty"`
  41. }
  42. type want struct {
  43. Path string `json:"path,omitempty"`
  44. Err string `json:"err,omitempty"`
  45. Result string `json:"result,omitempty"`
  46. ResultMap map[string]string `json:"resultmap,omitempty"`
  47. }
  48. var testCases = `
  49. case: error url
  50. args:
  51. url: /api/getsecret?id={{ .unclosed.template
  52. want:
  53. err: failed to parse url
  54. ---
  55. case: error body
  56. args:
  57. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  58. body: Body error {{ .unclosed.template
  59. want:
  60. err: failed to parse body
  61. ---
  62. case: error connection
  63. args:
  64. url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  65. want:
  66. err: failed to call endpoint
  67. ---
  68. case: error not found
  69. args:
  70. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  71. key: testkey
  72. version: 1
  73. statuscode: 404
  74. response: not found
  75. want:
  76. path: /api/getsecret?id=testkey&version=1
  77. err: endpoint gave error 404
  78. ---
  79. case: error bad json
  80. args:
  81. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  82. key: testkey
  83. version: 1
  84. jsonpath: $.result.thesecret
  85. response: '{"result":{"thesecret":"secret-value"}'
  86. want:
  87. path: /api/getsecret?id=testkey&version=1
  88. err: failed to parse response json
  89. ---
  90. case: error bad jsonpath
  91. args:
  92. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  93. key: testkey
  94. version: 1
  95. jsonpath: $.result.thesecret
  96. response: '{"result":{"nosecret":"secret-value"}}'
  97. want:
  98. path: /api/getsecret?id=testkey&version=1
  99. err: failed to get response path
  100. ---
  101. case: error bad json data
  102. args:
  103. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  104. key: testkey
  105. version: 1
  106. jsonpath: $.result.thesecret
  107. response: '{"result":{"thesecret":{"one":"secret-value"}}}'
  108. want:
  109. path: /api/getsecret?id=testkey&version=1
  110. err: failed to get response (wrong type
  111. ---
  112. case: error timeout
  113. args:
  114. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  115. key: testkey
  116. version: 1
  117. response: secret-value
  118. timeout: 0.01ms
  119. want:
  120. path: /api/getsecret?id=testkey&version=1
  121. err: context deadline exceeded
  122. ---
  123. case: good plaintext
  124. args:
  125. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  126. key: testkey
  127. version: 1
  128. response: secret-value
  129. want:
  130. path: /api/getsecret?id=testkey&version=1
  131. result: secret-value
  132. ---
  133. case: good json
  134. args:
  135. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  136. key: testkey
  137. version: 1
  138. jsonpath: $.result.thesecret
  139. response: '{"result":{"thesecret":"secret-value"}}'
  140. want:
  141. path: /api/getsecret?id=testkey&version=1
  142. result: secret-value
  143. ---
  144. case: good json map
  145. args:
  146. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  147. key: testkey
  148. version: 1
  149. jsonpath: $.result
  150. response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
  151. want:
  152. path: /api/getsecret?id=testkey&version=1
  153. resultmap:
  154. thesecret: secret-value
  155. alsosecret: another-value
  156. ---
  157. case: good json map string
  158. args:
  159. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  160. key: testkey
  161. version: 1
  162. response: '{"thesecret":"secret-value","alsosecret":"another-value"}'
  163. want:
  164. path: /api/getsecret?id=testkey&version=1
  165. resultmap:
  166. thesecret: secret-value
  167. alsosecret: another-value
  168. ---
  169. case: error json map string
  170. args:
  171. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  172. key: testkey
  173. version: 1
  174. response: 'some simple string'
  175. want:
  176. path: /api/getsecret?id=testkey&version=1
  177. err: failed to get response (wrong type
  178. resultmap:
  179. thesecret: secret-value
  180. alsosecret: another-value
  181. ---
  182. case: error json map
  183. args:
  184. url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
  185. key: testkey
  186. version: 1
  187. jsonpath: $.result.thesecret
  188. response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
  189. want:
  190. path: /api/getsecret?id=testkey&version=1
  191. err: failed to get response (wrong type
  192. resultmap:
  193. thesecret: secret-value
  194. alsosecret: another-value
  195. `
  196. func TestWebhookGetSecret(t *testing.T) {
  197. ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
  198. for {
  199. var tc testCase
  200. if err := ydec.Decode(&tc); err != nil {
  201. if !errors.Is(err, io.EOF) {
  202. t.Errorf("testcase decode error %v", err)
  203. }
  204. break
  205. }
  206. runTestCase(tc, t)
  207. }
  208. }
  209. func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
  210. // Start a new server for every test case because the server wants to check the expected api path
  211. return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  212. if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
  213. t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
  214. }
  215. if tc.Args.StatusCode != 0 {
  216. rw.WriteHeader(tc.Args.StatusCode)
  217. }
  218. rw.Write([]byte(tc.Args.Response))
  219. }))
  220. }
  221. func parseTimeout(timeout string) (*metav1.Duration, error) {
  222. if timeout == "" {
  223. return nil, nil
  224. }
  225. dur, err := time.ParseDuration(timeout)
  226. if err != nil {
  227. return nil, err
  228. }
  229. return &metav1.Duration{Duration: dur}, nil
  230. }
  231. func runTestCase(tc testCase, t *testing.T) {
  232. ts := testCaseServer(tc, t)
  233. defer ts.Close()
  234. testStore := makeClusterSecretStore(ts.URL, tc.Args)
  235. var err error
  236. timeout, err := parseTimeout(tc.Args.Timeout)
  237. if err != nil {
  238. t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
  239. return
  240. }
  241. testStore.Spec.Provider.Webhook.Timeout = timeout
  242. testProv := &Provider{}
  243. client, err := testProv.NewClient(context.Background(), testStore, nil, "testnamespace")
  244. if err != nil {
  245. t.Errorf("%s: error creating client: %s", tc.Case, err.Error())
  246. return
  247. }
  248. if tc.Want.ResultMap != nil {
  249. testGetSecretMap(tc, t, client)
  250. } else {
  251. testGetSecret(tc, t, client)
  252. }
  253. }
  254. func testGetSecretMap(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
  255. testRef := esv1beta1.ExternalSecretDataRemoteRef{
  256. Key: tc.Args.Key,
  257. Version: tc.Args.Version,
  258. }
  259. secretmap, err := client.GetSecretMap(context.Background(), testRef)
  260. errStr := ""
  261. if err != nil {
  262. errStr = err.Error()
  263. }
  264. if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
  265. t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
  266. }
  267. if err == nil {
  268. for wantkey, wantval := range tc.Want.ResultMap {
  269. gotval, ok := secretmap[wantkey]
  270. if !ok {
  271. t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
  272. } else if string(gotval) != wantval {
  273. t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
  274. }
  275. }
  276. }
  277. }
  278. func testGetSecret(tc testCase, t *testing.T, client esv1beta1.SecretsClient) {
  279. testRef := esv1beta1.ExternalSecretDataRemoteRef{
  280. Key: tc.Args.Key,
  281. Version: tc.Args.Version,
  282. }
  283. ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  284. defer cancel()
  285. secret, err := client.GetSecret(ctx, testRef)
  286. errStr := ""
  287. if err != nil {
  288. errStr = err.Error()
  289. }
  290. if !strings.Contains(errStr, tc.Want.Err) {
  291. t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
  292. }
  293. if err == nil && string(secret) != tc.Want.Result {
  294. t.Errorf("%s: unexpected response: '%s' (expected '%s')", tc.Case, secret, tc.Want.Result)
  295. }
  296. }
  297. func makeClusterSecretStore(url string, args args) *esv1beta1.ClusterSecretStore {
  298. store := &esv1beta1.ClusterSecretStore{
  299. TypeMeta: metav1.TypeMeta{
  300. Kind: "ClusterSecretStore",
  301. },
  302. ObjectMeta: metav1.ObjectMeta{
  303. Name: "wehbook-store",
  304. Namespace: "default",
  305. },
  306. Spec: esv1beta1.SecretStoreSpec{
  307. Provider: &esv1beta1.SecretStoreProvider{
  308. Webhook: &esv1beta1.WebhookProvider{
  309. URL: url + args.URL,
  310. Body: args.Body,
  311. Headers: map[string]string{
  312. "Content-Type": "application.json",
  313. "X-SecretKey": "{{ .remoteRef.key }}",
  314. },
  315. Result: esv1beta1.WebhookResult{
  316. JSONPath: args.JSONPath,
  317. },
  318. },
  319. },
  320. },
  321. }
  322. return store
  323. }