Repository: knative/client (go)

Issue:
Add config option to specify `readOnly` volumes
<!-- If you need to report a security issue with Knative, send an email to knative-security@googlegroups.com. -->

### Feature request

Per discussion at PR #1709.

Extend current `--volume` flag to accept config parameter `readOnly=true` to limit volume accessibility.

### UI Example

<!-- If this is about a new command or command line options, please let us know how you would add it to the "kn" UI (in the code block below). If this is not applicable for your feature, delete this section. -->

```
--volume pvc:myvol:readOnly=true
```

/kind good-first-issue
/kind feature
<!--
Optional Classifications, remove the '> ' to select:
/kind good-first-issue
> /kind proposal
> /kind doc
> /kind cleanup
-->


Pull request description:
Added config support to specify read only volumes
## Description
Added config support to specify read only volumes
<!-- Please add a short summary about what the pull request is going to bring on the table -->

## Changes

<!-- Please add list of more detailed changes. These changes should be reflected also in the commit messages -->

* User can specify if the volume is read only or not using the `--volume` flag
* Example: kn service create svc1 --image imagename/v0.0.1 --mount /mydir=vol1 --volume cm:cm1:readOnly=true


## Reference

<!-- Please add the corresponding issue number which this pull request is about to fix -->
Fixes #1712 

**Release Note**

<!-- Enter your extended release note in the below block. If the PR requires
additional action from users switching to the new release, include the string
"action required". If no release note is required, write "NONE". -->

```release-note

```

<!--
Please add an entry to CHANGELOG.adoc file, too, as part of your Pull Request.

In the following cases, add a short description of PR to the unreleased section in CHANGELOG.adoc:

- 🎁 New feature
- 🐛 Bug fix
- ✨ Feature Update
- 🐣 Refactoring
- 🗑️ Remove feature or internal logic

See other entries in CHANGELOG.adoc as an example for how to add the entry, including a reference to the corresponding issue/PR

PLEASE DON'T ADD THAT LINE HERE IN THE PULL-REQUEST DESCRIPTION BUT DIRECTLY IN CHANGELOG.ADOC AND ADD CHANGELOG.ADOC AS PART OF YOUR PULL-REQUEST.
-->

<!--
To automatically lint go code in this pull request uncomment the line below. You get feedback as comments on your pull request then -->

<!--
/lint
-->


Patch:
diff --git a/OWNERS_ALIASES b/OWNERS_ALIASES
index ea395ff5..ac08d352 100644
--- a/OWNERS_ALIASES
+++ b/OWNERS_ALIASES
@@ -66,9 +66,10 @@ aliases:
   - knative-test-reporter-robot
   - kvmware
   - lance
-  - matzew
-  - n3wscott
+  - lionelvillard
+  - nak3
   - pmorie
+  - psschwei
   - smoser-ibm
   - spencerdillard
   - thisisnotapril
@@ -76,8 +77,8 @@ aliases:
   - vaikas
   - zroubalik
   knative-release-leads:
-  - dprotaso
-  - matzew
+  - lionelvillard
+  - nak3
   knative-robots:
   - knative-automation
   - knative-prow-releaser-robot
@@ -196,7 +197,7 @@ aliases:
   - dprotaso
   - dsimansk
   - evankanderson
-  - n3wscott
+  - psschwei
   - zroubalik
   trademark-committee:
   - evankanderson
diff --git a/docs/cmd/kn_container_add.md b/docs/cmd/kn_container_add.md
index 814e7f98..15c41a2c 100644
--- a/docs/cmd/kn_container_add.md
+++ b/docs/cmd/kn_container_add.md
@@ -38,7 +38,7 @@ kn container add NAME
   -h, --help                          help for add
       --image string                  Image to run.
       --limit strings                 The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
-      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -p, --port string                   The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'.
       --probe-liveness string         Add liveness probe to Service deployment. Supported probe types are HTTGet, Exec and TCPSocket. Format: [http,https]:host:port:path, exec:cmd[,cmd,...], tcp:host:port.
       --probe-liveness-opts string    Add common options to liveness probe. Common opts (comma separated, case insensitive): InitialDelaySeconds=<int_value>, FailureThreshold=<int_value>, SuccessThreshold=<int_value>, PeriodSeconds=<int_value>, TimeoutSeconds=<int_value>
diff --git a/docs/cmd/kn_service_apply.md b/docs/cmd/kn_service_apply.md
index fa816735..c4e1360e 100644
--- a/docs/cmd/kn_service_apply.md
+++ b/docs/cmd/kn_service_apply.md
@@ -50,7 +50,7 @@ kn service apply s0 --filename my-svc.yml
       --label-service stringArray         Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag.
       --limit strings                     The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
       --lock-to-digest                    Keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
-      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -n, --namespace string                  Specify the namespace to operate in.
       --no-cluster-local                  Do not specify that the service be private. (--no-cluster-local will make the service publicly available) (default true)
       --no-lock-to-digest                 Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
diff --git a/docs/cmd/kn_service_create.md b/docs/cmd/kn_service_create.md
index 0221644d..26e77609 100644
--- a/docs/cmd/kn_service_create.md
+++ b/docs/cmd/kn_service_create.md
@@ -75,7 +75,7 @@ kn service create NAME --image IMAGE
       --label-service stringArray         Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag.
       --limit strings                     The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
       --lock-to-digest                    Keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
-      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -n, --namespace string                  Specify the namespace to operate in.
       --no-cluster-local                  Do not specify that the service be private. (--no-cluster-local will make the service publicly available) (default true)
       --no-lock-to-digest                 Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
diff --git a/docs/cmd/kn_service_update.md b/docs/cmd/kn_service_update.md
index 89280731..81b5847f 100644
--- a/docs/cmd/kn_service_update.md
+++ b/docs/cmd/kn_service_update.md
@@ -62,7 +62,7 @@ kn service update NAME
       --label-service stringArray         Service label to set. name=value; you may provide this flag any number of times to set multiple labels. To unset, specify the label name followed by a "-" (e.g., name-). This flag takes precedence over the "label" flag.
       --limit strings                     The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
       --lock-to-digest                    Keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision) (default true)
-      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray                 Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -n, --namespace string                  Specify the namespace to operate in.
       --no-cluster-local                  Do not specify that the service be private. (--no-cluster-local will make the service publicly available) (default true)
       --no-lock-to-digest                 Do not keep the running image for the service constant when not explicitly specifying the image. (--no-lock-to-digest pulls the image tag afresh with each new revision)
diff --git a/docs/cmd/kn_source_container_create.md b/docs/cmd/kn_source_container_create.md
index 89eac8a4..d51b1b12 100644
--- a/docs/cmd/kn_source_container_create.md
+++ b/docs/cmd/kn_source_container_create.md
@@ -27,7 +27,7 @@ kn source container create NAME --image IMAGE --sink SINK
   -h, --help                          help for create
       --image string                  Image to run.
       --limit strings                 The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
-      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -n, --namespace string              Specify the namespace to operate in.
   -p, --port string                   The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'.
       --probe-liveness string         Add liveness probe to Service deployment. Supported probe types are HTTGet, Exec and TCPSocket. Format: [http,https]:host:port:path, exec:cmd[,cmd,...], tcp:host:port.
diff --git a/docs/cmd/kn_source_container_update.md b/docs/cmd/kn_source_container_update.md
index 87113828..81d947d4 100644
--- a/docs/cmd/kn_source_container_update.md
+++ b/docs/cmd/kn_source_container_update.md
@@ -27,7 +27,7 @@ kn source container update NAME --image IMAGE
   -h, --help                          help for update
       --image string                  Image to run.
       --limit strings                 The resource requirement limits for this Service. For example, 'cpu=100m,memory=256Mi'. You can use this flag multiple times. To unset a resource limit, append "-" to the resource name, e.g. '--limit memory-'.
-      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
+      --mount stringArray             Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), an EmptyDir (prefix ed: or emptyDir:), a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. You can mount a volume with readOnly config (true | false) also. Example: --mount /mydir=ed:ed1:readOnly=true. You can specify a volume subpath by following the volume name with slash separated path. Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. You can use this flag multiple times. For unmounting a directory, append "-", e.g. --mount /mydir-, which also removes any auto-generated volume.
   -n, --namespace string              Specify the namespace to operate in.
   -p, --port string                   The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'.
       --probe-liveness string         Add liveness probe to Service deployment. Supported probe types are HTTGet, Exec and TCPSocket. Format: [http,https]:host:port:path, exec:cmd[,cmd,...], tcp:host:port.
diff --git a/pkg/kn/flags/podspec.go b/pkg/kn/flags/podspec.go
index 553e4035..aec09ec4 100644
--- a/pkg/kn/flags/podspec.go
+++ b/pkg/kn/flags/podspec.go
@@ -146,7 +146,9 @@ func (p *PodSpecFlags) AddFlags(flagset *pflag.FlagSet) []string {
 			"a PersistentVolumeClaim (prefix pvc: or persistentVolumeClaim) or an existing Volume (without any prefix) on the specified directory. "+
 			"Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, --mount /mydir=emptyDir:myvol "+
 			"or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is "+
-			"automatically generated. You can specify a volume subpath by following the volume name with slash separated path. "+
+			"automatically generated. You can mount a volume with readOnly config (true | false) also. "+
+			"Example: --mount /mydir=ed:ed1:readOnly=true. "+
+			"You can specify a volume subpath by following the volume name with slash separated path. "+
 			"Example: --mount /mydir=cm:myconfigmap/subpath/to/be/mounted. "+
 			"You can use this flag multiple times. "+
 			"For unmounting a directory, append \"-\", e.g. --mount /mydir-, which also removes any auto-generated volume.")
diff --git a/pkg/kn/flags/podspec_helper.go b/pkg/kn/flags/podspec_helper.go
index b59f35da..97ee7c81 100644
--- a/pkg/kn/flags/podspec_helper.go
+++ b/pkg/kn/flags/podspec_helper.go
@@ -44,8 +44,9 @@ const (
 )
 
 type MountInfo struct {
-	VolumeName string
-	SubPath    string
+	VolumeName   string
+	SubPath      string
+	MountOptions string
 }
 
 func (vt VolumeSourceType) String() string {
@@ -539,6 +540,7 @@ func updateVolume(volume *corev1.Volume, info *volumeSourceInfo) error {
 func updateVolumeMountsFromMap(volumeMounts []corev1.VolumeMount, toUpdate *util.OrderedMap, volumes []corev1.Volume) ([]corev1.VolumeMount, error) {
 	set := make(map[string]bool)
 
+	var err error
 	for i := range volumeMounts {
 		volumeMount := &volumeMounts[i]
 		mountInfo, present := toUpdate.Get(volumeMount.MountPath)
@@ -549,7 +551,10 @@ func updateVolumeMountsFromMap(volumeMounts []corev1.VolumeMount, toUpdate *util
 			if !existsVolumeNameInVolumes(name, volumes) {
 				return nil, fmt.Errorf("There is no volume matched with %q", name)
 			}
-			volumeMount.ReadOnly = isReadOnlyVolume(name, volumes)
+			volumeMount.ReadOnly, err = isReadOnlyVolume(name, volumeMountInfo.MountOptions, volumes)
+			if err != nil {
+				return nil, err
+			}
 			volumeMount.Name = name
 			volumeMount.SubPath = volumeMountInfo.SubPath
 			set[volumeMount.MountPath] = true
@@ -560,10 +565,14 @@ func updateVolumeMountsFromMap(volumeMounts []corev1.VolumeMount, toUpdate *util
 	for mountPath, mountInfo, ok := it.Next(); ok; mountPath, mountInfo, ok = it.Next() {
 		volumeMountInfo := mountInfo.(*MountInfo)
 		name := volumeMountInfo.VolumeName
+		readOnly, err := isReadOnlyVolume(name, volumeMountInfo.MountOptions, volumes)
+		if err != nil {
+			return nil, err
+		}
 		if !set[mountPath] {
 			volumeMounts = append(volumeMounts, corev1.VolumeMount{
 				Name:      name,
-				ReadOnly:  isReadOnlyVolume(name, volumes),
+				ReadOnly:  readOnly,
 				MountPath: mountPath,
 				SubPath:   volumeMountInfo.SubPath,
 			})
@@ -740,12 +749,43 @@ func (vol *volumeSourceInfo) createEnvFromSource() *corev1.EnvFromSource {
 
 // =======================================================================================
 
-func isReadOnlyVolume(volumeName string, volumes []corev1.Volume) bool {
+func isReadOnlyVolume(volumeName string, mountOptions string, volumes []corev1.Volume) (bool, error) {
+	if mountOptions != "" {
+		options, err := parseMountOptions(mountOptions)
+		if err != nil {
+			return false, err
+		}
+		if val, ok := options.Get("readonly"); ok && val != "" {
+			return strconv.ParseBool(val.(string))
+		}
+	}
 	for _, volume := range volumes {
 		if volume.Name == volumeName {
-			return volume.EmptyDir == nil
+			return defaultReadOnly(volume), nil
 		}
 	}
+	return true, nil
+}
+
+func parseMountOptions(options string) (*util.OrderedMap, error) {
+	mountOptions := util.NewOrderedMap()
+	slices := strings.Split(options, ",")
+	for _, slice := range slices {
+		pair := strings.SplitN(slice, "=", 2)
+		switch strings.ToLower(pair[0]) {
+		case "readonly":
+			mountOptions.Set("readonly", pair[1])
+		default:
+			return nil, fmt.Errorf("unknown mount option %q", pair[0])
+		}
+	}
+	return mountOptions, nil
+}
+
+func defaultReadOnly(volume v1.Volume) bool {
+	if volume.EmptyDir != nil || volume.PersistentVolumeClaim != nil {
+		return false
+	}
 	return true
 }
 
@@ -770,16 +810,24 @@ func existsVolumeNameInVolumeMounts(volumeName string, volumeMounts []corev1.Vol
 // =======================================================================================
 
 func getMountInfo(volume string) *MountInfo {
-	slices := strings.SplitN(volume, "/", 2)
-	if len(slices) == 1 || slices[1] == "" {
-		return &MountInfo{
-			VolumeName: slices[0],
+	configSlices := strings.SplitN(volume, ":", 2)
+	var mountInfo MountInfo
+	if len(configSlices) == 2 {
+		readOnlySlices := strings.SplitN(configSlices[1], "=", 2)
+		switch strings.ToLower(readOnlySlices[0]) {
+		case "readonly":
+
 		}
+		mountInfo.MountOptions = configSlices[1]
 	}
-	return &MountInfo{
-		VolumeName: slices[0],
-		SubPath:    slices[1],
+	slices := strings.SplitN(configSlices[0], "/", 2)
+	if len(slices) == 1 || slices[1] == "" {
+		mountInfo.VolumeName = slices[0]
+	} else {
+		mountInfo.VolumeName = slices[0]
+		mountInfo.SubPath = slices[1]
 	}
+	return &mountInfo
 }
 
 func reviseVolumeInfoAndMountsToUpdate(mountsToUpdate *util.OrderedMap, volumesToUpdate *util.OrderedMap) (*util.OrderedMap, *util.OrderedMap, error) {
diff --git a/tools/knb/README.md b/tools/knb/README.md
index bd0f60e2..2d228bb6 100644
--- a/tools/knb/README.md
+++ b/tools/knb/README.md
@@ -10,7 +10,7 @@ go build
 
 ### Install
 ```bash
-go get knative.dev/client/tools/knb
+go install knative.dev/client/tools/knb@latest
 ```
 
 ### Usage
@@ -34,6 +34,19 @@ plugins:
         version: v0.0.0-20200302150141-5c8b2ff67527
 ```
 
+Alternatively drop-in replace of a plugin in `.kn.yaml`
+```yaml
+plugins:
+  - name: kn-plugin-source-kafka
+    module: knative.dev/kn-plugin-source-kafka
+    pluginImportPath: knative.dev/kn-plugin-source-kafka/plugin
+    version: v0.19.0
+    replace:
+      - module: knative.dev/kn-plugin-source-kafka
+        moduleSource: github.com/forked-repository/kn-plugin-source-kafka
+        version: WIP
+```
+
 Required:
 * name
 * module - go module name to be used for import and in go.mod file
diff --git a/tools/knb/pkg/plugin/distro.go b/tools/knb/pkg/plugin/distro.go
index 6ffc9c79..55fe2448 100644
--- a/tools/knb/pkg/plugin/distro.go
+++ b/tools/knb/pkg/plugin/distro.go
@@ -74,6 +74,14 @@ func NewDistroGenerateCmd() *cobra.Command {
 			if err := exec.Command("gofmt", "-s", "-w", registerFile).Run(); err != nil {
 				return fmt.Errorf("gofmt failed: %w", err)
 			}
+			if fileExists("hack/update-dep.sh") {
+				_, err := exec.Command("hack/update-deps.sh").Output()
+				if err != nil {
+					return fmt.Errorf("update-deps.sh failed failed: %w", err)
+				}
+			} else {
+				fmt.Println("⚠️  hack/update-dep.sh script doesn't exist, make sure to update vendor/ dir")
+			}
 			return nil
 		},
 	}
@@ -102,6 +110,10 @@ func processPlugin(p Plugin, registerFile string) error {
 	if err := processModuleReplace(p); err != nil {
 		return err
 	}
+	// Exec `go mod tidy` per plugin added to resolve version
+	if err := goModTidy(); err != nil {
+		return err
+	}
 	return nil
 }
 
@@ -120,8 +132,13 @@ func processModuleRequire(plugin Plugin) error {
 func processModuleReplace(plugin Plugin) error {
 	if len(plugin.Replace) > 0 {
 		for _, r := range plugin.Replace {
+			// replace source of module dep, e.g. custom git repository
+			source := r.Module
+			if r.ModuleSource != "" {
+				source = r.ModuleSource
+			}
 			//nolint:gosec // Expected go cmd.
-			_, err := exec.Command("go", "mod", "edit", "-replace", r.Module+"="+r.Module+"@"+r.Version).Output()
+			_, err := exec.Command("go", "mod", "edit", "-replace", r.Module+"="+source+"@"+r.Version).Output()
 			if err != nil {
 				return fmt.Errorf("go mod edit -replace failed: %w", err)
 			}
@@ -131,6 +148,17 @@ func processModuleReplace(plugin Plugin) error {
 	return nil
 }
 
+// goModTidy executes `go mod tidy` command
+func goModTidy() error {
+	//nolint:gosec // Expected go cmd.
+	_, err := exec.Command("go", "mod", "tidy").Output()
+	if err != nil {
+		return fmt.Errorf("go mod tidy failed: %w", err)
+	}
+	fmt.Println("✔  go mod tidy")
+	return nil
+}
+
 // appendImport adds specified importPath to plugin registration file.
 // New file is initialized if it doesn't exist.
 // Warning message is displayed if the plugin import is already present.
diff --git a/tools/knb/pkg/plugin/plugin.go b/tools/knb/pkg/plugin/plugin.go
index 89e9eea8..508a480a 100644
--- a/tools/knb/pkg/plugin/plugin.go
+++ b/tools/knb/pkg/plugin/plugin.go
@@ -31,8 +31,9 @@ type Plugin struct {
 
 // Plugin represents go module replacement declaration
 type Replace struct {
-	Module  string `yaml:"module"`
-	Version string `yaml:"version"`
+	Module       string `yaml:"module"`
+	ModuleSource string `yaml:"moduleSource,omitempty"`
+	Version      string `yaml:"version"`
 }
 
 // NewPluginCmd represents plugin command group


Test patch:
diff --git a/pkg/kn/flags/podspec_helper_test.go b/pkg/kn/flags/podspec_helper_test.go
index 5a0c0de1..7671b7df 100644
--- a/pkg/kn/flags/podspec_helper_test.go
+++ b/pkg/kn/flags/podspec_helper_test.go
@@ -300,7 +300,6 @@ func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
 			VolumeSource: corev1.VolumeSource{
 				PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
 					ClaimName: "pvc1",
-					ReadOnly:  true,
 				},
 			},
 		})
@@ -353,20 +352,18 @@ func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
 		},
 		corev1.VolumeMount{
 			Name:      "new-pvc-volume-name-1",
-			ReadOnly:  true,
 			MountPath: "/pvc-1/mount/path",
 		},
 		corev1.VolumeMount{
 			Name:      "new-pvc-volume-name-2",
-			ReadOnly:  true,
 			MountPath: "/pvc-2/mount/path",
 		},
 	)
 
 	err := UpdateVolumeMountsAndVolumes(spec,
-		util.NewOrderedMapWithKVStrings([][]string{{"/new-config-map/mount/path", "new-config-map-volume-name"}}),
+		util.NewOrderedMapWithKVStrings([][]string{{"/new-config-map/mount/path", "config-map:new-config-map:readOnly=false"}}),
 		[]string{},
-		util.NewOrderedMapWithKVStrings([][]string{{"new-config-map-volume-name", "config-map:new-config-map"}}),
+		util.NewOrderedMap(),
 		[]string{})
 	assert.NilError(t, err)
 
@@ -434,7 +431,7 @@ func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
 		[]string{})
 	assert.NilError(t, err)
 	err = UpdateVolumeMountsAndVolumes(spec,
-		util.NewOrderedMapWithKVStrings([][]string{{"/pvc-2/mount/path", "pvc:pvc2"}}),
+		util.NewOrderedMapWithKVStrings([][]string{{"/pvc-2/mount/path", "pvc:pvc2:readOnly=true"}}),
 		[]string{},
 		util.NewOrderedMap(),
 		[]string{})
@@ -459,8 +456,6 @@ func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
 	assert.DeepEqual(t, spec.Volumes[5].EmptyDir.SizeLimit, &quantity)
 	assert.Equal(t, spec.Volumes[6].Name, "new-pvc-volume-name-1")
 	assert.Equal(t, spec.Volumes[6].PersistentVolumeClaim.ClaimName, "pvc1")
-	assert.Equal(t, spec.Volumes[7].Name, "new-config-map-volume-name")
-	assert.Equal(t, spec.Volumes[7].ConfigMap.Name, "new-config-map")
 	assert.Equal(t, spec.Volumes[8].Name, "new-secret-volume-name")
 	assert.Equal(t, spec.Volumes[8].Secret.SecretName, "new-secret")
 	assert.Assert(t, strings.Contains(spec.Volumes[9].Name, "empty-dir-5"))
@@ -469,28 +464,40 @@ func TestUpdateVolumeMountsAndVolumes(t *testing.T) {
 
 	assert.Equal(t, container.VolumeMounts[0].Name, "existing-config-map-volume-name-2")
 	assert.Equal(t, container.VolumeMounts[0].MountPath, "/existing-config-map-2/mount/path")
+	assert.Equal(t, container.VolumeMounts[0].ReadOnly, true)
 	assert.Equal(t, container.VolumeMounts[1].Name, "existing-secret-volume-name-2")
 	assert.Equal(t, container.VolumeMounts[1].MountPath, "/existing-secret-2/mount/path")
+	assert.Equal(t, container.VolumeMounts[1].ReadOnly, true)
 	assert.Equal(t, container.VolumeMounts[2].Name, "new-empty-dir-volume-name-1")
 	assert.Equal(t, container.VolumeMounts[2].MountPath, "/empty-dir-1/mount/path")
+	assert.Equal(t, container.VolumeMounts[2].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[3].Name, "new-empty-dir-volume-name-2")
 	assert.Equal(t, container.VolumeMounts[3].MountPath, "/empty-dir-2/mount/path")
+	assert.Equal(t, container.VolumeMounts[3].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[4].Name, "new-empty-dir-volume-name-3")
 	assert.Equal(t, container.VolumeMounts[4].MountPath, "/empty-dir-3/mount/path")
+	assert.Equal(t, container.VolumeMounts[4].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[5].Name, "new-empty-dir-volume-name-4")
 	assert.Equal(t, container.VolumeMounts[5].MountPath, "/empty-dir-4/mount/path")
+	assert.Equal(t, container.VolumeMounts[5].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[6].MountPath, "/empty-dir-5/mount/path")
+	assert.Equal(t, container.VolumeMounts[6].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[7].Name, "new-pvc-volume-name-1")
 	assert.Equal(t, container.VolumeMounts[7].MountPath, "/pvc-1/mount/path")
+	assert.Equal(t, container.VolumeMounts[7].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[8].MountPath, "/pvc-2/mount/path")
-	assert.Equal(t, container.VolumeMounts[9].Name, "new-config-map-volume-name")
+	assert.Equal(t, container.VolumeMounts[8].ReadOnly, true)
 	assert.Equal(t, container.VolumeMounts[9].MountPath, "/new-config-map/mount/path")
+	assert.Equal(t, container.VolumeMounts[9].ReadOnly, false)
 	assert.Equal(t, container.VolumeMounts[10].Name, "existing-config-map-volume-name-2")
 	assert.Equal(t, container.VolumeMounts[10].MountPath, "/updated-config-map/mount/path")
+	assert.Equal(t, container.VolumeMounts[10].ReadOnly, true)
 	assert.Equal(t, container.VolumeMounts[11].Name, "new-secret-volume-name")
 	assert.Equal(t, container.VolumeMounts[11].MountPath, "/new-secret/mount/path")
+	assert.Equal(t, container.VolumeMounts[11].ReadOnly, true)
 	assert.Equal(t, container.VolumeMounts[12].Name, "existing-secret-volume-name-2")
 	assert.Equal(t, container.VolumeMounts[12].MountPath, "/updated-secret/mount/path")
+	assert.Equal(t, container.VolumeMounts[12].ReadOnly, true)
 }
 
 func TestUpdateContainerImage(t *testing.T) {
diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go
index 8dbde138..62e3c402 100644
--- a/test/e2e/service_test.go
+++ b/test/e2e/service_test.go
@@ -231,7 +231,7 @@ func serviceCreateWithMount(r *test.KnRunResultCollector) {
 	r.AssertNoError(out)
 
 	r.T().Log("update service with a new emptyDir mount with Memory and dir size")
-	out = r.KnTest().Kn().Run("service", "update", "test-svc", "--mount", "/mydir4=ed:myvol:type=Memory,size=100Mi")
+	out = r.KnTest().Kn().Run("service", "update", "test-svc", "--mount", "/mydir4=myvol", "--volume", "myvol=ed:myvol:type=Memory,size=100Mi")
 	r.AssertNoError(out)
 
 	r.T().Log("create PVC test-pvc")