Browse code

VKE patch for kubernetes

Change-Id: If2ff0d6824e661f2c90c9d0abf436e653e5b4720
Reviewed-on: http://photon-jenkins.eng.vmware.com:8082/5275
Tested-by: gerrit-photon <photon-checkins@vmware.com>
Reviewed-by: Bo Gan <ganb@vmware.com>

Bo Gan authored on 2018/06/19 20:09:15
Showing 4 changed files
... ...
@@ -1,7 +1,7 @@
1
-From 6b4efdbb56d5d4a0521fd0612e770b17f1e8aae2 Mon Sep 17 00:00:00 2001
1
+From 5db4faec519b8f4c471e59f1083a8b6581d77bd3 Mon Sep 17 00:00:00 2001
2 2
 From: Bo Gan <ganb@vmware.com>
3 3
 Date: Sun, 10 Jun 2018 02:16:47 -0700
4
-Subject: [PATCH] Cascade Kubernetes patches for v1.9.6 (d06c534)
4
+Subject: [PATCH] Cascade Kubernetes patches for v1.9.6 (da5bd0d)
5 5
 
6 6
 ---
7 7
  api/swagger-spec/apps_v1alpha1.json                |  21 +
... ...
@@ -22,18 +22,18 @@ Subject: [PATCH] Cascade Kubernetes patches for v1.9.6 (d06c534)
22 22
  pkg/apis/core/validation/validation.go             |  25 +
23 23
  pkg/apis/extensions/types.go                       |   1 +
24 24
  pkg/cloudprovider/providers/BUILD                  |   2 +
25
- pkg/cloudprovider/providers/cascade/BUILD          |  56 +++
25
+ pkg/cloudprovider/providers/cascade/BUILD          |  56 ++
26 26
  pkg/cloudprovider/providers/cascade/OWNERS         |   3 +
27
- pkg/cloudprovider/providers/cascade/apitypes.go    | 227 +++++++++
28
- pkg/cloudprovider/providers/cascade/auth.go        | 145 ++++++
29
- pkg/cloudprovider/providers/cascade/cascade.go     | 218 +++++++++
30
- .../providers/cascade/cascade_disks.go             | 225 +++++++++
31
- .../providers/cascade/cascade_instances.go         |  91 ++++
27
+ pkg/cloudprovider/providers/cascade/apitypes.go    | 227 ++++++
28
+ pkg/cloudprovider/providers/cascade/auth.go        | 145 ++++
29
+ pkg/cloudprovider/providers/cascade/cascade.go     | 218 ++++++
30
+ .../providers/cascade/cascade_disks.go             | 225 ++++++
31
+ .../providers/cascade/cascade_instances.go         |  91 +++
32 32
  .../providers/cascade/cascade_instances_test.go    |  43 ++
33
- .../providers/cascade/cascade_loadbalancer.go      | 284 +++++++++++
34
- pkg/cloudprovider/providers/cascade/client.go      | 394 +++++++++++++++
35
- pkg/cloudprovider/providers/cascade/oidcclient.go  | 297 ++++++++++++
36
- pkg/cloudprovider/providers/cascade/restclient.go  | 262 ++++++++++
33
+ .../providers/cascade/cascade_loadbalancer.go      | 284 ++++++++
34
+ pkg/cloudprovider/providers/cascade/client.go      | 394 ++++++++++
35
+ pkg/cloudprovider/providers/cascade/oidcclient.go  | 297 ++++++++
36
+ pkg/cloudprovider/providers/cascade/restclient.go  | 262 +++++++
37 37
  pkg/cloudprovider/providers/cascade/tests_owed     |   5 +
38 38
  pkg/cloudprovider/providers/cascade/utils.go       |  25 +
39 39
  pkg/cloudprovider/providers/providers.go           |   1 +
... ...
@@ -41,16 +41,16 @@ Subject: [PATCH] Cascade Kubernetes patches for v1.9.6 (d06c534)
41 41
  pkg/security/podsecuritypolicy/util/util.go        |   3 +
42 42
  pkg/volume/cascade_disk/BUILD                      |  43 ++
43 43
  pkg/volume/cascade_disk/OWNERS                     |   2 +
44
- pkg/volume/cascade_disk/attacher.go                | 269 +++++++++++
45
- pkg/volume/cascade_disk/cascade_disk.go            | 391 +++++++++++++++
46
- pkg/volume/cascade_disk/cascade_util.go            | 107 ++++
47
- .../admission/persistentvolume/label/admission.go  |  54 +++
48
- plugin/pkg/admission/vke/BUILD                     |  58 +++
49
- plugin/pkg/admission/vke/admission.go              | 349 +++++++++++++
50
- plugin/pkg/admission/vke/admission_test.go         | 538 +++++++++++++++++++++
51
- staging/src/k8s.io/api/core/v1/generated.pb.go     | 310 +++++++++++-
44
+ pkg/volume/cascade_disk/attacher.go                | 265 +++++++
45
+ pkg/volume/cascade_disk/cascade_disk.go            | 391 ++++++++++
46
+ pkg/volume/cascade_disk/cascade_util.go            | 152 ++++
47
+ .../admission/persistentvolume/label/admission.go  |  54 ++
48
+ plugin/pkg/admission/vke/BUILD                     |  60 ++
49
+ plugin/pkg/admission/vke/admission.go              | 499 +++++++++++++
50
+ plugin/pkg/admission/vke/admission_test.go         | 809 +++++++++++++++++++++
51
+ staging/src/k8s.io/api/core/v1/generated.pb.go     | 310 +++++++-
52 52
  staging/src/k8s.io/api/core/v1/types.go            |  26 +-
53
- 46 files changed, 4653 insertions(+), 29 deletions(-)
53
+ 46 files changed, 5117 insertions(+), 29 deletions(-)
54 54
  create mode 100644 pkg/cloudprovider/providers/cascade/BUILD
55 55
  create mode 100644 pkg/cloudprovider/providers/cascade/OWNERS
56 56
  create mode 100644 pkg/cloudprovider/providers/cascade/apitypes.go
... ...
@@ -3145,10 +3145,10 @@ index 0000000..c3a4ed7
3145 3145
 +- ashokc
3146 3146
 diff --git a/pkg/volume/cascade_disk/attacher.go b/pkg/volume/cascade_disk/attacher.go
3147 3147
 new file mode 100644
3148
-index 0000000..607fcb5
3148
+index 0000000..66b5836
3149 3149
 --- /dev/null
3150 3150
 +++ b/pkg/volume/cascade_disk/attacher.go
3151
-@@ -0,0 +1,269 @@
3151
+@@ -0,0 +1,265 @@
3152 3152
 +package cascade_disk
3153 3153
 +
3154 3154
 +import (
... ...
@@ -3165,7 +3165,6 @@ index 0000000..607fcb5
3165 3165
 +	"k8s.io/kubernetes/pkg/volume"
3166 3166
 +	volumeutil "k8s.io/kubernetes/pkg/volume/util"
3167 3167
 +	"k8s.io/kubernetes/pkg/volume/util/volumehelper"
3168
-+	"strings"
3169 3168
 +)
3170 3169
 +
3171 3170
 +type cascadeDiskAttacher struct {
... ...
@@ -3207,10 +3206,6 @@ index 0000000..607fcb5
3207 3207
 +		glog.Errorf("Error attaching volume %q to node %q: %+v", volumeSource.DiskID, nodeName, err)
3208 3208
 +		return "", err
3209 3209
 +	}
3210
-+
3211
-+	// Cacsade uses device names of the format /dev/sdX, but newer Linux Kernels mount them under /dev/xvdX
3212
-+	// (source: AWS console). So we have to rename the first occurrence of sd to xvd.
3213
-+	devicePath = strings.Replace(devicePath, "sd", "xvd", 1)
3214 3210
 +	return devicePath, nil
3215 3211
 +}
3216 3212
 +
... ...
@@ -3273,15 +3268,16 @@ index 0000000..607fcb5
3273 3273
 +		select {
3274 3274
 +		case <-ticker.C:
3275 3275
 +			glog.V(4).Infof("Checking disk %s is attached", volumeSource.DiskID)
3276
++			devicePath := getDiskByIdPath(devicePath)
3276 3277
 +			checkPath, err := verifyDevicePath(devicePath)
3277 3278
 +			if err != nil {
3278 3279
 +				// Log error, if any, and continue checking periodically. See issue #11321
3279
-+				glog.Warningf("Cascade attacher: WaitForAttach with devicePath %s Checking PD %s Error verify "+
3280
++				glog.Warningf("VKE attacher: WaitForAttach with devicePath %s Checking PD %s Error verify "+
3280 3281
 +					"path", devicePath, volumeSource.DiskID)
3281 3282
 +			} else if checkPath != "" {
3282 3283
 +				// A device path has successfully been created for the disk
3283 3284
 +				glog.V(4).Infof("Successfully found attached disk %s.", volumeSource.DiskID)
3284
-+				return devicePath, nil
3285
++				return checkPath, nil
3285 3286
 +			}
3286 3287
 +		case <-timer.C:
3287 3288
 +			return "", fmt.Errorf("Could not find attached disk %s. Timeout waiting for mount paths to be "+
... ...
@@ -3818,14 +3814,16 @@ index 0000000..a25f224
3818 3818
 \ No newline at end of file
3819 3819
 diff --git a/pkg/volume/cascade_disk/cascade_util.go b/pkg/volume/cascade_disk/cascade_util.go
3820 3820
 new file mode 100644
3821
-index 0000000..3dcef3d
3821
+index 0000000..cbcc115
3822 3822
 --- /dev/null
3823 3823
 +++ b/pkg/volume/cascade_disk/cascade_util.go
3824
-@@ -0,0 +1,107 @@
3824
+@@ -0,0 +1,152 @@
3825 3825
 +package cascade_disk
3826 3826
 +
3827 3827
 +import (
3828 3828
 +	"fmt"
3829
++	"os"
3830
++	"path/filepath"
3829 3831
 +	"strings"
3830 3832
 +	"time"
3831 3833
 +
... ...
@@ -3854,6 +3852,18 @@ index 0000000..3dcef3d
3854 3854
 +	return "", nil
3855 3855
 +}
3856 3856
 +
3857
++// Returns path for given VKE disk mount
3858
++func getDiskByIdPath(devicePath string) string {
3859
++	nvmePath, err := findNvmeVolume(devicePath)
3860
++	if err != nil {
3861
++		glog.Warningf("error looking for nvme volume %q: %v", devicePath, err)
3862
++	} else if nvmePath != "" {
3863
++		devicePath = nvmePath
3864
++	}
3865
++
3866
++	return devicePath
3867
++}
3868
++
3857 3869
 +// CreateVolume creates a Cascade persistent disk.
3858 3870
 +func (util *CascadeDiskUtil) CreateVolume(p *cascadeDiskProvisioner) (diskID string, capacityGB int, fstype string,
3859 3871
 +	err error) {
... ...
@@ -3929,6 +3939,37 @@ index 0000000..3dcef3d
3929 3929
 +	}
3930 3930
 +	return cc, nil
3931 3931
 +}
3932
++
3933
++// findNvmeVolume looks for the nvme volume with the specified name
3934
++// It follows the symlink (if it exists) and returns the absolute path to the device
3935
++func findNvmeVolume(findName string) (device string, err error) {
3936
++	stat, err := os.Lstat(findName)
3937
++	if err != nil {
3938
++		if os.IsNotExist(err) {
3939
++			glog.V(6).Infof("nvme path not found %q", findName)
3940
++			return "", nil
3941
++		}
3942
++		return "", fmt.Errorf("error getting stat of %q: %v", findName, err)
3943
++	}
3944
++
3945
++	if stat.Mode()&os.ModeSymlink != os.ModeSymlink {
3946
++		glog.Warningf("nvme file %q found, but was not a symlink", findName)
3947
++		return "", nil
3948
++	}
3949
++
3950
++	// Find the target, resolving to an absolute path
3951
++	// For example, /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 -> ../../nvme2n1
3952
++	resolved, err := filepath.EvalSymlinks(findName)
3953
++	if err != nil {
3954
++		return "", fmt.Errorf("error reading target of symlink %q: %v", findName, err)
3955
++	}
3956
++
3957
++	if !strings.HasPrefix(resolved, "/dev") {
3958
++		return "", fmt.Errorf("resolved symlink for %q was unexpected: %q", findName, resolved)
3959
++	}
3960
++
3961
++	return resolved, nil
3962
++}
3932 3963
 diff --git a/plugin/pkg/admission/persistentvolume/label/admission.go b/plugin/pkg/admission/persistentvolume/label/admission.go
3933 3964
 index 86e1921..bf2912b 100644
3934 3965
 --- a/plugin/pkg/admission/persistentvolume/label/admission.go
... ...
@@ -4014,10 +4055,10 @@ index 86e1921..bf2912b 100644
4014 4014
 +}
4015 4015
 diff --git a/plugin/pkg/admission/vke/BUILD b/plugin/pkg/admission/vke/BUILD
4016 4016
 new file mode 100644
4017
-index 0000000..b0a6026
4017
+index 0000000..d0bb7c7
4018 4018
 --- /dev/null
4019 4019
 +++ b/plugin/pkg/admission/vke/BUILD
4020
-@@ -0,0 +1,58 @@
4020
+@@ -0,0 +1,60 @@
4021 4021
 +package(default_visibility = ["//visibility:public"])
4022 4022
 +
4023 4023
 +load(
... ...
@@ -4032,10 +4073,12 @@ index 0000000..b0a6026
4032 4032
 +    deps = [
4033 4033
 +        "//pkg/apis/core:go_default_library",
4034 4034
 +        "//pkg/apis/extensions:go_default_library",
4035
++        "//pkg/apis/extensions/v1beta1:go_default_library",
4035 4036
 +        "//pkg/apis/rbac:go_default_library",
4036 4037
 +        "//pkg/registry/rbac:go_default_library",
4037 4038
 +        "//pkg/security/podsecuritypolicy:go_default_library",
4038 4039
 +        "//vendor/github.com/golang/glog:go_default_library",
4040
++        "//vendor/k8s.io/api/extensions/v1beta1:go_default_library"
4039 4041
 +        "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
4040 4042
 +        "//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
4041 4043
 +        "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
... ...
@@ -4079,18 +4122,20 @@ index 0000000..b0a6026
4079 4079
 \ No newline at end of file
4080 4080
 diff --git a/plugin/pkg/admission/vke/admission.go b/plugin/pkg/admission/vke/admission.go
4081 4081
 new file mode 100644
4082
-index 0000000..6325ca0
4082
+index 0000000..c73fc1b
4083 4083
 --- /dev/null
4084 4084
 +++ b/plugin/pkg/admission/vke/admission.go
4085
-@@ -0,0 +1,349 @@
4085
+@@ -0,0 +1,499 @@
4086 4086
 +package vke
4087 4087
 +
4088 4088
 +import (
4089 4089
 +	"fmt"
4090 4090
 +	"io"
4091
++	"os"
4091 4092
 +	"strings"
4092 4093
 +
4093 4094
 +	"github.com/golang/glog"
4095
++	"k8s.io/api/extensions/v1beta1"
4094 4096
 +	apiequality "k8s.io/apimachinery/pkg/api/equality"
4095 4097
 +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4096 4098
 +	"k8s.io/apimachinery/pkg/util/validation/field"
... ...
@@ -4098,6 +4143,7 @@ index 0000000..6325ca0
4098 4098
 +	"k8s.io/apiserver/pkg/admission"
4099 4099
 +	api "k8s.io/kubernetes/pkg/apis/core"
4100 4100
 +	"k8s.io/kubernetes/pkg/apis/extensions"
4101
++	policybeta "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
4101 4102
 +	"k8s.io/kubernetes/pkg/apis/rbac"
4102 4103
 +	rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
4103 4104
 +	"k8s.io/kubernetes/pkg/security/podsecuritypolicy"
... ...
@@ -4113,6 +4159,10 @@ index 0000000..6325ca0
4113 4113
 +	reservedPrefix           = "vke"
4114 4114
 +	kubeletGroup             = "system:nodes"
4115 4115
 +	kubeProxyGroup           = "vke:kube-proxies"
4116
++	reservedTolerationKey    = "Dedicated"
4117
++	reservedTolerationValue  = "Master"
4118
++	masterNodePrefix         = "master"
4119
++	etcSslCerts              = "/etc/ssl/certs"
4116 4120
 +)
4117 4121
 +
4118 4122
 +// Register registers a plugin.
... ...
@@ -4132,7 +4182,8 @@ index 0000000..6325ca0
4132 4132
 +
4133 4133
 +// vmwareAdmissionControllerConfig holds config data for VMwareAdmissionController.
4134 4134
 +type vmwareAdmissionControllerConfig struct {
4135
-+	PrivilegedGroup string `yaml:"privilegedGroup"`
4135
++	PrivilegedGroup       string `yaml:"privilegedGroup"`
4136
++	PodSecurityPolicyFile string `yaml:"podSecurityPolicyFile"`
4136 4137
 +}
4137 4138
 +
4138 4139
 +// AdmissionConfig holds config data for admission controllers.
... ...
@@ -4161,7 +4212,7 @@ index 0000000..6325ca0
4161 4161
 +	case api.Resource("pods"):
4162 4162
 +		err = validatePods(vac, a)
4163 4163
 +	case api.Resource("nodes"):
4164
-+		err = admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot modify nodes", PluginName))
4164
++		err = validateNodes(a)
4165 4165
 +	case rbac.Resource("clusterroles"):
4166 4166
 +		err = validateClusterRoles(a)
4167 4167
 +	case rbac.Resource("clusterrolebindings"):
... ...
@@ -4192,8 +4243,14 @@ index 0000000..6325ca0
4192 4192
 +		return nil, err
4193 4193
 +	}
4194 4194
 +
4195
++	// Load PSP from file. If it fails, use default.
4196
++	psp := getPSPFromFile(config.VMwareAdmissionController.PodSecurityPolicyFile)
4197
++	if psp == nil {
4198
++		psp = getDefaultPSP()
4199
++	}
4200
++
4195 4201
 +	return &vmwareAdmissionController{
4196
-+		psp:             getDefaultPSP(),
4202
++		psp:             psp,
4197 4203
 +		strategyFactory: podsecuritypolicy.NewSimpleStrategyFactory(),
4198 4204
 +		privilegedGroup: config.VMwareAdmissionController.PrivilegedGroup,
4199 4205
 +	}, nil
... ...
@@ -4221,6 +4278,14 @@ index 0000000..6325ca0
4221 4221
 +				"configMap",
4222 4222
 +				"persistentVolumeClaim",
4223 4223
 +				"projected",
4224
++				"hostPath",
4225
++			},
4226
++			// We allow /etc/ssl/certs to be mounted in read only mode as a hack to allow Wavefront pods to be deployed.
4227
++			// TODO(ashokc): Once we have support for users to create pods using privileged mode and host path, remove this.
4228
++			AllowedHostPaths: []extensions.AllowedHostPath{
4229
++				{
4230
++					etcSslCerts,
4231
++				},
4224 4232
 +			},
4225 4233
 +			FSGroup: extensions.FSGroupStrategyOptions{
4226 4234
 +				Rule: extensions.FSGroupStrategyRunAsAny,
... ...
@@ -4238,6 +4303,39 @@ index 0000000..6325ca0
4238 4238
 +	}
4239 4239
 +}
4240 4240
 +
4241
++func getPSPFromFile(pspFile string) *extensions.PodSecurityPolicy {
4242
++	pspBeta := v1beta1.PodSecurityPolicy{}
4243
++	pspExtensions := extensions.PodSecurityPolicy{}
4244
++
4245
++	if pspFile == "" {
4246
++		glog.V(2).Infof("%s: PSP file not specified, using default PSP", PluginName)
4247
++		return nil
4248
++	}
4249
++
4250
++	pspConfig, err := os.Open(pspFile)
4251
++	if err != nil {
4252
++		glog.V(2).Infof("%s: cannot open PSP file, using default PSP: %v", PluginName, err)
4253
++		return nil
4254
++	}
4255
++
4256
++	// We load the PSP that we read from file into pspBeta because this is the struct to which we can decode yaml to.
4257
++	d := yaml.NewYAMLOrJSONDecoder(pspConfig, 4096)
4258
++	err = d.Decode(&pspBeta)
4259
++	if err != nil {
4260
++		glog.V(2).Infof("%s: cannot decode PSP file, using default PSP: %v", PluginName, err)
4261
++		return nil
4262
++	}
4263
++
4264
++	// We convert pspBeta object into pspExtensions object because this is the one that pod validation uses.
4265
++	err = policybeta.Convert_v1beta1_PodSecurityPolicy_To_extensions_PodSecurityPolicy(&pspBeta, &pspExtensions, nil)
4266
++	if err != nil {
4267
++		glog.V(2).Infof("%s: cannot convert v1beta1.PSP to extensions.PSP, using default PSP: %v", PluginName, err)
4268
++		return nil
4269
++	}
4270
++
4271
++	return &pspExtensions
4272
++}
4273
++
4241 4274
 +func isPrivilegedUser(vac *vmwareAdmissionController, a admission.Attributes) bool {
4242 4275
 +	// We need to allow the service accounts inside the privileged namespace to be able to access pods and nodes.
4243 4276
 +	// Node-monitor agent, Photon OS update controller and Cluster autoscaler depend on this.
... ...
@@ -4305,6 +4403,22 @@ index 0000000..6325ca0
4305 4305
 +	return false
4306 4306
 +}
4307 4307
 +
4308
++func validateNodes(a admission.Attributes) error {
4309
++	// If the operation is Delete, fail. Deleting a node is not something that is useful to the user. Also, by deleting
4310
++	// a node, they can potentially make their cluster useless.
4311
++	if a.GetOperation() == admission.Delete {
4312
++		return admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot delete nodes", PluginName))
4313
++	}
4314
++
4315
++	// If the operation is on a master node, fail. We do not want to allow the users to modify labels and taints on the
4316
++	// master node because it can compromise the security of the cluster.
4317
++	if strings.HasPrefix(a.GetName(), masterNodePrefix) {
4318
++		return admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot modify master nodes", PluginName))
4319
++	}
4320
++
4321
++	return nil
4322
++}
4323
++
4308 4324
 +func validateClusterRoles(a admission.Attributes) error {
4309 4325
 +	// If the name in the request is not empty and has the reserved prefix, then fail. We will hit this during delete
4310 4326
 +	// and update operations on the cluster roles. If it does not have the reserved prefix, allow it. If the name is
... ...
@@ -4393,6 +4507,12 @@ index 0000000..6325ca0
4393 4393
 +	// Validate the pod.
4394 4394
 +	errs = append(errs, provider.ValidatePodSecurityContext(pod, field.NewPath("spec", "securityContext"))...)
4395 4395
 +
4396
++	// Validate the pod's tolerations.
4397
++	fieldErr := validatePodToleration(pod)
4398
++	if fieldErr != nil {
4399
++		errs = append(errs, fieldErr)
4400
++	}
4401
++
4396 4402
 +	// Validate the initContainers that are part of the pod.
4397 4403
 +	for i := range pod.Spec.InitContainers {
4398 4404
 +		pod.Spec.InitContainers[i].SecurityContext, _, err = provider.CreateContainerSecurityContext(pod, &pod.Spec.InitContainers[i])
... ...
@@ -4417,6 +4537,12 @@ index 0000000..6325ca0
4417 4417
 +			field.NewPath("spec", "containers").Index(i).Child("securityContext"))...)
4418 4418
 +	}
4419 4419
 +
4420
++	// Validate that /etc/ssl/certs if mounted using hostPath volume mount is readOnly.
4421
++	fieldErr = validateEtcSslCertsHostPath(pod)
4422
++	if fieldErr != nil {
4423
++		errs = append(errs, fieldErr)
4424
++	}
4425
++
4420 4426
 +	if len(errs) > 0 {
4421 4427
 +		return admission.NewForbidden(a,
4422 4428
 +			fmt.Errorf("%s validation failed: %v", PluginName, errs))
... ...
@@ -4425,6 +4551,73 @@ index 0000000..6325ca0
4425 4425
 +	return nil
4426 4426
 +}
4427 4427
 +
4428
++func validatePodToleration(pod *api.Pod) *field.Error {
4429
++	// Master nodes are tainted with "Dedicated=Master:NoSchedule". Only vke-system pods are allowed to tolerate
4430
++	// this taint and to run on master nodes. A user's pod will be rejected if its spec has toleration for this taint.
4431
++	for _, t := range pod.Spec.Tolerations {
4432
++		reject := false
4433
++
4434
++		if t.Key == reservedTolerationKey && t.Value == reservedTolerationValue {
4435
++			// Reject pod that has the reserved toleration "Dedicated=Master"
4436
++			reject = true
4437
++		} else if t.Operator == api.TolerationOpExists && (t.Key == reservedTolerationKey || t.Key == "") {
4438
++			// Reject pod that has wildcard toleration matching the reserved toleration
4439
++			reject = true
4440
++		}
4441
++
4442
++		if reject {
4443
++			return field.Invalid(field.NewPath("spec", "toleration"), fmt.Sprintf("%+v", t),
4444
++				fmt.Sprintf("%s validation failed: should not tolerate master node taint", PluginName))
4445
++		}
4446
++	}
4447
++	return nil
4448
++}
4449
++
4450
++// Validate that /etc/ssl/certs if mounted using hostPath volume mount is readOnly. If not, fail.
4451
++// This is a hack that is needed to get Wavefront pods to work.
4452
++// TODO(ashokc): Once we have support for users to create pods using privileged mode and host path, remove this.
4453
++func validateEtcSslCertsHostPath(pod *api.Pod) *field.Error {
4454
++	// Get volumes which mount /etc/ssl/certs and put them in a map.
4455
++	volumes := map[string]struct{}{}
4456
++	for _, vol := range pod.Spec.Volumes {
4457
++		if vol.HostPath != nil && strings.HasPrefix(vol.HostPath.Path, etcSslCerts) {
4458
++			volumes[vol.Name] = struct{}{}
4459
++		}
4460
++	}
4461
++
4462
++	// For every initContainer, get all volumeMounts and verify if it matches any of the volumes in the volumes map.
4463
++	// If yes, then check if they are read only. If not, return an error.
4464
++	err := checkVolumeReadOnly(pod.Spec.InitContainers, volumes, "initContainers")
4465
++	if err != nil {
4466
++		return err
4467
++	}
4468
++
4469
++	// For every container, get all volumeMounts and verify if it matches any of the volumes in the volumes map.
4470
++	// If yes, then check if they are read only. If not, return an error.
4471
++	err = checkVolumeReadOnly(pod.Spec.Containers, volumes, "containers")
4472
++	if err != nil {
4473
++		return err
4474
++	}
4475
++
4476
++	return nil
4477
++}
4478
++
4479
++// Checks if the container has a volumeMount belonging to the volumes map. If yes, it has to be read only. If not,
4480
++// return error.
4481
++func checkVolumeReadOnly(containers []api.Container, volumes map[string]struct{}, containerType string) *field.Error {
4482
++	for i, container := range containers {
4483
++		for _, vol := range container.VolumeMounts {
4484
++			if _, ok := volumes[vol.Name]; ok {
4485
++				if !vol.ReadOnly {
4486
++					return field.Invalid(field.NewPath("spec", containerType).Index(i).Child("volumeMounts"),
4487
++						fmt.Sprintf("%+v", vol), fmt.Sprintf("%s has to be mount as readOnly", etcSslCerts))
4488
++				}
4489
++			}
4490
++		}
4491
++	}
4492
++	return nil
4493
++}
4494
++
4428 4495
 +func checkReservedPrefix(resourceName string, a admission.Attributes) error {
4429 4496
 +	if strings.HasPrefix(resourceName, reservedPrefix) {
4430 4497
 +		return admission.NewForbidden(a,
... ...
@@ -4434,14 +4627,15 @@ index 0000000..6325ca0
4434 4434
 +}
4435 4435
 diff --git a/plugin/pkg/admission/vke/admission_test.go b/plugin/pkg/admission/vke/admission_test.go
4436 4436
 new file mode 100644
4437
-index 0000000..596b7d4
4437
+index 0000000..52c88cc
4438 4438
 --- /dev/null
4439 4439
 +++ b/plugin/pkg/admission/vke/admission_test.go
4440
-@@ -0,0 +1,538 @@
4440
+@@ -0,0 +1,809 @@
4441 4441
 +package vke
4442 4442
 +
4443 4443
 +import (
4444 4444
 +	"fmt"
4445
++	"os"
4445 4446
 +	"strings"
4446 4447
 +	"testing"
4447 4448
 +
... ...
@@ -4457,6 +4651,38 @@ index 0000000..596b7d4
4457 4457
 +	defaultConfigFileFormat  = `
4458 4458
 +vmwareAdmissionController:
4459 4459
 +  privilegedGroup: %s
4460
++  podSecurityPolicyFile: %s
4461
++`
4462
++	pspFileName   = "/tmp/psp.yaml"
4463
++	pspConfigFile = `
4464
++apiVersion: extensions/v1beta1
4465
++kind: PodSecurityPolicy
4466
++metadata:
4467
++  name: vmware-pod-security-policy-restricted
4468
++spec:
4469
++  privileged: true
4470
++  fsGroup:
4471
++    rule: RunAsAny
4472
++  runAsUser:
4473
++    rule: RunAsAny
4474
++  seLinux:
4475
++    rule: RunAsAny
4476
++  supplementalGroups:
4477
++    rule: RunAsAny
4478
++  volumes:
4479
++  - 'emptyDir'
4480
++  - 'secret'
4481
++  - 'downwardAPI'
4482
++  - 'configMap'
4483
++  - 'persistentVolumeClaim'
4484
++  - 'projected'
4485
++  - 'hostPath'
4486
++  hostPID: false
4487
++  hostIPC: false
4488
++  hostNetwork: true
4489
++  hostPorts:
4490
++  - min: 1
4491
++    max: 65536
4460 4492
 +`
4461 4493
 +)
4462 4494
 +
... ...
@@ -4524,7 +4750,19 @@ index 0000000..596b7d4
4524 4524
 +		},
4525 4525
 +		"create pod with HostVolume denied": {
4526 4526
 +			operation:          kadmission.Create,
4527
-+			pod:                newTestPodBuilder().withHostVolume().build(),
4527
++			pod:                newTestPodBuilder().withHostVolume("/", false).build(),
4528
++			userInfo:           newTestUserBuilder().build(),
4529
++			shouldPassValidate: false,
4530
++		},
4531
++		"create pod with HostVolume /etc/ssl/certs in read-only mode allowed": {
4532
++			operation:          kadmission.Create,
4533
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", true).build(),
4534
++			userInfo:           newTestUserBuilder().build(),
4535
++			shouldPassValidate: true,
4536
++		},
4537
++		"create pod with HostVolume /etc/ssl/certs in read-write mode denied": {
4538
++			operation:          kadmission.Create,
4539
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", false).build(),
4528 4540
 +			userInfo:           newTestUserBuilder().build(),
4529 4541
 +			shouldPassValidate: false,
4530 4542
 +		},
... ...
@@ -4536,7 +4774,7 @@ index 0000000..596b7d4
4536 4536
 +		},
4537 4537
 +		"create pod with HostVolume and CascadeDisk denied": {
4538 4538
 +			operation:          kadmission.Create,
4539
-+			pod:                newTestPodBuilder().withHostVolume().withCascadeDisk().build(),
4539
++			pod:                newTestPodBuilder().withHostVolume("/", false).withCascadeDisk().build(),
4540 4540
 +			userInfo:           newTestUserBuilder().build(),
4541 4541
 +			shouldPassValidate: false,
4542 4542
 +		},
... ...
@@ -4548,15 +4786,144 @@ index 0000000..596b7d4
4548 4548
 +		},
4549 4549
 +		"delete pod allowed": {
4550 4550
 +			operation:          kadmission.Delete,
4551
++			pod:                nil,
4552
++			userInfo:           newTestUserBuilder().build(),
4553
++			shouldPassValidate: true,
4554
++		},
4555
++	}
4556
++
4557
++	for k, v := range tests {
4558
++		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4559
++	}
4560
++}
4561
++
4562
++func TestAdmitPrivilegedWithCustomPSP(t *testing.T) {
4563
++	tests := map[string]struct {
4564
++		operation          kadmission.Operation
4565
++		pod                *kapi.Pod
4566
++		name               string
4567
++		userInfo           user.Info
4568
++		shouldPassValidate bool
4569
++	}{
4570
++		"create pod with Privileged=nil allowed": {
4571
++			operation:          kadmission.Create,
4572
++			pod:                newTestPodBuilder().build(),
4573
++			userInfo:           newTestUserBuilder().build(),
4574
++			shouldPassValidate: true,
4575
++		},
4576
++		"create pod with Privileged=false allowed": {
4577
++			operation:          kadmission.Create,
4578
++			pod:                newTestPodBuilder().withPrivileged(false).build(),
4579
++			userInfo:           newTestUserBuilder().build(),
4580
++			shouldPassValidate: true,
4581
++		},
4582
++		"create pod with Privileged=true allowed": {
4583
++			operation:          kadmission.Create,
4584
++			pod:                newTestPodBuilder().withPrivileged(true).build(),
4585
++			userInfo:           newTestUserBuilder().build(),
4586
++			shouldPassValidate: true,
4587
++		},
4588
++		"create pod with multiple containers, one has Privileged=true allowed": {
4589
++			operation:          kadmission.Create,
4590
++			pod:                newTestPodBuilder().withPrivileged(true).withInitContainer().withContainer().build(),
4591
++			userInfo:           newTestUserBuilder().build(),
4592
++			shouldPassValidate: true,
4593
++		},
4594
++		"update pod with Privileged=true allowed": {
4595
++			operation:          kadmission.Update,
4596
++			pod:                newTestPodBuilder().withPrivileged(true).build(),
4597
++			userInfo:           newTestUserBuilder().build(),
4598
++			shouldPassValidate: true,
4599
++		},
4600
++		"create pod with HostNetwork=true allowed": {
4601
++			operation:          kadmission.Create,
4602
++			pod:                newTestPodBuilder().withHostNetwork(true).build(),
4603
++			userInfo:           newTestUserBuilder().build(),
4604
++			shouldPassValidate: true,
4605
++		},
4606
++		"create pod with HostIPC=true denied": {
4607
++			operation:          kadmission.Create,
4608
++			pod:                newTestPodBuilder().withHostIPC(true).build(),
4609
++			userInfo:           newTestUserBuilder().build(),
4610
++			shouldPassValidate: false,
4611
++		},
4612
++		"create pod with HostPID=true denied": {
4613
++			operation:          kadmission.Create,
4614
++			pod:                newTestPodBuilder().withHostPID(true).build(),
4615
++			userInfo:           newTestUserBuilder().build(),
4616
++			shouldPassValidate: false,
4617
++		},
4618
++		"create pod with HostPort allowed": {
4619
++			operation:          kadmission.Create,
4620
++			pod:                newTestPodBuilder().withHostPort().build(),
4621
++			userInfo:           newTestUserBuilder().build(),
4622
++			shouldPassValidate: true,
4623
++		},
4624
++		"create pod with HostVolume allowed": {
4625
++			operation:          kadmission.Create,
4626
++			pod:                newTestPodBuilder().withHostVolume("/", false).build(),
4627
++			userInfo:           newTestUserBuilder().build(),
4628
++			shouldPassValidate: true,
4629
++		},
4630
++		"create pod with HostVolume /etc/ssl/certs in read-only mode allowed": {
4631
++			operation:          kadmission.Create,
4632
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", true).build(),
4633
++			userInfo:           newTestUserBuilder().build(),
4634
++			shouldPassValidate: true,
4635
++		},
4636
++		"create pod with HostVolume /etc/ssl/certs in read-write mode denied": {
4637
++			operation:          kadmission.Create,
4638
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", false).build(),
4639
++			userInfo:           newTestUserBuilder().build(),
4640
++			shouldPassValidate: false,
4641
++		},
4642
++		"create pod with CascadeDisk allowed": {
4643
++			operation:          kadmission.Create,
4644
++			pod:                newTestPodBuilder().withCascadeDisk().build(),
4645
++			userInfo:           newTestUserBuilder().build(),
4646
++			shouldPassValidate: true,
4647
++		},
4648
++		"create pod with HostVolume and CascadeDisk allowed": {
4649
++			operation:          kadmission.Create,
4650
++			pod:                newTestPodBuilder().withHostVolume("/", false).withCascadeDisk().build(),
4651
++			userInfo:           newTestUserBuilder().build(),
4652
++			shouldPassValidate: true,
4653
++		},
4654
++		"connect pod allowed": {
4655
++			operation:          kadmission.Connect,
4551 4656
 +			pod:                newTestPodBuilder().build(),
4552 4657
 +			userInfo:           newTestUserBuilder().build(),
4553 4658
 +			shouldPassValidate: true,
4554 4659
 +		},
4660
++		"delete pod allowed": {
4661
++			operation:          kadmission.Delete,
4662
++			pod:                nil,
4663
++			userInfo:           newTestUserBuilder().build(),
4664
++			shouldPassValidate: true,
4665
++		},
4666
++	}
4667
++
4668
++	// Setup custom PSP file.
4669
++	file, err := os.Create(pspFileName)
4670
++	if err != nil {
4671
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to open custom PSP file %v", err)
4672
++		return
4673
++	}
4674
++	_, err = file.WriteString(pspConfigFile)
4675
++	if err != nil {
4676
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to write to custom PSP file %v", err)
4677
++		return
4555 4678
 +	}
4556 4679
 +
4557 4680
 +	for k, v := range tests {
4558 4681
 +		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4559 4682
 +	}
4683
++
4684
++	// Delete custom PSP file.
4685
++	err = os.Remove(pspFileName)
4686
++	if err != nil {
4687
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to delete custom PSP file %v", err)
4688
++	}
4560 4689
 +}
4561 4690
 +
4562 4691
 +func TestPrivilegedNamespace(t *testing.T) {
... ...
@@ -4639,6 +5006,63 @@ index 0000000..596b7d4
4639 4639
 +	}
4640 4640
 +}
4641 4641
 +
4642
++func TestToleration(t *testing.T) {
4643
++	tests := map[string]struct {
4644
++		operation          kadmission.Operation
4645
++		pod                *kapi.Pod
4646
++		name               string
4647
++		userInfo           user.Info
4648
++		shouldPassValidate bool
4649
++	}{
4650
++		"allowed: create pod with no toleration": {
4651
++			operation:          kadmission.Create,
4652
++			pod:                newTestPodBuilder().build(),
4653
++			userInfo:           newTestUserBuilder().build(),
4654
++			shouldPassValidate: true,
4655
++		},
4656
++		"allowed: create pod with normal toleration key": {
4657
++			operation:          kadmission.Create,
4658
++			pod:                newTestPodBuilder().withToleration("mykey", reservedTolerationValue, kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4659
++			userInfo:           newTestUserBuilder().build(),
4660
++			shouldPassValidate: true,
4661
++		},
4662
++		"allowed: create pod with normal toleration value": {
4663
++			operation:          kadmission.Create,
4664
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, "myval", kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4665
++			userInfo:           newTestUserBuilder().build(),
4666
++			shouldPassValidate: true,
4667
++		},
4668
++		"denied: create pod with reserved toleration": {
4669
++			operation:          kadmission.Create,
4670
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, reservedTolerationValue, kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4671
++			userInfo:           newTestUserBuilder().build(),
4672
++			shouldPassValidate: false,
4673
++		},
4674
++		"denied: create pod with wildcard toleration": {
4675
++			operation:          kadmission.Create,
4676
++			pod:                newTestPodBuilder().withToleration("", "", kapi.TolerationOpExists, "").build(),
4677
++			userInfo:           newTestUserBuilder().build(),
4678
++			shouldPassValidate: false,
4679
++		},
4680
++		"denied: create pod with value wildcard toleration": {
4681
++			operation:          kadmission.Create,
4682
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, "", kapi.TolerationOpExists, kapi.TaintEffectNoSchedule).build(),
4683
++			userInfo:           newTestUserBuilder().build(),
4684
++			shouldPassValidate: false,
4685
++		},
4686
++		"allowed: create pod with value wildcard and normal key": {
4687
++			operation:          kadmission.Create,
4688
++			pod:                newTestPodBuilder().withToleration("mykey", "", kapi.TolerationOpExists, kapi.TaintEffectNoSchedule).build(),
4689
++			userInfo:           newTestUserBuilder().build(),
4690
++			shouldPassValidate: true,
4691
++		},
4692
++	}
4693
++
4694
++	for k, v := range tests {
4695
++		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4696
++	}
4697
++}
4698
++
4642 4699
 +func TestClusterLevelResources(t *testing.T) {
4643 4700
 +	tests := map[string]struct {
4644 4701
 +		operation          kadmission.Operation
... ...
@@ -4747,9 +5171,34 @@ index 0000000..596b7d4
4747 4747
 +			userInfo:           newTestUserBuilder().withName(systemUnsecuredUser).build(),
4748 4748
 +			shouldPassValidate: true,
4749 4749
 +		},
4750
-+		"denied: regular lightwave user update nodes": {
4750
++		"allowed: regular lightwave user update worker nodes": {
4751 4751
 +			operation:          kadmission.Update,
4752 4752
 +			resource:           "nodes",
4753
++			name:               "worker-guid",
4754
++			namespace:          "",
4755
++			userInfo:           newTestUserBuilder().build(),
4756
++			shouldPassValidate: true,
4757
++		},
4758
++		"denied: regular lightwave user update master nodes": {
4759
++			operation:          kadmission.Update,
4760
++			resource:           "nodes",
4761
++			name:               "master-guid",
4762
++			namespace:          "",
4763
++			userInfo:           newTestUserBuilder().build(),
4764
++			shouldPassValidate: false,
4765
++		},
4766
++		"denied: regular lightwave user delete master nodes": {
4767
++			operation:          kadmission.Delete,
4768
++			resource:           "nodes",
4769
++			name:               "master-guid",
4770
++			namespace:          "",
4771
++			userInfo:           newTestUserBuilder().build(),
4772
++			shouldPassValidate: false,
4773
++		},
4774
++		"denied: regular lightwave user delete worker nodes": {
4775
++			operation:          kadmission.Delete,
4776
++			resource:           "nodes",
4777
++			name:               "worker-guid",
4753 4778
 +			namespace:          "",
4754 4779
 +			userInfo:           newTestUserBuilder().build(),
4755 4780
 +			shouldPassValidate: false,
... ...
@@ -4770,15 +5219,20 @@ index 0000000..596b7d4
4770 4770
 +func testPodValidation(testCaseName string, op kadmission.Operation, pod *kapi.Pod, name string, userInfo user.Info,
4771 4771
 +	shouldPassValidate bool, t *testing.T) {
4772 4772
 +
4773
-+	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup)
4773
++	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup, pspFileName)
4774 4774
 +	configFile := strings.NewReader(defaultConfigFile)
4775 4775
 +	plugin, err := NewVMwareAdmissionController(configFile)
4776 4776
 +	if err != nil {
4777 4777
 +		t.Errorf("%s: failed to create admission controller %v", testCaseName, err)
4778 4778
 +	}
4779 4779
 +
4780
++	namespace := "default"
4781
++	if pod != nil {
4782
++		namespace = pod.Namespace
4783
++	}
4784
++
4780 4785
 +	attrs := kadmission.NewAttributesRecord(pod, nil, kapi.Kind("Pod").WithVersion("version"),
4781
-+		pod.Namespace, name, kapi.Resource("pods").WithVersion("version"), "", op, userInfo)
4786
++		namespace, name, kapi.Resource("pods").WithVersion("version"), "", op, userInfo)
4782 4787
 +
4783 4788
 +	err = plugin.Validate(attrs)
4784 4789
 +	if shouldPassValidate && err != nil {
... ...
@@ -4791,7 +5245,7 @@ index 0000000..596b7d4
4791 4791
 +func testResourceValidation(testCaseName string, op kadmission.Operation, resource string, name string,
4792 4792
 +	namespace string, userInfo user.Info, shouldPassValidate bool, t *testing.T) {
4793 4793
 +
4794
-+	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup)
4794
++	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup, pspFileName)
4795 4795
 +	configFile := strings.NewReader(defaultConfigFile)
4796 4796
 +	plugin, err := NewVMwareAdmissionController(configFile)
4797 4797
 +	if err != nil {
... ...
@@ -4891,19 +5345,19 @@ index 0000000..596b7d4
4891 4891
 +	return p
4892 4892
 +}
4893 4893
 +
4894
-+func (p *testPodBuilder) withHostVolume() *testPodBuilder {
4894
++func (p *testPodBuilder) withHostVolume(hostPath string, readOnly bool) *testPodBuilder {
4895 4895
 +	volume := kapi.Volume{
4896 4896
 +		Name: "host",
4897 4897
 +		VolumeSource: kapi.VolumeSource{
4898 4898
 +			HostPath: &kapi.HostPathVolumeSource{
4899
-+				Path: "/",
4899
++				Path: hostPath,
4900 4900
 +			},
4901 4901
 +		},
4902 4902
 +	}
4903
-+	device := kapi.VolumeDevice{Name: "host", DevicePath: "/host"}
4903
++	volumeMount := kapi.VolumeMount{Name: "host", MountPath: "/data", ReadOnly: readOnly}
4904 4904
 +
4905 4905
 +	p.pod.Spec.Volumes = append(p.pod.Spec.Volumes, volume)
4906
-+	p.pod.Spec.Containers[0].VolumeDevices = append(p.pod.Spec.Containers[0].VolumeDevices, device)
4906
++	p.pod.Spec.Containers[0].VolumeMounts = append(p.pod.Spec.Containers[0].VolumeMounts, volumeMount)
4907 4907
 +	return p
4908 4908
 +}
4909 4909
 +
... ...
@@ -4944,6 +5398,16 @@ index 0000000..596b7d4
4944 4944
 +	return p
4945 4945
 +}
4946 4946
 +
4947
++func (p *testPodBuilder) withToleration(key, value string, operator kapi.TolerationOperator, effect kapi.TaintEffect) *testPodBuilder {
4948
++	p.pod.Spec.Tolerations = append(p.pod.Spec.Tolerations, kapi.Toleration{
4949
++		Key:      key,
4950
++		Value:    value,
4951
++		Operator: operator,
4952
++		Effect:   effect,
4953
++	})
4954
++	return p
4955
++}
4956
++
4947 4957
 +// testUserBuilder
4948 4958
 +type testUserBuilder struct {
4949 4959
 +	user *user.DefaultInfo
... ...
@@ -1,7 +1,7 @@
1
-From ba75f0a3934a48bfc8be1908c7eefdff3e9b9eaa Mon Sep 17 00:00:00 2001
1
+From 4a1a6d90627ddb29b65450deac8b4a7a528a91bf Mon Sep 17 00:00:00 2001
2 2
 From: Bo Gan <ganb@vmware.com>
3 3
 Date: Sun, 10 Jun 2018 02:13:51 -0700
4
-Subject: [PATCH] Cascade Kubernetes patches for v1.10.2 (d06c534)
4
+Subject: [PATCH] Cascade Kubernetes patches for v1.10.2 (da5bd0d)
5 5
 
6 6
 ---
7 7
  api/swagger-spec/apps_v1alpha1.json                |  21 +
... ...
@@ -21,18 +21,18 @@ Subject: [PATCH] Cascade Kubernetes patches for v1.10.2 (d06c534)
21 21
  pkg/apis/core/validation/validation.go             |  29 +-
22 22
  pkg/apis/extensions/types.go                       |   1 +
23 23
  pkg/cloudprovider/providers/BUILD                  |   2 +
24
- pkg/cloudprovider/providers/cascade/BUILD          |  56 +++
24
+ pkg/cloudprovider/providers/cascade/BUILD          |  56 ++
25 25
  pkg/cloudprovider/providers/cascade/OWNERS         |   3 +
26
- pkg/cloudprovider/providers/cascade/apitypes.go    | 227 +++++++++
27
- pkg/cloudprovider/providers/cascade/auth.go        | 145 ++++++
28
- pkg/cloudprovider/providers/cascade/cascade.go     | 214 ++++++++
29
- .../providers/cascade/cascade_disks.go             | 227 +++++++++
30
- .../providers/cascade/cascade_instances.go         |  92 ++++
26
+ pkg/cloudprovider/providers/cascade/apitypes.go    | 227 ++++++
27
+ pkg/cloudprovider/providers/cascade/auth.go        | 145 ++++
28
+ pkg/cloudprovider/providers/cascade/cascade.go     | 214 ++++++
29
+ .../providers/cascade/cascade_disks.go             | 227 ++++++
30
+ .../providers/cascade/cascade_instances.go         |  92 +++
31 31
  .../providers/cascade/cascade_instances_test.go    |  44 ++
32
- .../providers/cascade/cascade_loadbalancer.go      | 285 +++++++++++
33
- pkg/cloudprovider/providers/cascade/client.go      | 394 +++++++++++++++
34
- pkg/cloudprovider/providers/cascade/oidcclient.go  | 297 ++++++++++++
35
- pkg/cloudprovider/providers/cascade/restclient.go  | 262 ++++++++++
32
+ .../providers/cascade/cascade_loadbalancer.go      | 285 ++++++++
33
+ pkg/cloudprovider/providers/cascade/client.go      | 394 ++++++++++
34
+ pkg/cloudprovider/providers/cascade/oidcclient.go  | 297 ++++++++
35
+ pkg/cloudprovider/providers/cascade/restclient.go  | 262 +++++++
36 36
  pkg/cloudprovider/providers/cascade/tests_owed     |   5 +
37 37
  pkg/cloudprovider/providers/cascade/utils.go       |  25 +
38 38
  pkg/cloudprovider/providers/providers.go           |   1 +
... ...
@@ -41,16 +41,16 @@ Subject: [PATCH] Cascade Kubernetes patches for v1.10.2 (d06c534)
41 41
  pkg/security/podsecuritypolicy/util/util.go        |   3 +
42 42
  pkg/volume/cascade_disk/BUILD                      |  43 ++
43 43
  pkg/volume/cascade_disk/OWNERS                     |   2 +
44
- pkg/volume/cascade_disk/attacher.go                | 268 ++++++++++
45
- pkg/volume/cascade_disk/cascade_disk.go            | 390 +++++++++++++++
46
- pkg/volume/cascade_disk/cascade_util.go            | 107 ++++
47
- .../admission/persistentvolume/label/admission.go  |  54 +++
48
- plugin/pkg/admission/vke/BUILD                     |  58 +++
49
- plugin/pkg/admission/vke/admission.go              | 349 +++++++++++++
50
- plugin/pkg/admission/vke/admission_test.go         | 538 +++++++++++++++++++++
51
- staging/src/k8s.io/api/core/v1/generated.pb.go     | 310 +++++++++++-
44
+ pkg/volume/cascade_disk/attacher.go                | 264 +++++++
45
+ pkg/volume/cascade_disk/cascade_disk.go            | 390 ++++++++++
46
+ pkg/volume/cascade_disk/cascade_util.go            | 152 ++++
47
+ .../admission/persistentvolume/label/admission.go  |  54 ++
48
+ plugin/pkg/admission/vke/BUILD                     |  60 ++
49
+ plugin/pkg/admission/vke/admission.go              | 499 +++++++++++++
50
+ plugin/pkg/admission/vke/admission_test.go         | 809 +++++++++++++++++++++
51
+ staging/src/k8s.io/api/core/v1/generated.pb.go     | 310 +++++++-
52 52
  staging/src/k8s.io/api/core/v1/types.go            |  24 +-
53
- 46 files changed, 4655 insertions(+), 29 deletions(-)
53
+ 46 files changed, 5119 insertions(+), 29 deletions(-)
54 54
  create mode 100644 pkg/cloudprovider/providers/cascade/BUILD
55 55
  create mode 100644 pkg/cloudprovider/providers/cascade/OWNERS
56 56
  create mode 100644 pkg/cloudprovider/providers/cascade/apitypes.go
... ...
@@ -3173,10 +3173,10 @@ index 0000000..c3a4ed7
3173 3173
 +- ashokc
3174 3174
 diff --git a/pkg/volume/cascade_disk/attacher.go b/pkg/volume/cascade_disk/attacher.go
3175 3175
 new file mode 100644
3176
-index 0000000..80d8d3a
3176
+index 0000000..c19c37c
3177 3177
 --- /dev/null
3178 3178
 +++ b/pkg/volume/cascade_disk/attacher.go
3179
-@@ -0,0 +1,268 @@
3179
+@@ -0,0 +1,264 @@
3180 3180
 +package cascade_disk
3181 3181
 +
3182 3182
 +import (
... ...
@@ -3192,7 +3192,6 @@ index 0000000..80d8d3a
3192 3192
 +	"k8s.io/kubernetes/pkg/util/mount"
3193 3193
 +	"k8s.io/kubernetes/pkg/volume"
3194 3194
 +	volumeutil "k8s.io/kubernetes/pkg/volume/util"
3195
-+	"strings"
3196 3195
 +)
3197 3196
 +
3198 3197
 +type cascadeDiskAttacher struct {
... ...
@@ -3234,10 +3233,6 @@ index 0000000..80d8d3a
3234 3234
 +		glog.Errorf("Error attaching volume %q to node %q: %+v", volumeSource.DiskID, nodeName, err)
3235 3235
 +		return "", err
3236 3236
 +	}
3237
-+
3238
-+	// Cacsade uses device names of the format /dev/sdX, but newer Linux Kernels mount them under /dev/xvdX
3239
-+	// (source: AWS console). So we have to rename the first occurrence of sd to xvd.
3240
-+	devicePath = strings.Replace(devicePath, "sd", "xvd", 1)
3241 3237
 +	return devicePath, nil
3242 3238
 +}
3243 3239
 +
... ...
@@ -3300,6 +3295,7 @@ index 0000000..80d8d3a
3300 3300
 +		select {
3301 3301
 +		case <-ticker.C:
3302 3302
 +			glog.V(4).Infof("Checking disk %s is attached", volumeSource.DiskID)
3303
++			devicePath := getDiskByIdPath(devicePath)
3303 3304
 +			checkPath, err := verifyDevicePath(devicePath)
3304 3305
 +			if err != nil {
3305 3306
 +				// Log error, if any, and continue checking periodically. See issue #11321
... ...
@@ -3308,7 +3304,7 @@ index 0000000..80d8d3a
3308 3308
 +			} else if checkPath != "" {
3309 3309
 +				// A device path has successfully been created for the disk
3310 3310
 +				glog.V(4).Infof("Successfully found attached disk %s.", volumeSource.DiskID)
3311
-+				return devicePath, nil
3311
++				return checkPath, nil
3312 3312
 +			}
3313 3313
 +		case <-timer.C:
3314 3314
 +			return "", fmt.Errorf("Could not find attached disk %s. Timeout waiting for mount paths to be "+
... ...
@@ -3843,10 +3839,10 @@ index 0000000..3968060
3843 3843
 +}
3844 3844
 diff --git a/pkg/volume/cascade_disk/cascade_util.go b/pkg/volume/cascade_disk/cascade_util.go
3845 3845
 new file mode 100644
3846
-index 0000000..19ddb7f
3846
+index 0000000..e08b7d0
3847 3847
 --- /dev/null
3848 3848
 +++ b/pkg/volume/cascade_disk/cascade_util.go
3849
-@@ -0,0 +1,107 @@
3849
+@@ -0,0 +1,152 @@
3850 3850
 +package cascade_disk
3851 3851
 +
3852 3852
 +import (
... ...
@@ -3860,6 +3856,8 @@ index 0000000..19ddb7f
3860 3860
 +	"k8s.io/kubernetes/pkg/cloudprovider/providers/cascade"
3861 3861
 +	"k8s.io/kubernetes/pkg/volume"
3862 3862
 +	volumeutil "k8s.io/kubernetes/pkg/volume/util"
3863
++	"path/filepath"
3864
++	"os"
3863 3865
 +)
3864 3866
 +
3865 3867
 +const (
... ...
@@ -3879,6 +3877,18 @@ index 0000000..19ddb7f
3879 3879
 +	return "", nil
3880 3880
 +}
3881 3881
 +
3882
++// Returns path for given VKE disk mount
3883
++func getDiskByIdPath(devicePath string) string {
3884
++	nvmePath, err := findNvmeVolume(devicePath)
3885
++	if err != nil {
3886
++		glog.Warningf("error looking for nvme volume %q: %v", devicePath, err)
3887
++	} else if nvmePath != "" {
3888
++		devicePath = nvmePath
3889
++	}
3890
++
3891
++	return devicePath
3892
++}
3893
++
3882 3894
 +// CreateVolume creates a Cascade persistent disk.
3883 3895
 +func (util *CascadeDiskUtil) CreateVolume(p *cascadeDiskProvisioner) (diskID string, capacityGB int, fstype string,
3884 3896
 +	err error) {
... ...
@@ -3954,6 +3964,37 @@ index 0000000..19ddb7f
3954 3954
 +	}
3955 3955
 +	return cc, nil
3956 3956
 +}
3957
++
3958
++// findNvmeVolume looks for the nvme volume with the specified name
3959
++// It follows the symlink (if it exists) and returns the absolute path to the device
3960
++func findNvmeVolume(findName string) (device string, err error) {
3961
++	stat, err := os.Lstat(findName)
3962
++	if err != nil {
3963
++		if os.IsNotExist(err) {
3964
++			glog.V(6).Infof("nvme path not found %q", findName)
3965
++			return "", nil
3966
++		}
3967
++		return "", fmt.Errorf("error getting stat of %q: %v", findName, err)
3968
++	}
3969
++
3970
++	if stat.Mode()&os.ModeSymlink != os.ModeSymlink {
3971
++		glog.Warningf("nvme file %q found, but was not a symlink", findName)
3972
++		return "", nil
3973
++	}
3974
++
3975
++	// Find the target, resolving to an absolute path
3976
++	// For example, /dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_vol0fab1d5e3f72a5e23 -> ../../nvme2n1
3977
++	resolved, err := filepath.EvalSymlinks(findName)
3978
++	if err != nil {
3979
++		return "", fmt.Errorf("error reading target of symlink %q: %v", findName, err)
3980
++	}
3981
++
3982
++	if !strings.HasPrefix(resolved, "/dev") {
3983
++		return "", fmt.Errorf("resolved symlink for %q was unexpected: %q", findName, resolved)
3984
++	}
3985
++
3986
++	return resolved, nil
3987
++}
3957 3988
 diff --git a/plugin/pkg/admission/persistentvolume/label/admission.go b/plugin/pkg/admission/persistentvolume/label/admission.go
3958 3989
 index 819adae..3d55589 100644
3959 3990
 --- a/plugin/pkg/admission/persistentvolume/label/admission.go
... ...
@@ -4039,10 +4080,10 @@ index 819adae..3d55589 100644
4039 4039
 +}
4040 4040
 diff --git a/plugin/pkg/admission/vke/BUILD b/plugin/pkg/admission/vke/BUILD
4041 4041
 new file mode 100644
4042
-index 0000000..b0a6026
4042
+index 0000000..2fb36c7
4043 4043
 --- /dev/null
4044 4044
 +++ b/plugin/pkg/admission/vke/BUILD
4045
-@@ -0,0 +1,58 @@
4045
+@@ -0,0 +1,60 @@
4046 4046
 +package(default_visibility = ["//visibility:public"])
4047 4047
 +
4048 4048
 +load(
... ...
@@ -4057,10 +4098,12 @@ index 0000000..b0a6026
4057 4057
 +    deps = [
4058 4058
 +        "//pkg/apis/core:go_default_library",
4059 4059
 +        "//pkg/apis/extensions:go_default_library",
4060
++        "//pkg/apis/policy/v1beta1:go_default_library",
4060 4061
 +        "//pkg/apis/rbac:go_default_library",
4061 4062
 +        "//pkg/registry/rbac:go_default_library",
4062 4063
 +        "//pkg/security/podsecuritypolicy:go_default_library",
4063 4064
 +        "//vendor/github.com/golang/glog:go_default_library",
4065
++        "//vendor/k8s.io/api/policy/v1beta1:go_default_library"
4064 4066
 +        "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
4065 4067
 +        "//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
4066 4068
 +        "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
... ...
@@ -4104,18 +4147,20 @@ index 0000000..b0a6026
4104 4104
 \ No newline at end of file
4105 4105
 diff --git a/plugin/pkg/admission/vke/admission.go b/plugin/pkg/admission/vke/admission.go
4106 4106
 new file mode 100644
4107
-index 0000000..e33d4e9
4107
+index 0000000..64f6c36
4108 4108
 --- /dev/null
4109 4109
 +++ b/plugin/pkg/admission/vke/admission.go
4110
-@@ -0,0 +1,349 @@
4110
+@@ -0,0 +1,499 @@
4111 4111
 +package vke
4112 4112
 +
4113 4113
 +import (
4114 4114
 +	"fmt"
4115 4115
 +	"io"
4116
++	"os"
4116 4117
 +	"strings"
4117 4118
 +
4118 4119
 +	"github.com/golang/glog"
4120
++	"k8s.io/api/policy/v1beta1"
4119 4121
 +	apiequality "k8s.io/apimachinery/pkg/api/equality"
4120 4122
 +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4121 4123
 +	"k8s.io/apimachinery/pkg/util/validation/field"
... ...
@@ -4123,6 +4168,7 @@ index 0000000..e33d4e9
4123 4123
 +	"k8s.io/apiserver/pkg/admission"
4124 4124
 +	api "k8s.io/kubernetes/pkg/apis/core"
4125 4125
 +	"k8s.io/kubernetes/pkg/apis/extensions"
4126
++	policybeta "k8s.io/kubernetes/pkg/apis/policy/v1beta1"
4126 4127
 +	"k8s.io/kubernetes/pkg/apis/rbac"
4127 4128
 +	rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
4128 4129
 +	"k8s.io/kubernetes/pkg/security/podsecuritypolicy"
... ...
@@ -4138,6 +4184,10 @@ index 0000000..e33d4e9
4138 4138
 +	reservedPrefix           = "vke"
4139 4139
 +	kubeletGroup             = "system:nodes"
4140 4140
 +	kubeProxyGroup           = "vke:kube-proxies"
4141
++	reservedTolerationKey    = "Dedicated"
4142
++	reservedTolerationValue  = "Master"
4143
++	masterNodePrefix         = "master"
4144
++	etcSslCerts              = "/etc/ssl/certs"
4141 4145
 +)
4142 4146
 +
4143 4147
 +// Register registers a plugin.
... ...
@@ -4157,7 +4207,8 @@ index 0000000..e33d4e9
4157 4157
 +
4158 4158
 +// vmwareAdmissionControllerConfig holds config data for VMwareAdmissionController.
4159 4159
 +type vmwareAdmissionControllerConfig struct {
4160
-+	PrivilegedGroup string `yaml:"privilegedGroup"`
4160
++	PrivilegedGroup       string `yaml:"privilegedGroup"`
4161
++	PodSecurityPolicyFile string `yaml:"podSecurityPolicyFile"`
4161 4162
 +}
4162 4163
 +
4163 4164
 +// AdmissionConfig holds config data for admission controllers.
... ...
@@ -4186,7 +4237,7 @@ index 0000000..e33d4e9
4186 4186
 +	case api.Resource("pods"):
4187 4187
 +		err = validatePods(vac, a)
4188 4188
 +	case api.Resource("nodes"):
4189
-+		err = admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot modify nodes", PluginName))
4189
++		err = validateNodes(a)
4190 4190
 +	case rbac.Resource("clusterroles"):
4191 4191
 +		err = validateClusterRoles(a)
4192 4192
 +	case rbac.Resource("clusterrolebindings"):
... ...
@@ -4217,8 +4268,14 @@ index 0000000..e33d4e9
4217 4217
 +		return nil, err
4218 4218
 +	}
4219 4219
 +
4220
++	// Load PSP from file. If it fails, use default.
4221
++	psp := getPSPFromFile(config.VMwareAdmissionController.PodSecurityPolicyFile)
4222
++	if psp == nil {
4223
++		psp = getDefaultPSP()
4224
++	}
4225
++
4220 4226
 +	return &vmwareAdmissionController{
4221
-+		psp:             getDefaultPSP(),
4227
++		psp:             psp,
4222 4228
 +		strategyFactory: podsecuritypolicy.NewSimpleStrategyFactory(),
4223 4229
 +		privilegedGroup: config.VMwareAdmissionController.PrivilegedGroup,
4224 4230
 +	}, nil
... ...
@@ -4246,6 +4303,14 @@ index 0000000..e33d4e9
4246 4246
 +				"configMap",
4247 4247
 +				"persistentVolumeClaim",
4248 4248
 +				"projected",
4249
++				"hostPath",
4250
++			},
4251
++			// We allow /etc/ssl/certs to be mounted in read only mode as a hack to allow Wavefront pods to be deployed.
4252
++			// TODO(ashokc): Once we have support for users to create pods using privileged mode and host path, remove this.
4253
++			AllowedHostPaths: []extensions.AllowedHostPath{
4254
++				{
4255
++					etcSslCerts,
4256
++				},
4249 4257
 +			},
4250 4258
 +			FSGroup: extensions.FSGroupStrategyOptions{
4251 4259
 +				Rule: extensions.FSGroupStrategyRunAsAny,
... ...
@@ -4263,6 +4328,39 @@ index 0000000..e33d4e9
4263 4263
 +	}
4264 4264
 +}
4265 4265
 +
4266
++func getPSPFromFile(pspFile string) *extensions.PodSecurityPolicy {
4267
++	pspBeta := v1beta1.PodSecurityPolicy{}
4268
++	pspExtensions := extensions.PodSecurityPolicy{}
4269
++
4270
++	if pspFile == "" {
4271
++		glog.V(2).Infof("%s: PSP file not specified, using default PSP", PluginName)
4272
++		return nil
4273
++	}
4274
++
4275
++	pspConfig, err := os.Open(pspFile)
4276
++	if err != nil {
4277
++		glog.V(2).Infof("%s: cannot open PSP file, using default PSP: %v", PluginName, err)
4278
++		return nil
4279
++	}
4280
++
4281
++	// We load the PSP that we read from file into pspBeta because this is the struct to which we can decode yaml to.
4282
++	d := yaml.NewYAMLOrJSONDecoder(pspConfig, 4096)
4283
++	err = d.Decode(&pspBeta)
4284
++	if err != nil {
4285
++		glog.V(2).Infof("%s: cannot decode PSP file, using default PSP: %v", PluginName, err)
4286
++		return nil
4287
++	}
4288
++
4289
++	// We convert pspBeta object into pspExtensions object because this is the one that pod validation uses.
4290
++	err = policybeta.Convert_v1beta1_PodSecurityPolicy_To_extensions_PodSecurityPolicy(&pspBeta, &pspExtensions, nil)
4291
++	if err != nil {
4292
++		glog.V(2).Infof("%s: cannot convert v1beta1.PSP to extensions.PSP, using default PSP: %v", PluginName, err)
4293
++		return nil
4294
++	}
4295
++
4296
++	return &pspExtensions
4297
++}
4298
++
4266 4299
 +func isPrivilegedUser(vac *vmwareAdmissionController, a admission.Attributes) bool {
4267 4300
 +	// We need to allow the service accounts inside the privileged namespace to be able to access pods and nodes.
4268 4301
 +	// Node-monitor agent, Photon OS update controller and Cluster autoscaler depend on this.
... ...
@@ -4330,6 +4428,22 @@ index 0000000..e33d4e9
4330 4330
 +	return false
4331 4331
 +}
4332 4332
 +
4333
++func validateNodes(a admission.Attributes) error {
4334
++	// If the operation is Delete, fail. Deleting a node is not something that is useful to the user. Also, by deleting
4335
++	// a node, they can potentially make their cluster useless.
4336
++	if a.GetOperation() == admission.Delete {
4337
++		return admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot delete nodes", PluginName))
4338
++	}
4339
++
4340
++	// If the operation is on a master node, fail. We do not want to allow the users to modify labels and taints on the
4341
++	// master node because it can compromise the security of the cluster.
4342
++	if strings.HasPrefix(a.GetName(), masterNodePrefix) {
4343
++		return admission.NewForbidden(a, fmt.Errorf("%s validation failed: cannot modify master nodes", PluginName))
4344
++	}
4345
++
4346
++	return nil
4347
++}
4348
++
4333 4349
 +func validateClusterRoles(a admission.Attributes) error {
4334 4350
 +	// If the name in the request is not empty and has the reserved prefix, then fail. We will hit this during delete
4335 4351
 +	// and update operations on the cluster roles. If it does not have the reserved prefix, allow it. If the name is
... ...
@@ -4418,6 +4532,12 @@ index 0000000..e33d4e9
4418 4418
 +	// Validate the pod.
4419 4419
 +	errs = append(errs, provider.ValidatePod(pod, field.NewPath("spec", "securityContext"))...)
4420 4420
 +
4421
++	// Validate the pod's tolerations.
4422
++	fieldErr := validatePodToleration(pod)
4423
++	if fieldErr != nil {
4424
++		errs = append(errs, fieldErr)
4425
++	}
4426
++
4421 4427
 +	// Validate the initContainers that are part of the pod.
4422 4428
 +	for i := range pod.Spec.InitContainers {
4423 4429
 +		err := provider.DefaultContainerSecurityContext(pod, &pod.Spec.InitContainers[i])
... ...
@@ -4442,6 +4562,12 @@ index 0000000..e33d4e9
4442 4442
 +			field.NewPath("spec", "containers").Index(i).Child("securityContext"))...)
4443 4443
 +	}
4444 4444
 +
4445
++	// Validate that /etc/ssl/certs if mounted using hostPath volume mount is readOnly.
4446
++	fieldErr = validateEtcSslCertsHostPath(pod)
4447
++	if fieldErr != nil {
4448
++		errs = append(errs, fieldErr)
4449
++	}
4450
++
4445 4451
 +	if len(errs) > 0 {
4446 4452
 +		return admission.NewForbidden(a,
4447 4453
 +			fmt.Errorf("%s validation failed: %v", PluginName, errs))
... ...
@@ -4450,6 +4576,73 @@ index 0000000..e33d4e9
4450 4450
 +	return nil
4451 4451
 +}
4452 4452
 +
4453
++func validatePodToleration(pod *api.Pod) *field.Error {
4454
++	// Master nodes are tainted with "Dedicated=Master:NoSchedule". Only vke-system pods are allowed to tolerate
4455
++	// this taint and to run on master nodes. A user's pod will be rejected if its spec has toleration for this taint.
4456
++	for _, t := range pod.Spec.Tolerations {
4457
++		reject := false
4458
++
4459
++		if t.Key == reservedTolerationKey && t.Value == reservedTolerationValue {
4460
++			// Reject pod that has the reserved toleration "Dedicated=Master"
4461
++			reject = true
4462
++		} else if t.Operator == api.TolerationOpExists && (t.Key == reservedTolerationKey || t.Key == "") {
4463
++			// Reject pod that has wildcard toleration matching the reserved toleration
4464
++			reject = true
4465
++		}
4466
++
4467
++		if reject {
4468
++			return field.Invalid(field.NewPath("spec", "toleration"), fmt.Sprintf("%+v", t),
4469
++				fmt.Sprintf("%s validation failed: should not tolerate master node taint", PluginName))
4470
++		}
4471
++	}
4472
++	return nil
4473
++}
4474
++
4475
++// Validate that /etc/ssl/certs if mounted using hostPath volume mount is readOnly. If not, fail.
4476
++// This is a hack that is needed to get Wavefront pods to work.
4477
++// TODO(ashokc): Once we have support for users to create pods using privileged mode and host path, remove this.
4478
++func validateEtcSslCertsHostPath(pod *api.Pod) *field.Error {
4479
++	// Get volumes which mount /etc/ssl/certs and put them in a map.
4480
++	volumes := map[string]struct{}{}
4481
++	for _, vol := range pod.Spec.Volumes {
4482
++		if vol.HostPath != nil && strings.HasPrefix(vol.HostPath.Path, etcSslCerts) {
4483
++			volumes[vol.Name] = struct{}{}
4484
++		}
4485
++	}
4486
++
4487
++	// For every initContainer, get all volumeMounts and verify if it matches any of the volumes in the volumes map.
4488
++	// If yes, then check if they are read only. If not, return an error.
4489
++	err := checkVolumeReadOnly(pod.Spec.InitContainers, volumes, "initContainers")
4490
++	if err != nil {
4491
++		return err
4492
++	}
4493
++
4494
++	// For every container, get all volumeMounts and verify if it matches any of the volumes in the volumes map.
4495
++	// If yes, then check if they are read only. If not, return an error.
4496
++	err = checkVolumeReadOnly(pod.Spec.Containers, volumes, "containers")
4497
++	if err != nil {
4498
++		return err
4499
++	}
4500
++
4501
++	return nil
4502
++}
4503
++
4504
++// Checks if the container has a volumeMount belonging to the volumes map. If yes, it has to be read only. If not,
4505
++// return error.
4506
++func checkVolumeReadOnly(containers []api.Container, volumes map[string]struct{}, containerType string) *field.Error {
4507
++	for i, container := range containers {
4508
++		for _, vol := range container.VolumeMounts {
4509
++			if _, ok := volumes[vol.Name]; ok {
4510
++				if !vol.ReadOnly {
4511
++					return field.Invalid(field.NewPath("spec", containerType).Index(i).Child("volumeMounts"),
4512
++						fmt.Sprintf("%+v", vol), fmt.Sprintf("%s has to be mount as readOnly", etcSslCerts))
4513
++				}
4514
++			}
4515
++		}
4516
++	}
4517
++	return nil
4518
++}
4519
++
4453 4520
 +func checkReservedPrefix(resourceName string, a admission.Attributes) error {
4454 4521
 +	if strings.HasPrefix(resourceName, reservedPrefix) {
4455 4522
 +		return admission.NewForbidden(a,
... ...
@@ -4459,14 +4652,15 @@ index 0000000..e33d4e9
4459 4459
 +}
4460 4460
 diff --git a/plugin/pkg/admission/vke/admission_test.go b/plugin/pkg/admission/vke/admission_test.go
4461 4461
 new file mode 100644
4462
-index 0000000..596b7d4
4462
+index 0000000..52c88cc
4463 4463
 --- /dev/null
4464 4464
 +++ b/plugin/pkg/admission/vke/admission_test.go
4465
-@@ -0,0 +1,538 @@
4465
+@@ -0,0 +1,809 @@
4466 4466
 +package vke
4467 4467
 +
4468 4468
 +import (
4469 4469
 +	"fmt"
4470
++	"os"
4470 4471
 +	"strings"
4471 4472
 +	"testing"
4472 4473
 +
... ...
@@ -4482,6 +4676,38 @@ index 0000000..596b7d4
4482 4482
 +	defaultConfigFileFormat  = `
4483 4483
 +vmwareAdmissionController:
4484 4484
 +  privilegedGroup: %s
4485
++  podSecurityPolicyFile: %s
4486
++`
4487
++	pspFileName   = "/tmp/psp.yaml"
4488
++	pspConfigFile = `
4489
++apiVersion: extensions/v1beta1
4490
++kind: PodSecurityPolicy
4491
++metadata:
4492
++  name: vmware-pod-security-policy-restricted
4493
++spec:
4494
++  privileged: true
4495
++  fsGroup:
4496
++    rule: RunAsAny
4497
++  runAsUser:
4498
++    rule: RunAsAny
4499
++  seLinux:
4500
++    rule: RunAsAny
4501
++  supplementalGroups:
4502
++    rule: RunAsAny
4503
++  volumes:
4504
++  - 'emptyDir'
4505
++  - 'secret'
4506
++  - 'downwardAPI'
4507
++  - 'configMap'
4508
++  - 'persistentVolumeClaim'
4509
++  - 'projected'
4510
++  - 'hostPath'
4511
++  hostPID: false
4512
++  hostIPC: false
4513
++  hostNetwork: true
4514
++  hostPorts:
4515
++  - min: 1
4516
++    max: 65536
4485 4517
 +`
4486 4518
 +)
4487 4519
 +
... ...
@@ -4549,7 +4775,19 @@ index 0000000..596b7d4
4549 4549
 +		},
4550 4550
 +		"create pod with HostVolume denied": {
4551 4551
 +			operation:          kadmission.Create,
4552
-+			pod:                newTestPodBuilder().withHostVolume().build(),
4552
++			pod:                newTestPodBuilder().withHostVolume("/", false).build(),
4553
++			userInfo:           newTestUserBuilder().build(),
4554
++			shouldPassValidate: false,
4555
++		},
4556
++		"create pod with HostVolume /etc/ssl/certs in read-only mode allowed": {
4557
++			operation:          kadmission.Create,
4558
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", true).build(),
4559
++			userInfo:           newTestUserBuilder().build(),
4560
++			shouldPassValidate: true,
4561
++		},
4562
++		"create pod with HostVolume /etc/ssl/certs in read-write mode denied": {
4563
++			operation:          kadmission.Create,
4564
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", false).build(),
4553 4565
 +			userInfo:           newTestUserBuilder().build(),
4554 4566
 +			shouldPassValidate: false,
4555 4567
 +		},
... ...
@@ -4561,7 +4799,7 @@ index 0000000..596b7d4
4561 4561
 +		},
4562 4562
 +		"create pod with HostVolume and CascadeDisk denied": {
4563 4563
 +			operation:          kadmission.Create,
4564
-+			pod:                newTestPodBuilder().withHostVolume().withCascadeDisk().build(),
4564
++			pod:                newTestPodBuilder().withHostVolume("/", false).withCascadeDisk().build(),
4565 4565
 +			userInfo:           newTestUserBuilder().build(),
4566 4566
 +			shouldPassValidate: false,
4567 4567
 +		},
... ...
@@ -4573,15 +4811,144 @@ index 0000000..596b7d4
4573 4573
 +		},
4574 4574
 +		"delete pod allowed": {
4575 4575
 +			operation:          kadmission.Delete,
4576
++			pod:                nil,
4577
++			userInfo:           newTestUserBuilder().build(),
4578
++			shouldPassValidate: true,
4579
++		},
4580
++	}
4581
++
4582
++	for k, v := range tests {
4583
++		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4584
++	}
4585
++}
4586
++
4587
++func TestAdmitPrivilegedWithCustomPSP(t *testing.T) {
4588
++	tests := map[string]struct {
4589
++		operation          kadmission.Operation
4590
++		pod                *kapi.Pod
4591
++		name               string
4592
++		userInfo           user.Info
4593
++		shouldPassValidate bool
4594
++	}{
4595
++		"create pod with Privileged=nil allowed": {
4596
++			operation:          kadmission.Create,
4597
++			pod:                newTestPodBuilder().build(),
4598
++			userInfo:           newTestUserBuilder().build(),
4599
++			shouldPassValidate: true,
4600
++		},
4601
++		"create pod with Privileged=false allowed": {
4602
++			operation:          kadmission.Create,
4603
++			pod:                newTestPodBuilder().withPrivileged(false).build(),
4604
++			userInfo:           newTestUserBuilder().build(),
4605
++			shouldPassValidate: true,
4606
++		},
4607
++		"create pod with Privileged=true allowed": {
4608
++			operation:          kadmission.Create,
4609
++			pod:                newTestPodBuilder().withPrivileged(true).build(),
4610
++			userInfo:           newTestUserBuilder().build(),
4611
++			shouldPassValidate: true,
4612
++		},
4613
++		"create pod with multiple containers, one has Privileged=true allowed": {
4614
++			operation:          kadmission.Create,
4615
++			pod:                newTestPodBuilder().withPrivileged(true).withInitContainer().withContainer().build(),
4616
++			userInfo:           newTestUserBuilder().build(),
4617
++			shouldPassValidate: true,
4618
++		},
4619
++		"update pod with Privileged=true allowed": {
4620
++			operation:          kadmission.Update,
4621
++			pod:                newTestPodBuilder().withPrivileged(true).build(),
4622
++			userInfo:           newTestUserBuilder().build(),
4623
++			shouldPassValidate: true,
4624
++		},
4625
++		"create pod with HostNetwork=true allowed": {
4626
++			operation:          kadmission.Create,
4627
++			pod:                newTestPodBuilder().withHostNetwork(true).build(),
4628
++			userInfo:           newTestUserBuilder().build(),
4629
++			shouldPassValidate: true,
4630
++		},
4631
++		"create pod with HostIPC=true denied": {
4632
++			operation:          kadmission.Create,
4633
++			pod:                newTestPodBuilder().withHostIPC(true).build(),
4634
++			userInfo:           newTestUserBuilder().build(),
4635
++			shouldPassValidate: false,
4636
++		},
4637
++		"create pod with HostPID=true denied": {
4638
++			operation:          kadmission.Create,
4639
++			pod:                newTestPodBuilder().withHostPID(true).build(),
4640
++			userInfo:           newTestUserBuilder().build(),
4641
++			shouldPassValidate: false,
4642
++		},
4643
++		"create pod with HostPort allowed": {
4644
++			operation:          kadmission.Create,
4645
++			pod:                newTestPodBuilder().withHostPort().build(),
4646
++			userInfo:           newTestUserBuilder().build(),
4647
++			shouldPassValidate: true,
4648
++		},
4649
++		"create pod with HostVolume allowed": {
4650
++			operation:          kadmission.Create,
4651
++			pod:                newTestPodBuilder().withHostVolume("/", false).build(),
4652
++			userInfo:           newTestUserBuilder().build(),
4653
++			shouldPassValidate: true,
4654
++		},
4655
++		"create pod with HostVolume /etc/ssl/certs in read-only mode allowed": {
4656
++			operation:          kadmission.Create,
4657
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", true).build(),
4658
++			userInfo:           newTestUserBuilder().build(),
4659
++			shouldPassValidate: true,
4660
++		},
4661
++		"create pod with HostVolume /etc/ssl/certs in read-write mode denied": {
4662
++			operation:          kadmission.Create,
4663
++			pod:                newTestPodBuilder().withHostVolume("/etc/ssl/certs", false).build(),
4664
++			userInfo:           newTestUserBuilder().build(),
4665
++			shouldPassValidate: false,
4666
++		},
4667
++		"create pod with CascadeDisk allowed": {
4668
++			operation:          kadmission.Create,
4669
++			pod:                newTestPodBuilder().withCascadeDisk().build(),
4670
++			userInfo:           newTestUserBuilder().build(),
4671
++			shouldPassValidate: true,
4672
++		},
4673
++		"create pod with HostVolume and CascadeDisk allowed": {
4674
++			operation:          kadmission.Create,
4675
++			pod:                newTestPodBuilder().withHostVolume("/", false).withCascadeDisk().build(),
4676
++			userInfo:           newTestUserBuilder().build(),
4677
++			shouldPassValidate: true,
4678
++		},
4679
++		"connect pod allowed": {
4680
++			operation:          kadmission.Connect,
4576 4681
 +			pod:                newTestPodBuilder().build(),
4577 4682
 +			userInfo:           newTestUserBuilder().build(),
4578 4683
 +			shouldPassValidate: true,
4579 4684
 +		},
4685
++		"delete pod allowed": {
4686
++			operation:          kadmission.Delete,
4687
++			pod:                nil,
4688
++			userInfo:           newTestUserBuilder().build(),
4689
++			shouldPassValidate: true,
4690
++		},
4691
++	}
4692
++
4693
++	// Setup custom PSP file.
4694
++	file, err := os.Create(pspFileName)
4695
++	if err != nil {
4696
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to open custom PSP file %v", err)
4697
++		return
4698
++	}
4699
++	_, err = file.WriteString(pspConfigFile)
4700
++	if err != nil {
4701
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to write to custom PSP file %v", err)
4702
++		return
4580 4703
 +	}
4581 4704
 +
4582 4705
 +	for k, v := range tests {
4583 4706
 +		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4584 4707
 +	}
4708
++
4709
++	// Delete custom PSP file.
4710
++	err = os.Remove(pspFileName)
4711
++	if err != nil {
4712
++		t.Errorf("TestAdmitPrivilegedWithCustomPSP: failed to delete custom PSP file %v", err)
4713
++	}
4585 4714
 +}
4586 4715
 +
4587 4716
 +func TestPrivilegedNamespace(t *testing.T) {
... ...
@@ -4664,6 +5031,63 @@ index 0000000..596b7d4
4664 4664
 +	}
4665 4665
 +}
4666 4666
 +
4667
++func TestToleration(t *testing.T) {
4668
++	tests := map[string]struct {
4669
++		operation          kadmission.Operation
4670
++		pod                *kapi.Pod
4671
++		name               string
4672
++		userInfo           user.Info
4673
++		shouldPassValidate bool
4674
++	}{
4675
++		"allowed: create pod with no toleration": {
4676
++			operation:          kadmission.Create,
4677
++			pod:                newTestPodBuilder().build(),
4678
++			userInfo:           newTestUserBuilder().build(),
4679
++			shouldPassValidate: true,
4680
++		},
4681
++		"allowed: create pod with normal toleration key": {
4682
++			operation:          kadmission.Create,
4683
++			pod:                newTestPodBuilder().withToleration("mykey", reservedTolerationValue, kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4684
++			userInfo:           newTestUserBuilder().build(),
4685
++			shouldPassValidate: true,
4686
++		},
4687
++		"allowed: create pod with normal toleration value": {
4688
++			operation:          kadmission.Create,
4689
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, "myval", kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4690
++			userInfo:           newTestUserBuilder().build(),
4691
++			shouldPassValidate: true,
4692
++		},
4693
++		"denied: create pod with reserved toleration": {
4694
++			operation:          kadmission.Create,
4695
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, reservedTolerationValue, kapi.TolerationOpEqual, kapi.TaintEffectNoSchedule).build(),
4696
++			userInfo:           newTestUserBuilder().build(),
4697
++			shouldPassValidate: false,
4698
++		},
4699
++		"denied: create pod with wildcard toleration": {
4700
++			operation:          kadmission.Create,
4701
++			pod:                newTestPodBuilder().withToleration("", "", kapi.TolerationOpExists, "").build(),
4702
++			userInfo:           newTestUserBuilder().build(),
4703
++			shouldPassValidate: false,
4704
++		},
4705
++		"denied: create pod with value wildcard toleration": {
4706
++			operation:          kadmission.Create,
4707
++			pod:                newTestPodBuilder().withToleration(reservedTolerationKey, "", kapi.TolerationOpExists, kapi.TaintEffectNoSchedule).build(),
4708
++			userInfo:           newTestUserBuilder().build(),
4709
++			shouldPassValidate: false,
4710
++		},
4711
++		"allowed: create pod with value wildcard and normal key": {
4712
++			operation:          kadmission.Create,
4713
++			pod:                newTestPodBuilder().withToleration("mykey", "", kapi.TolerationOpExists, kapi.TaintEffectNoSchedule).build(),
4714
++			userInfo:           newTestUserBuilder().build(),
4715
++			shouldPassValidate: true,
4716
++		},
4717
++	}
4718
++
4719
++	for k, v := range tests {
4720
++		testPodValidation(k, v.operation, v.pod, v.name, v.userInfo, v.shouldPassValidate, t)
4721
++	}
4722
++}
4723
++
4667 4724
 +func TestClusterLevelResources(t *testing.T) {
4668 4725
 +	tests := map[string]struct {
4669 4726
 +		operation          kadmission.Operation
... ...
@@ -4772,9 +5196,34 @@ index 0000000..596b7d4
4772 4772
 +			userInfo:           newTestUserBuilder().withName(systemUnsecuredUser).build(),
4773 4773
 +			shouldPassValidate: true,
4774 4774
 +		},
4775
-+		"denied: regular lightwave user update nodes": {
4775
++		"allowed: regular lightwave user update worker nodes": {
4776 4776
 +			operation:          kadmission.Update,
4777 4777
 +			resource:           "nodes",
4778
++			name:               "worker-guid",
4779
++			namespace:          "",
4780
++			userInfo:           newTestUserBuilder().build(),
4781
++			shouldPassValidate: true,
4782
++		},
4783
++		"denied: regular lightwave user update master nodes": {
4784
++			operation:          kadmission.Update,
4785
++			resource:           "nodes",
4786
++			name:               "master-guid",
4787
++			namespace:          "",
4788
++			userInfo:           newTestUserBuilder().build(),
4789
++			shouldPassValidate: false,
4790
++		},
4791
++		"denied: regular lightwave user delete master nodes": {
4792
++			operation:          kadmission.Delete,
4793
++			resource:           "nodes",
4794
++			name:               "master-guid",
4795
++			namespace:          "",
4796
++			userInfo:           newTestUserBuilder().build(),
4797
++			shouldPassValidate: false,
4798
++		},
4799
++		"denied: regular lightwave user delete worker nodes": {
4800
++			operation:          kadmission.Delete,
4801
++			resource:           "nodes",
4802
++			name:               "worker-guid",
4778 4803
 +			namespace:          "",
4779 4804
 +			userInfo:           newTestUserBuilder().build(),
4780 4805
 +			shouldPassValidate: false,
... ...
@@ -4795,15 +5244,20 @@ index 0000000..596b7d4
4795 4795
 +func testPodValidation(testCaseName string, op kadmission.Operation, pod *kapi.Pod, name string, userInfo user.Info,
4796 4796
 +	shouldPassValidate bool, t *testing.T) {
4797 4797
 +
4798
-+	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup)
4798
++	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup, pspFileName)
4799 4799
 +	configFile := strings.NewReader(defaultConfigFile)
4800 4800
 +	plugin, err := NewVMwareAdmissionController(configFile)
4801 4801
 +	if err != nil {
4802 4802
 +		t.Errorf("%s: failed to create admission controller %v", testCaseName, err)
4803 4803
 +	}
4804 4804
 +
4805
++	namespace := "default"
4806
++	if pod != nil {
4807
++		namespace = pod.Namespace
4808
++	}
4809
++
4805 4810
 +	attrs := kadmission.NewAttributesRecord(pod, nil, kapi.Kind("Pod").WithVersion("version"),
4806
-+		pod.Namespace, name, kapi.Resource("pods").WithVersion("version"), "", op, userInfo)
4811
++		namespace, name, kapi.Resource("pods").WithVersion("version"), "", op, userInfo)
4807 4812
 +
4808 4813
 +	err = plugin.Validate(attrs)
4809 4814
 +	if shouldPassValidate && err != nil {
... ...
@@ -4816,7 +5270,7 @@ index 0000000..596b7d4
4816 4816
 +func testResourceValidation(testCaseName string, op kadmission.Operation, resource string, name string,
4817 4817
 +	namespace string, userInfo user.Info, shouldPassValidate bool, t *testing.T) {
4818 4818
 +
4819
-+	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup)
4819
++	defaultConfigFile := fmt.Sprintf(defaultConfigFileFormat, testServiceAccountsGroup, pspFileName)
4820 4820
 +	configFile := strings.NewReader(defaultConfigFile)
4821 4821
 +	plugin, err := NewVMwareAdmissionController(configFile)
4822 4822
 +	if err != nil {
... ...
@@ -4916,19 +5370,19 @@ index 0000000..596b7d4
4916 4916
 +	return p
4917 4917
 +}
4918 4918
 +
4919
-+func (p *testPodBuilder) withHostVolume() *testPodBuilder {
4919
++func (p *testPodBuilder) withHostVolume(hostPath string, readOnly bool) *testPodBuilder {
4920 4920
 +	volume := kapi.Volume{
4921 4921
 +		Name: "host",
4922 4922
 +		VolumeSource: kapi.VolumeSource{
4923 4923
 +			HostPath: &kapi.HostPathVolumeSource{
4924
-+				Path: "/",
4924
++				Path: hostPath,
4925 4925
 +			},
4926 4926
 +		},
4927 4927
 +	}
4928
-+	device := kapi.VolumeDevice{Name: "host", DevicePath: "/host"}
4928
++	volumeMount := kapi.VolumeMount{Name: "host", MountPath: "/data", ReadOnly: readOnly}
4929 4929
 +
4930 4930
 +	p.pod.Spec.Volumes = append(p.pod.Spec.Volumes, volume)
4931
-+	p.pod.Spec.Containers[0].VolumeDevices = append(p.pod.Spec.Containers[0].VolumeDevices, device)
4931
++	p.pod.Spec.Containers[0].VolumeMounts = append(p.pod.Spec.Containers[0].VolumeMounts, volumeMount)
4932 4932
 +	return p
4933 4933
 +}
4934 4934
 +
... ...
@@ -4969,6 +5423,16 @@ index 0000000..596b7d4
4969 4969
 +	return p
4970 4970
 +}
4971 4971
 +
4972
++func (p *testPodBuilder) withToleration(key, value string, operator kapi.TolerationOperator, effect kapi.TaintEffect) *testPodBuilder {
4973
++	p.pod.Spec.Tolerations = append(p.pod.Spec.Tolerations, kapi.Toleration{
4974
++		Key:      key,
4975
++		Value:    value,
4976
++		Operator: operator,
4977
++		Effect:   effect,
4978
++	})
4979
++	return p
4980
++}
4981
++
4972 4982
 +// testUserBuilder
4973 4983
 +type testUserBuilder struct {
4974 4984
 +	user *user.DefaultInfo
... ...
@@ -1,7 +1,7 @@
1 1
 Summary:        Kubernetes cluster management
2 2
 Name:           kubernetes
3 3
 Version:        1.10.2
4
-Release:        6%{?dist}
4
+Release:        7%{?dist}
5 5
 License:        ASL 2.0
6 6
 URL:            https://github.com/kubernetes/kubernetes/archive/v%{version}.tar.gz
7 7
 Source0:        kubernetes-%{version}.tar.gz
... ...
@@ -207,6 +207,8 @@ fi
207 207
 /opt/vmware/kubernetes/windows/amd64/kubectl.exe
208 208
 
209 209
 %changelog
210
+*   Tue Jun 19 2018 Bo Gan <ganb@vmware.com> 1.10.2-7
211
+-   Update vke patch (da5bd0d)
210 212
 *   Sat Jun 09 2018 Bo Gan <ganb@vmware.com> 1.10.2-6
211 213
 -   Update vke patch (d06c534)
212 214
 *   Fri Jun 08 2018 Bo Gan <ganb@vmware.com> 1.10.2-5
... ...
@@ -1,7 +1,7 @@
1 1
 Summary:        Kubernetes cluster management
2 2
 Name:           kubernetes
3 3
 Version:        1.9.6
4
-Release:        5%{?dist}
4
+Release:        6%{?dist}
5 5
 License:        ASL 2.0
6 6
 URL:            https://github.com/kubernetes/kubernetes/archive/v%{version}.tar.gz
7 7
 Source0:        kubernetes-v%{version}.tar.gz
... ...
@@ -185,6 +185,8 @@ fi
185 185
 %{_bindir}/pause-amd64
186 186
 
187 187
 %changelog
188
+*   Tue Jun 19 2018 Bo Gan <ganb@vmware.com> 1.9.6-6
189
+-   Update vke patch (da5bd0d)
188 190
 *   Sat Jun 09 2018 Bo Gan <ganb@vmware.com> 1.9.6-5
189 191
 -   Update vke patch (d06c534)
190 192
 *   Fri Jun 08 2018 Bo Gan <ganb@vmware.com> 1.9.6-4