-
Notifications
You must be signed in to change notification settings - Fork 450
/
seedadmissioncontroller.go
586 lines (549 loc) · 21.1 KB
/
seedadmissioncontroller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
// Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package seedadmissioncontroller
import (
"context"
"fmt"
"time"
druidv1alpha1 "github.com/gardener/etcd-druid/api/v1alpha1"
v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants"
extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1"
"github.com/gardener/gardener/pkg/client/kubernetes"
"github.com/gardener/gardener/pkg/features"
gardenletfeatures "github.com/gardener/gardener/pkg/gardenlet/features"
"github.com/gardener/gardener/pkg/operation/botanist/component"
"github.com/gardener/gardener/pkg/resourcemanager/controller/garbagecollector/references"
"github.com/gardener/gardener/pkg/seedadmissioncontroller/webhooks/admission/extensioncrds"
"github.com/gardener/gardener/pkg/seedadmissioncontroller/webhooks/admission/extensionresources"
kutil "github.com/gardener/gardener/pkg/utils/kubernetes"
"github.com/gardener/gardener/pkg/utils/managedresources"
admissionv1 "k8s.io/api/admission/v1"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
autoscalingv1beta2 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1beta2"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
// Name is used as metadata.name of the ServiceAccount, ManagedResource,
// ClusterRole, ClusterRoleBinding, Service, Deployment and ValidatingWebhookConfiguration
// of the seed admission controller.
Name = "gardener-seed-admission-controller"
managedResourceName = Name
deploymentName = Name
containerName = Name
port = 10250
volumeName = Name + "-tls"
volumeMountPath = "/srv/gardener-seed-admission-controller"
defaultReplicas = int32(3)
)
// New creates a new instance of DeployWaiter for the gardener-seed-admission-controller.
func New(c client.Client, namespace string, image string) component.DeployWaiter {
return &gardenerSeedAdmissionController{
client: c,
namespace: namespace,
image: image,
}
}
type gardenerSeedAdmissionController struct {
client client.Client
namespace string
image string
}
func (g *gardenerSeedAdmissionController) Deploy(ctx context.Context) error {
replicas, err := g.getReplicas(ctx)
if err != nil {
return err
}
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: Name + "-tls",
Namespace: g.namespace,
Labels: getLabels(),
},
Type: corev1.SecretTypeTLS,
Data: map[string][]byte{
corev1.TLSCertKey: []byte(tlsServerCert),
corev1.TLSPrivateKeyKey: []byte(tlsServerKey),
},
}
utilruntime.Must(kutil.MakeUnique(secret))
var (
registry = managedresources.NewRegistry(kubernetes.SeedScheme, kubernetes.SeedCodec, kubernetes.SeedSerializer)
serviceAccount = &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Namespace: g.namespace,
Labels: getLabels(),
},
AutomountServiceAccountToken: pointer.Bool(false),
}
clusterRole = &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Labels: getLabels(),
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{apiextensionsv1.SchemeGroupVersion.Group},
Resources: []string{"customresourcedefinitions"},
Verbs: []string{"get", "list"},
},
{
APIGroups: []string{druidv1alpha1.GroupVersion.Group},
Resources: []string{"etcds"},
Verbs: []string{"get", "list"},
},
{
APIGroups: []string{extensionsv1alpha1.SchemeGroupVersion.Group},
Resources: []string{
"backupbuckets",
"backupentries",
"bastions",
"containerruntimes",
"controlplanes",
"dnsrecords",
"extensions",
"infrastructures",
"networks",
"operatingsystemconfigs",
"workers",
"clusters",
},
Verbs: []string{"get", "list"},
},
},
}
clusterRoleBinding = &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Labels: getLabels(),
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: clusterRole.Name,
},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: serviceAccount.Name,
Namespace: serviceAccount.Namespace,
}},
}
service = &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Namespace: g.namespace,
Labels: getLabels(),
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeClusterIP,
Selector: getLabels(),
Ports: []corev1.ServicePort{{
Name: "web",
Port: 443,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromInt(port),
}},
},
}
// if maxUnavailable would not be set, new pods don't come up in small seed clusters
// (due to the pod anti affinity new pods are stuck in pending state)
maxUnavailable = intstr.FromInt(1)
deployment = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName,
Namespace: g.namespace,
Labels: getLabels(),
},
Spec: appsv1.DeploymentSpec{
RevisionHistoryLimit: pointer.Int32(1),
Replicas: &replicas,
Strategy: appsv1.DeploymentStrategy{
Type: appsv1.RollingUpdateDeploymentStrategyType,
RollingUpdate: &appsv1.RollingUpdateDeployment{
MaxUnavailable: &maxUnavailable,
},
},
Selector: &metav1.LabelSelector{MatchLabels: getLabels()},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
// TODO(rfranzke): Remove in a future release.
"security.gardener.cloud/trigger": "rollout",
},
Labels: getLabels(),
},
Spec: corev1.PodSpec{
Affinity: &corev1.Affinity{
PodAntiAffinity: &corev1.PodAntiAffinity{
PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
{
Weight: 100,
PodAffinityTerm: corev1.PodAffinityTerm{
TopologyKey: corev1.LabelHostname,
LabelSelector: &metav1.LabelSelector{MatchLabels: getLabels()},
},
},
},
},
},
ServiceAccountName: serviceAccount.Name,
Containers: []corev1.Container{{
Name: containerName,
Image: g.image,
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{
"/gardener-seed-admission-controller",
fmt.Sprintf("--port=%d", port),
fmt.Sprintf("--tls-cert-dir=%s", volumeMountPath),
fmt.Sprintf("--allow-invalid-extension-resources=%t", !gardenletfeatures.FeatureGate.Enabled(features.DenyInvalidExtensionResources)),
},
Ports: []corev1.ContainerPort{{
ContainerPort: int32(port),
}},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("20m"),
corev1.ResourceMemory: resource.MustParse("50Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("100m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
},
VolumeMounts: []corev1.VolumeMount{{
Name: volumeName,
MountPath: volumeMountPath,
ReadOnly: true,
}},
}},
Volumes: []corev1.Volume{{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secret.Name,
},
},
}},
},
},
},
}
podDisruptionBudget = &policyv1beta1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Namespace: g.namespace,
Labels: getLabels(),
},
Spec: policyv1beta1.PodDisruptionBudgetSpec{
MaxUnavailable: &maxUnavailable,
Selector: &metav1.LabelSelector{
MatchLabels: getLabels(),
},
},
}
updateMode = autoscalingv1beta2.UpdateModeAuto
vpa = &autoscalingv1beta2.VerticalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: Name + "-vpa",
Namespace: g.namespace,
Labels: getLabels(),
},
Spec: autoscalingv1beta2.VerticalPodAutoscalerSpec{
TargetRef: &autoscalingv1.CrossVersionObjectReference{
APIVersion: appsv1.SchemeGroupVersion.String(),
Kind: "Deployment",
Name: deployment.Name,
},
UpdatePolicy: &autoscalingv1beta2.PodUpdatePolicy{
UpdateMode: &updateMode,
},
},
}
caBundle = []byte(TLSCACert)
validatingWebhookConfiguration = GetValidatingWebhookConfig(caBundle, service)
)
utilruntime.Must(references.InjectAnnotations(deployment))
resources, err := registry.AddAllAndSerialize(
serviceAccount,
clusterRole,
clusterRoleBinding,
service,
secret,
deployment,
podDisruptionBudget,
vpa,
validatingWebhookConfiguration,
)
if err != nil {
return err
}
return managedresources.CreateForSeed(ctx, g.client, g.namespace, managedResourceName, false, resources)
}
func (g *gardenerSeedAdmissionController) Destroy(ctx context.Context) error {
return managedresources.DeleteForSeed(ctx, g.client, g.namespace, managedResourceName)
}
// GetValidatingWebhookConfig returns the ValidatingWebhookConfiguration for the seedadmissioncontroller component for
// reuse between the component and integration tests.
func GetValidatingWebhookConfig(caBundle []byte, webhookClientService *corev1.Service) *admissionregistrationv1.ValidatingWebhookConfiguration {
var (
failurePolicy = admissionregistrationv1.Fail
matchPolicy = admissionregistrationv1.Exact
sideEffect = admissionregistrationv1.SideEffectClassNone
)
return &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: Name,
Labels: getLabels(),
},
Webhooks: []admissionregistrationv1.ValidatingWebhook{{
Name: "crds.seed.admission.core.gardener.cloud",
Rules: []admissionregistrationv1.RuleWithOperations{{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{apiextensionsv1.GroupName},
APIVersions: []string{apiextensionsv1beta1.SchemeGroupVersion.Version, apiextensionsv1.SchemeGroupVersion.Version},
Resources: []string{"customresourcedefinitions"},
},
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Delete},
}},
FailurePolicy: &failurePolicy,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: extensioncrds.ObjectSelector,
},
ClientConfig: admissionregistrationv1.WebhookClientConfig{
CABundle: caBundle,
Service: &admissionregistrationv1.ServiceReference{
Name: webhookClientService.Name,
Namespace: webhookClientService.Namespace,
Path: pointer.String(extensioncrds.WebhookPath),
},
},
AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version},
MatchPolicy: &matchPolicy,
SideEffects: &sideEffect,
TimeoutSeconds: pointer.Int32(10),
}, {
Name: "crs.seed.admission.core.gardener.cloud",
Rules: []admissionregistrationv1.RuleWithOperations{
{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{druidv1alpha1.GroupVersion.Group},
APIVersions: []string{druidv1alpha1.GroupVersion.Version},
Resources: []string{"etcds"},
},
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Delete},
},
{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{extensionsv1alpha1.SchemeGroupVersion.Group},
APIVersions: []string{extensionsv1alpha1.SchemeGroupVersion.Version},
Resources: []string{
"backupbuckets",
"backupentries",
"bastions",
"containerruntimes",
"controlplanes",
"dnsrecords",
"extensions",
"infrastructures",
"networks",
"operatingsystemconfigs",
"workers",
},
},
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Delete},
},
},
FailurePolicy: &failurePolicy,
NamespaceSelector: &metav1.LabelSelector{},
ClientConfig: admissionregistrationv1.WebhookClientConfig{
CABundle: caBundle,
Service: &admissionregistrationv1.ServiceReference{
Name: webhookClientService.Name,
Namespace: webhookClientService.Namespace,
Path: pointer.String(extensioncrds.WebhookPath),
},
},
AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version},
MatchPolicy: &matchPolicy,
SideEffects: &sideEffect,
TimeoutSeconds: pointer.Int32(10),
}, {
Name: "validation.extensions.seed.admission.core.gardener.cloud",
Rules: []admissionregistrationv1.RuleWithOperations{
{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{druidv1alpha1.GroupVersion.Group},
APIVersions: []string{druidv1alpha1.GroupVersion.Version},
Resources: []string{"etcds"},
},
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
},
{
Rule: admissionregistrationv1.Rule{
APIGroups: []string{extensionsv1alpha1.SchemeGroupVersion.Group},
APIVersions: []string{extensionsv1alpha1.SchemeGroupVersion.Version},
Resources: []string{
"backupbuckets",
"backupentries",
"bastions",
"containerruntimes",
"controlplanes",
"dnsrecords",
"extensions",
"infrastructures",
"networks",
"operatingsystemconfigs",
"workers",
},
},
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
},
},
FailurePolicy: &failurePolicy,
NamespaceSelector: &metav1.LabelSelector{},
ClientConfig: admissionregistrationv1.WebhookClientConfig{
CABundle: caBundle,
Service: &admissionregistrationv1.ServiceReference{
Name: webhookClientService.Name,
Namespace: webhookClientService.Namespace,
Path: pointer.String(extensionresources.WebhookPath),
},
},
AdmissionReviewVersions: []string{admissionv1beta1.SchemeGroupVersion.Version, admissionv1.SchemeGroupVersion.Version},
MatchPolicy: &matchPolicy,
SideEffects: &sideEffect,
TimeoutSeconds: pointer.Int32(10),
}},
}
}
func getLabels() map[string]string {
return map[string]string{
v1beta1constants.LabelApp: "gardener",
v1beta1constants.LabelRole: "seed-admission-controller",
}
}
// TimeoutWaitForManagedResource is the timeout used while waiting for the ManagedResources to become healthy
// or deleted.
var TimeoutWaitForManagedResource = 2 * time.Minute
func (g *gardenerSeedAdmissionController) Wait(ctx context.Context) error {
timeoutCtx, cancel := context.WithTimeout(ctx, TimeoutWaitForManagedResource)
defer cancel()
return managedresources.WaitUntilHealthy(timeoutCtx, g.client, g.namespace, managedResourceName)
}
func (g *gardenerSeedAdmissionController) WaitCleanup(ctx context.Context) error {
timeoutCtx, cancel := context.WithTimeout(ctx, TimeoutWaitForManagedResource)
defer cancel()
return managedresources.WaitUntilDeleted(timeoutCtx, g.client, g.namespace, managedResourceName)
}
func (g *gardenerSeedAdmissionController) getReplicas(ctx context.Context) (int32, error) {
nodeList := &metav1.PartialObjectMetadataList{}
nodeList.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("NodeList"))
err := g.client.List(ctx, nodeList, client.Limit(defaultReplicas))
if err != nil {
return 0, err
}
nodeCount := int32(len(nodeList.Items))
if nodeCount < defaultReplicas {
return nodeCount, nil
}
return defaultReplicas, nil
}
const (
// TLSCACert is the of certificate authority of the
// seed admission controller server.
// TODO(mvladev) this cert is hard-coded.
// fix it in another PR.
TLSCACert = `-----BEGIN CERTIFICATE-----
MIIC+jCCAeKgAwIBAgIUTp3XvhrWOVM8ZGe86YoXMV/UJ7AwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0xOTAyMjcxNTM0MDBaFw0yNDAy
MjYxNTM0MDBaMBUxEzARBgNVBAMTCmt1YmVybmV0ZXMwggEiMA0GCSqGSIb3DQEB
AQUAA4IBDwAwggEKAoIBAQCyi0QGOcv2bTf3N8OLN97RwsgH6QAr8wSpAOrttBJg
FnfnU2T1RHgxm7qd190WL8DChv0dZf76d6eSQ4ZrjjyArTzufb4DtPwg+VWq7XvF
BNyn+2hf4SySkwd6k7XLhUTRx048IbByC4v+FEvmoLAwrc0d0G14ec6snD+7jO7e
kykQ/NgAOL7P6kDs9z6+bOfgF0nGN+bmeWQqJejR0t+OyQDCx5/FMtUfEVR5QX80
aeefgp3JFZb6fAw9KhLtdRV3FP0tz6hS+e4Sg0mwAAOqijZsV87kP5GYzjtcfA12
lDYl/nb1GtVvvkQD49VnV7mDnl6mG3LCMNCNH6WlZNv3AgMBAAGjQjBAMA4GA1Ud
DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSFA3LvJM21d8qs
ZVVCe6RrTT9wiTANBgkqhkiG9w0BAQsFAAOCAQEAns/EJ3yKsjtISoteQ714r2Um
BMPyUYTTdRHD8LZMd3RykvsacF2l2y88Nz6wJcAuoUj1h8aBDP5oXVtNmFT9jybS
TXrRvWi+aeYdb556nEA5/a94e+cb+Ck3qy/1xgQoS457AZQODisDZNBYWkAFs2Lc
ucpcAtXJtIthVm7FjoAHYcsrY04yAiYEJLD02TjUDXg4iGOGMkVHdmhawBDBF3Aj
esfcqFwji6JyAKFRACPowykQONFwUSom89uYESSCJFvNCk9MJmjJ2PzDUt6CypR4
epFdd1fXLwuwn7fvPMmJqD3HtLalX1AZmPk+BI8ezfAiVcVqnTJQMXlYPpYe9A==
-----END CERTIFICATE-----`
tlsServerCert = `-----BEGIN CERTIFICATE-----
MIID0zCCArugAwIBAgIUaDMrqx0VRoOmGHM1afdZt39e2tMwDQYJKoZIhvcNAQEL
BQAwFTETMBEGA1UEAxMKa3ViZXJuZXRlczAeFw0yMDAzMTYxODE4MDBaFw0zMDAz
MTQxODE4MDBaMC0xKzApBgNVBAMTImdhcmRlbmVyLXNlZWQtYWRtaXNzaW9uLWNv
bnRyb2xsZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFqFORLK0P
+h2JxhyqCK850yviF0fByRqffpHfaRyfkGt33VrFXeuhGL+suTicfhzZSWMVojk/
9R3R8FkK02Emq544o9YY5Ho/FGwlE9s1l456dW4F7oblvw7dgcRFdO6N4h/xrVab
5qdNORnxRIZTJ3qz1ZjgcsOwjyzJwyO9PidlG6MW0qqX9Ab+g8Px0eSP2zBhqcLV
6uGy4gYc2+RiXfKgYCsOu+HuNb4DFVediM82J0ZYzchMe5Uqp+PYiBIAH0Xqqz36
GW9rb5O43V5R1HSVDioFrI0EkzWYLFGxol+4TRTNA4sjPXjAJFSXDr6gy6mNYqeI
6DbThhDPMPwtAgMBAAGjggEBMIH+MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAK
BggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQf/8Y23xQcoH8EYnWh
yLGZ31Bk4DAfBgNVHSMEGDAWgBSFA3LvJM21d8qsZVVCe6RrTT9wiTCBiAYDVR0R
BIGAMH6CImdhcmRlbmVyLXNlZWQtYWRtaXNzaW9uLWNvbnRyb2xsZXKCKWdhcmRl
bmVyLXNlZWQtYWRtaXNzaW9uLWNvbnRyb2xsZXIuZ2FyZGVugi1nYXJkZW5lci1z
ZWVkLWFkbWlzc2lvbi1jb250cm9sbGVyLmdhcmRlbi5zdmMwDQYJKoZIhvcNAQEL
BQADggEBALEsnx+Zcv3IME/Xs82x0PAxDuIFV4ZnGPbweCZ5JKKlAtHtrq2JTYoQ
zHbGTj2IEpzdq04RyRqY0ejD25HWeVHcAlhSLGvKKuuMznIl6e4G/Kfmg0NLwiMK
7jsSjpNdHnJOsPg3j3iblP0ZSY8A5p12uqMzfvKPNFK62EuyqmEfI9ec6P6wNAcZ
R3Ejum8yCcOCZlOczOH/8ZIdIC1jlFYm4Wwzm1uUgoSk240nqhuBirWqARjJNhfu
/0HDmy6Zs/2FlRNIWuskpNIgOtMa3A277qx2O542+UhKv2jaIXtX1BnRLTCFVyDZ
gj5593AJYDj8QFHulFeMeh5baOkksjc=
-----END CERTIFICATE-----`
tlsServerKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxahTkSytD/odicYcqgivOdMr4hdHwckan36R32kcn5Brd91a
xV3roRi/rLk4nH4c2UljFaI5P/Ud0fBZCtNhJqueOKPWGOR6PxRsJRPbNZeOenVu
Be6G5b8O3YHERXTujeIf8a1Wm+anTTkZ8USGUyd6s9WY4HLDsI8sycMjvT4nZRuj
FtKql/QG/oPD8dHkj9swYanC1erhsuIGHNvkYl3yoGArDrvh7jW+AxVXnYjPNidG
WM3ITHuVKqfj2IgSAB9F6qs9+hlva2+TuN1eUdR0lQ4qBayNBJM1mCxRsaJfuE0U
zQOLIz14wCRUlw6+oMupjWKniOg204YQzzD8LQIDAQABAoIBAFfUvp2qHpUU7X9F
W4NrLIIjhkKHWcmQ1ZW+JpACI0f8YuT2pdlCLOx/FN1pyPAxUhxz8eWxGoODJmcd
yFN5LpiCdmJw2zhgfrn9Fzk6o5Qi7psYB3X3UlZRGgfwHAlJNqAxtUQtZGkOi5VT
JGYDrzTQPEQhTDegh7izRpG5du4mIXqkrmzTWIwPznLRmAps0fJQuQ9WIUP0iJSt
CMLZ0898GANcdDbE8Ta3emPe3cgJjdUTyH3zMsnJT014N0zzX+e5aXcfxCwAaN+T
fGLaQe1PV714SIhuDo+KBSJo0K0poUA8d5lNIeetl8WD0cpAKjBzpf3CvF78cT3i
c/ZrxIECgYEAzBnDosYxuKIk8iVTe+eTRRwZsi7svaMTnmlcc6/3q5vLb3i1z5V9
n/CEP5ZlvikhNB/Dt3WXmgprHzQN2ljnIJn2KHkR0gWe57aCbYtGxOCijvsZGUoJ
F2iOLfTHBsnxiNP3uzjsuceCuiSD87e0bVBJon4oz6Y7eF6kzKRGFn0CgYEA9+sj
UYtjGZfsEYChtTObC0SLXawkzAGUgJUN1NAh2w5o9Gr61Itt+SwwhqQFQhXyW50d
+bsck3Jk6U6Hke+h9ITUB3Hnqg3KW9L8sPYPqCBT6EQ/qZPmWOKjZTyiSjO1kKx6
+aPM4NKZttzJOcVwQU9m19dvM3xqUfXFPkCve3ECgYAHFcHfzad+NEq6CServm8z
T/VoZQ6cyqNstVWbQnmDgIYAWZ1eFl9lBPFiT7M6da0MZSnjHXbkxwXO8Hymnr1v
OUj9QK6orr9EZeaDLPmI7g9WjUriwNot8Ng2qi+agbobuNf5rNEy5cUY9xmJhVAD
F21m8aAzDR81X3uzCuTP9QKBgQCu9zfZ2PF7oohsYce+Rklpzlo9JbxibcsMZCV6
x9jc7HKN7OJRFoXqkJE+tIsxdKOynFQHZ1JnjRhCv7VV/TTjiMrK5kyE626hF2pW
yZGLKiWNin0ThNnQaUK/s+clTxEYpWG0xTFWicsKDw/Ewd7TeOIv+k70mx298iHe
KXCvQQKBgGdI3bZ1xxKMWDeU5QamDaOkHeZl2SacEQ+C09/O975HLfE05+gsPYDE
+YNg06oQlO/U9tmOvyGX+Ca6yLF/XQMq62oNlp1a0oqnWlQnv57rgKrGXcv2+6sP
LhAfbwDR/NNiimZioPeJEGPocUq21OL5RFjj2Sz5l4NYk6Mmeyfz
-----END RSA PRIVATE KEY-----`
)