apiserver_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. /*
  2. Copyright 2016 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package apiserver
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "io/ioutil"
  19. "net"
  20. "net/http"
  21. "os"
  22. "path"
  23. "reflect"
  24. "testing"
  25. "time"
  26. "github.com/stretchr/testify/assert"
  27. apierrors "k8s.io/apimachinery/pkg/api/errors"
  28. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  29. "k8s.io/apimachinery/pkg/runtime/schema"
  30. "k8s.io/apimachinery/pkg/util/wait"
  31. "k8s.io/apiserver/pkg/server/dynamiccertificates"
  32. genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
  33. "k8s.io/client-go/discovery"
  34. client "k8s.io/client-go/kubernetes"
  35. "k8s.io/client-go/rest"
  36. "k8s.io/client-go/tools/clientcmd"
  37. clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
  38. "k8s.io/client-go/util/cert"
  39. apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
  40. aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
  41. kastesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
  42. "k8s.io/kubernetes/test/integration/framework"
  43. wardlev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1"
  44. wardlev1beta1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1beta1"
  45. sampleserver "k8s.io/sample-apiserver/pkg/cmd/server"
  46. )
  47. func TestAggregatedAPIServer(t *testing.T) {
  48. // makes the kube-apiserver very responsive. it's normally a minute
  49. dynamiccertificates.FileRefreshDuration = 1 * time.Second
  50. stopCh := make(chan struct{})
  51. defer close(stopCh)
  52. testServer := kastesting.StartTestServerOrDie(t, &kastesting.TestServerInstanceOptions{EnableCertAuth: true}, nil, framework.SharedEtcd())
  53. defer testServer.TearDownFn()
  54. kubeClientConfig := rest.CopyConfig(testServer.ClientConfig)
  55. // force json because everything speaks it
  56. kubeClientConfig.ContentType = ""
  57. kubeClientConfig.AcceptContentTypes = ""
  58. kubeClient := client.NewForConfigOrDie(kubeClientConfig)
  59. aggregatorClient := aggregatorclient.NewForConfigOrDie(kubeClientConfig)
  60. // start the wardle server to prove we can aggregate it
  61. wardleToKASKubeConfigFile := writeKubeConfigForWardleServerToKASConnection(t, rest.CopyConfig(kubeClientConfig))
  62. defer os.Remove(wardleToKASKubeConfigFile)
  63. wardleCertDir, _ := ioutil.TempDir("", "test-integration-wardle-server")
  64. defer os.RemoveAll(wardleCertDir)
  65. listener, wardlePort, err := genericapiserveroptions.CreateListener("tcp", "127.0.0.1:0")
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. go func() {
  70. o := sampleserver.NewWardleServerOptions(os.Stdout, os.Stderr)
  71. o.RecommendedOptions.SecureServing.Listener = listener
  72. o.RecommendedOptions.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
  73. wardleCmd := sampleserver.NewCommandStartWardleServer(o, stopCh)
  74. wardleCmd.SetArgs([]string{
  75. "--authentication-kubeconfig", wardleToKASKubeConfigFile,
  76. "--authorization-kubeconfig", wardleToKASKubeConfigFile,
  77. "--etcd-servers", framework.GetEtcdURL(),
  78. "--cert-dir", wardleCertDir,
  79. "--kubeconfig", wardleToKASKubeConfigFile,
  80. })
  81. if err := wardleCmd.Execute(); err != nil {
  82. t.Fatal(err)
  83. }
  84. }()
  85. directWardleClientConfig, err := waitForWardleRunning(t, kubeClientConfig, wardleCertDir, wardlePort)
  86. if err != nil {
  87. t.Fatal(err)
  88. }
  89. // now we're finally ready to test. These are what's run by default now
  90. wardleClient, err := client.NewForConfig(directWardleClientConfig)
  91. if err != nil {
  92. t.Fatal(err)
  93. }
  94. testAPIGroupList(t, wardleClient.Discovery().RESTClient())
  95. testAPIGroup(t, wardleClient.Discovery().RESTClient())
  96. testAPIResourceList(t, wardleClient.Discovery().RESTClient())
  97. wardleCA, err := ioutil.ReadFile(directWardleClientConfig.CAFile)
  98. if err != nil {
  99. t.Fatal(err)
  100. }
  101. _, err = aggregatorClient.ApiregistrationV1beta1().APIServices().Create(context.TODO(), &apiregistrationv1beta1.APIService{
  102. ObjectMeta: metav1.ObjectMeta{Name: "v1alpha1.wardle.example.com"},
  103. Spec: apiregistrationv1beta1.APIServiceSpec{
  104. Service: &apiregistrationv1beta1.ServiceReference{
  105. Namespace: "kube-wardle",
  106. Name: "api",
  107. },
  108. Group: "wardle.example.com",
  109. Version: "v1alpha1",
  110. CABundle: wardleCA,
  111. GroupPriorityMinimum: 200,
  112. VersionPriority: 200,
  113. },
  114. }, metav1.CreateOptions{})
  115. if err != nil {
  116. t.Fatal(err)
  117. }
  118. // wait for the unavailable API service to be processed with updated status
  119. err = wait.Poll(100*time.Millisecond, 5*time.Second, func() (done bool, err error) {
  120. _, err = kubeClient.Discovery().ServerResources()
  121. hasExpectedError := checkWardleUnavailableDiscoveryError(t, err)
  122. return hasExpectedError, nil
  123. })
  124. if err != nil {
  125. t.Fatal(err)
  126. }
  127. // TODO figure out how to turn on enough of services and dns to run more
  128. // Now we want to verify that the client CA bundles properly reflect the values for the cluster-authentication
  129. firstKubeCANames, err := cert.GetClientCANamesForURL(kubeClientConfig.Host)
  130. if err != nil {
  131. t.Fatal(err)
  132. }
  133. t.Log(firstKubeCANames)
  134. firstWardleCANames, err := cert.GetClientCANamesForURL(directWardleClientConfig.Host)
  135. if err != nil {
  136. t.Fatal(err)
  137. }
  138. t.Log(firstWardleCANames)
  139. if !reflect.DeepEqual(firstKubeCANames, firstWardleCANames) {
  140. t.Fatal("names don't match")
  141. }
  142. // now we update the client-ca nd request-header-client-ca-file and the kas will consume it, update the configmap
  143. // and then the wardle server will detect and update too.
  144. if err := ioutil.WriteFile(path.Join(testServer.TmpDir, "client-ca.crt"), differentClientCA, 0644); err != nil {
  145. t.Fatal(err)
  146. }
  147. if err := ioutil.WriteFile(path.Join(testServer.TmpDir, "proxy-ca.crt"), differentFrontProxyCA, 0644); err != nil {
  148. t.Fatal(err)
  149. }
  150. // wait for it to be picked up. there's a test in certreload_test.go that ensure this works
  151. time.Sleep(4 * time.Second)
  152. // Now we want to verify that the client CA bundles properly updated to reflect the new values written for the kube-apiserver
  153. secondKubeCANames, err := cert.GetClientCANamesForURL(kubeClientConfig.Host)
  154. if err != nil {
  155. t.Fatal(err)
  156. }
  157. t.Log(secondKubeCANames)
  158. for i := range firstKubeCANames {
  159. if firstKubeCANames[i] == secondKubeCANames[i] {
  160. t.Errorf("ca bundles should change")
  161. }
  162. }
  163. secondWardleCANames, err := cert.GetClientCANamesForURL(directWardleClientConfig.Host)
  164. if err != nil {
  165. t.Fatal(err)
  166. }
  167. t.Log(secondWardleCANames)
  168. // second wardle should contain all the certs, first and last
  169. numMatches := 0
  170. for _, needle := range firstKubeCANames {
  171. for _, haystack := range secondWardleCANames {
  172. if needle == haystack {
  173. numMatches++
  174. break
  175. }
  176. }
  177. }
  178. for _, needle := range secondKubeCANames {
  179. for _, haystack := range secondWardleCANames {
  180. if needle == haystack {
  181. numMatches++
  182. break
  183. }
  184. }
  185. }
  186. if numMatches != 4 {
  187. t.Fatal("names don't match")
  188. }
  189. }
  190. func waitForWardleRunning(t *testing.T, wardleToKASKubeConfig *rest.Config, wardleCertDir string, wardlePort int) (*rest.Config, error) {
  191. directWardleClientConfig := rest.AnonymousClientConfig(rest.CopyConfig(wardleToKASKubeConfig))
  192. directWardleClientConfig.CAFile = path.Join(wardleCertDir, "apiserver.crt")
  193. directWardleClientConfig.CAData = nil
  194. directWardleClientConfig.ServerName = ""
  195. directWardleClientConfig.BearerToken = wardleToKASKubeConfig.BearerToken
  196. var wardleClient client.Interface
  197. lastHealthContent := []byte{}
  198. var lastHealthErr error
  199. err := wait.PollImmediate(100*time.Millisecond, 10*time.Second, func() (done bool, err error) {
  200. if _, err := os.Stat(directWardleClientConfig.CAFile); os.IsNotExist(err) { // wait until the file trust is created
  201. lastHealthErr = err
  202. return false, nil
  203. }
  204. directWardleClientConfig.Host = fmt.Sprintf("https://127.0.0.1:%d", wardlePort)
  205. wardleClient, err = client.NewForConfig(directWardleClientConfig)
  206. if err != nil {
  207. // this happens because we race the API server start
  208. t.Log(err)
  209. return false, nil
  210. }
  211. healthStatus := 0
  212. result := wardleClient.Discovery().RESTClient().Get().AbsPath("/healthz").Do(context.TODO()).StatusCode(&healthStatus)
  213. lastHealthContent, lastHealthErr = result.Raw()
  214. if healthStatus != http.StatusOK {
  215. return false, nil
  216. }
  217. return true, nil
  218. })
  219. if err != nil {
  220. t.Log(string(lastHealthContent))
  221. t.Log(lastHealthErr)
  222. return nil, err
  223. }
  224. return directWardleClientConfig, nil
  225. }
  226. func writeKubeConfigForWardleServerToKASConnection(t *testing.T, kubeClientConfig *rest.Config) string {
  227. // write a kubeconfig out for starting other API servers with delegated auth. remember, no in-cluster config
  228. // the loopback client config uses a loopback cert with different SNI. We need to use the "real"
  229. // cert, so we'll hope we aren't hacked during a unit test and instead load it from the server we started.
  230. wardleToKASKubeClientConfig := rest.CopyConfig(kubeClientConfig)
  231. servingCerts, _, err := cert.GetServingCertificatesForURL(wardleToKASKubeClientConfig.Host, "")
  232. if err != nil {
  233. t.Fatal(err)
  234. }
  235. encodedServing, err := cert.EncodeCertificates(servingCerts...)
  236. if err != nil {
  237. t.Fatal(err)
  238. }
  239. wardleToKASKubeClientConfig.CAData = encodedServing
  240. for _, v := range servingCerts {
  241. t.Logf("Client: Server public key is %v\n", dynamiccertificates.GetHumanCertDetail(v))
  242. }
  243. certs, err := cert.ParseCertsPEM(wardleToKASKubeClientConfig.CAData)
  244. if err != nil {
  245. t.Fatal(err)
  246. }
  247. for _, curr := range certs {
  248. t.Logf("CA bundle %v\n", dynamiccertificates.GetHumanCertDetail(curr))
  249. }
  250. adminKubeConfig := createKubeConfig(wardleToKASKubeClientConfig)
  251. wardleToKASKubeConfigFile, _ := ioutil.TempFile("", "")
  252. if err := clientcmd.WriteToFile(*adminKubeConfig, wardleToKASKubeConfigFile.Name()); err != nil {
  253. t.Fatal(err)
  254. }
  255. return wardleToKASKubeConfigFile.Name()
  256. }
  257. func checkWardleUnavailableDiscoveryError(t *testing.T, err error) bool {
  258. if err == nil {
  259. t.Log("Discovery call expected to return failed unavailable service")
  260. return false
  261. }
  262. if !discovery.IsGroupDiscoveryFailedError(err) {
  263. t.Logf("Unexpected error: %T, %v", err, err)
  264. return false
  265. }
  266. discoveryErr := err.(*discovery.ErrGroupDiscoveryFailed)
  267. if len(discoveryErr.Groups) != 1 {
  268. t.Logf("Unexpected failed groups: %v", err)
  269. return false
  270. }
  271. groupVersion := schema.GroupVersion{Group: "wardle.example.com", Version: "v1alpha1"}
  272. groupVersionErr, ok := discoveryErr.Groups[groupVersion]
  273. if !ok {
  274. t.Logf("Unexpected failed group version: %v", err)
  275. return false
  276. }
  277. if !apierrors.IsServiceUnavailable(groupVersionErr) {
  278. t.Logf("Unexpected failed group version error: %v", err)
  279. return false
  280. }
  281. return true
  282. }
  283. func createKubeConfig(clientCfg *rest.Config) *clientcmdapi.Config {
  284. clusterNick := "cluster"
  285. userNick := "user"
  286. contextNick := "context"
  287. config := clientcmdapi.NewConfig()
  288. credentials := clientcmdapi.NewAuthInfo()
  289. credentials.Token = clientCfg.BearerToken
  290. credentials.ClientCertificate = clientCfg.TLSClientConfig.CertFile
  291. if len(credentials.ClientCertificate) == 0 {
  292. credentials.ClientCertificateData = clientCfg.TLSClientConfig.CertData
  293. }
  294. credentials.ClientKey = clientCfg.TLSClientConfig.KeyFile
  295. if len(credentials.ClientKey) == 0 {
  296. credentials.ClientKeyData = clientCfg.TLSClientConfig.KeyData
  297. }
  298. config.AuthInfos[userNick] = credentials
  299. cluster := clientcmdapi.NewCluster()
  300. cluster.Server = clientCfg.Host
  301. cluster.CertificateAuthority = clientCfg.CAFile
  302. if len(cluster.CertificateAuthority) == 0 {
  303. cluster.CertificateAuthorityData = clientCfg.CAData
  304. }
  305. cluster.InsecureSkipTLSVerify = clientCfg.Insecure
  306. config.Clusters[clusterNick] = cluster
  307. context := clientcmdapi.NewContext()
  308. context.Cluster = clusterNick
  309. context.AuthInfo = userNick
  310. config.Contexts[contextNick] = context
  311. config.CurrentContext = contextNick
  312. return config
  313. }
  314. func readResponse(client rest.Interface, location string) ([]byte, error) {
  315. return client.Get().AbsPath(location).DoRaw(context.TODO())
  316. }
  317. func testAPIGroupList(t *testing.T, client rest.Interface) {
  318. contents, err := readResponse(client, "/apis")
  319. if err != nil {
  320. t.Fatalf("%v", err)
  321. }
  322. t.Log(string(contents))
  323. var apiGroupList metav1.APIGroupList
  324. err = json.Unmarshal(contents, &apiGroupList)
  325. if err != nil {
  326. t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis", err)
  327. }
  328. assert.Equal(t, 1, len(apiGroupList.Groups))
  329. assert.Equal(t, wardlev1alpha1.GroupName, apiGroupList.Groups[0].Name)
  330. assert.Equal(t, 2, len(apiGroupList.Groups[0].Versions))
  331. v1alpha1 := metav1.GroupVersionForDiscovery{
  332. GroupVersion: wardlev1alpha1.SchemeGroupVersion.String(),
  333. Version: wardlev1alpha1.SchemeGroupVersion.Version,
  334. }
  335. v1beta1 := metav1.GroupVersionForDiscovery{
  336. GroupVersion: wardlev1beta1.SchemeGroupVersion.String(),
  337. Version: wardlev1beta1.SchemeGroupVersion.Version,
  338. }
  339. assert.Equal(t, v1beta1, apiGroupList.Groups[0].Versions[0])
  340. assert.Equal(t, v1alpha1, apiGroupList.Groups[0].Versions[1])
  341. assert.Equal(t, v1beta1, apiGroupList.Groups[0].PreferredVersion)
  342. }
  343. func testAPIGroup(t *testing.T, client rest.Interface) {
  344. contents, err := readResponse(client, "/apis/wardle.example.com")
  345. if err != nil {
  346. t.Fatalf("%v", err)
  347. }
  348. t.Log(string(contents))
  349. var apiGroup metav1.APIGroup
  350. err = json.Unmarshal(contents, &apiGroup)
  351. if err != nil {
  352. t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis/wardle.example.com", err)
  353. }
  354. assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.Group, apiGroup.Name)
  355. assert.Equal(t, 2, len(apiGroup.Versions))
  356. assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.String(), apiGroup.Versions[1].GroupVersion)
  357. assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.Version, apiGroup.Versions[1].Version)
  358. assert.Equal(t, apiGroup.PreferredVersion, apiGroup.Versions[0])
  359. }
  360. func testAPIResourceList(t *testing.T, client rest.Interface) {
  361. contents, err := readResponse(client, "/apis/wardle.example.com/v1alpha1")
  362. if err != nil {
  363. t.Fatalf("%v", err)
  364. }
  365. t.Log(string(contents))
  366. var apiResourceList metav1.APIResourceList
  367. err = json.Unmarshal(contents, &apiResourceList)
  368. if err != nil {
  369. t.Fatalf("Error in unmarshalling response from server %s: %v", "/apis/wardle.example.com/v1alpha1", err)
  370. }
  371. assert.Equal(t, wardlev1alpha1.SchemeGroupVersion.String(), apiResourceList.GroupVersion)
  372. assert.Equal(t, 2, len(apiResourceList.APIResources))
  373. assert.Equal(t, "fischers", apiResourceList.APIResources[0].Name)
  374. assert.False(t, apiResourceList.APIResources[0].Namespaced)
  375. assert.Equal(t, "flunders", apiResourceList.APIResources[1].Name)
  376. assert.True(t, apiResourceList.APIResources[1].Namespaced)
  377. }
  378. var (
  379. // I have no idea what these certs are, they just need to be different
  380. differentClientCA = []byte(`-----BEGIN CERTIFICATE-----
  381. MIIDQDCCAiigAwIBAgIJANWw74P5KJk2MA0GCSqGSIb3DQEBCwUAMDQxMjAwBgNV
  382. BAMMKWdlbmVyaWNfd2ViaG9va19hZG1pc3Npb25fcGx1Z2luX3Rlc3RzX2NhMCAX
  383. DTE3MTExNjAwMDUzOVoYDzIyOTEwOTAxMDAwNTM5WjAjMSEwHwYDVQQDExh3ZWJo
  384. b29rLXRlc3QuZGVmYXVsdC5zdmMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
  385. AoIBAQDXd/nQ89a5H8ifEsigmMd01Ib6NVR3bkJjtkvYnTbdfYEBj7UzqOQtHoLa
  386. dIVmefny5uIHvj93WD8WDVPB3jX2JHrXkDTXd/6o6jIXHcsUfFTVLp6/bZ+Anqe0
  387. r/7hAPkzA2A7APyTWM3ZbEeo1afXogXhOJ1u/wz0DflgcB21gNho4kKTONXO3NHD
  388. XLpspFqSkxfEfKVDJaYAoMnYZJtFNsa2OvsmLnhYF8bjeT3i07lfwrhUZvP+7Gsp
  389. 7UgUwc06WuNHjfx1s5e6ySzH0QioMD1rjYneqOvk0pKrMIhuAEWXqq7jlXcDtx1E
  390. j+wnYbVqqVYheHZ8BCJoVAAQGs9/AgMBAAGjZDBiMAkGA1UdEwQCMAAwCwYDVR0P
  391. BAQDAgXgMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATApBgNVHREEIjAg
  392. hwR/AAABghh3ZWJob29rLXRlc3QuZGVmYXVsdC5zdmMwDQYJKoZIhvcNAQELBQAD
  393. ggEBAD/GKSPNyQuAOw/jsYZesb+RMedbkzs18sSwlxAJQMUrrXwlVdHrA8q5WhE6
  394. ABLqU1b8lQ8AWun07R8k5tqTmNvCARrAPRUqls/ryER+3Y9YEcxEaTc3jKNZFLbc
  395. T6YtcnkdhxsiO136wtiuatpYL91RgCmuSpR8+7jEHhuFU01iaASu7ypFrUzrKHTF
  396. bKwiLRQi1cMzVcLErq5CDEKiKhUkoDucyARFszrGt9vNIl/YCcBOkcNvM3c05Hn3
  397. M++C29JwS3Hwbubg6WO3wjFjoEhpCwU6qRYUz3MRp4tHO4kxKXx+oQnUiFnR7vW0
  398. YkNtGc1RUDHwecCTFpJtPb7Yu/E=
  399. -----END CERTIFICATE-----
  400. `)
  401. differentFrontProxyCA = []byte(`-----BEGIN CERTIFICATE-----
  402. MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
  403. GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
  404. MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
  405. BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
  406. KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
  407. BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
  408. K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
  409. a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
  410. MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
  411. -----END CERTIFICATE-----
  412. `)
  413. )