diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index ec3cf42618..6a5c67245e 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -32,6 +32,10 @@ | Added --scale-metric flag to configure metric name | https://github.com/knative/client/pull/1653[#1653] +| 🎁 +| Added subpath functionality to --mount flag +| https://github.com/knative/client/pull/1655[#1655] + |=== ## v1.3.0 (2022-03-08) [cols="1,10,3", options="header", width="100%"] diff --git a/docs/cmd/kn_container_add.md b/docs/cmd/kn_container_add.md index 01d5146e40..be61871c5c 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -p, --port string The port where application listens on, in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'. --pull-policy string Image pull policy. Valid values (case insensitive): Always | Never | IfNotPresent --pull-secret string Image pull secret to set. An empty argument ("") clears the pull secret. The referenced secret must exist in the service's namespace. diff --git a/docs/cmd/kn_service_apply.md b/docs/cmd/kn_service_apply.md index 35c7234cd9..73a222d2ac 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -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 d522c335dc..5dce7e2969 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -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 86f46fcc0c..cc747559ef 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -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 93dd069553..fdaa3658bb 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -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'. --pull-policy string Image pull policy. Valid values (case insensitive): Always | Never | IfNotPresent diff --git a/docs/cmd/kn_source_container_update.md b/docs/cmd/kn_source_container_update.md index 95d96ef8fa..322e605a24 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, or --mount /mydir=myvolume. When a configmap or a secret is specified, a corresponding volume is automatically generated. 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:), or an existing Volume (without any prefix) on the specified directory. Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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. -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'. --pull-policy string Image pull policy. Valid values (case insensitive): Always | Never | IfNotPresent diff --git a/pkg/kn/flags/podspec.go b/pkg/kn/flags/podspec.go index ccd0b1eb6d..f79efab060 100644 --- a/pkg/kn/flags/podspec.go +++ b/pkg/kn/flags/podspec.go @@ -140,6 +140,8 @@ func (p *PodSpecFlags) AddFlags(flagset *pflag.FlagSet) []string { "Mount a ConfigMap (prefix cm: or config-map:), a Secret (prefix secret: or sc:), or an existing Volume (without any prefix) on the specified directory. "+ "Example: --mount /mydir=cm:myconfigmap, --mount /mydir=secret:mysecret, 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.") flagNames = append(flagNames, "mount") diff --git a/pkg/kn/flags/podspec_helper.go b/pkg/kn/flags/podspec_helper.go index f6ff0dfa35..e45824049f 100644 --- a/pkg/kn/flags/podspec_helper.go +++ b/pkg/kn/flags/podspec_helper.go @@ -38,6 +38,11 @@ const ( PortFormatErr = "the port specification '%s' is not valid. Please provide in the format 'NAME:PORT', where 'NAME' is optional. Examples: '--port h2c:8080' , '--port 8080'." ) +type MountInfo struct { + VolumeName string + SubPath string +} + func (vt VolumeSourceType) String() string { names := [...]string{"config-map", "secret"} if vt < ConfigMapVolumeSourceType || vt > SecretVolumeSourceType { @@ -473,26 +478,32 @@ func updateVolumeMountsFromMap(volumeMounts []corev1.VolumeMount, toUpdate *util for i := range volumeMounts { volumeMount := &volumeMounts[i] - name, present := toUpdate.GetString(volumeMount.MountPath) + mountInfo, present := toUpdate.Get(volumeMount.MountPath) if present { + volumeMountInfo := mountInfo.(*MountInfo) + name := volumeMountInfo.VolumeName if !existsVolumeNameInVolumes(name, volumes) { return nil, fmt.Errorf("There is no volume matched with %q", name) } volumeMount.ReadOnly = true volumeMount.Name = name + volumeMount.SubPath = volumeMountInfo.SubPath set[volumeMount.MountPath] = true } } it := toUpdate.Iterator() - for mountPath, name, ok := it.NextString(); ok; mountPath, name, ok = it.NextString() { + for mountPath, mountInfo, ok := it.Next(); ok; mountPath, mountInfo, ok = it.Next() { + volumeMountInfo := mountInfo.(*MountInfo) + name := volumeMountInfo.VolumeName if !set[mountPath] { volumeMounts = append(volumeMounts, corev1.VolumeMount{ Name: name, ReadOnly: true, MountPath: mountPath, + SubPath: volumeMountInfo.SubPath, }) } } @@ -658,6 +669,19 @@ 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], + } + } + return &MountInfo{ + VolumeName: slices[0], + SubPath: slices[1], + } +} + func reviseVolumeInfoAndMountsToUpdate(mountsToUpdate *util.OrderedMap, volumesToUpdate *util.OrderedMap) (*util.OrderedMap, *util.OrderedMap, error) { volumeSourceInfoByName := util.NewOrderedMap() //make(map[string]*volumeSourceInfo) mountsToUpdateRevised := util.NewOrderedMap() //make(map[string]string) @@ -668,23 +692,28 @@ func reviseVolumeInfoAndMountsToUpdate(mountsToUpdate *util.OrderedMap, volumesT // slices[1] -> secret, config-map, or volume name slices := strings.SplitN(value, ":", 2) if len(slices) == 1 { - mountsToUpdateRevised.Set(path, slices[0]) + mountInfo := getMountInfo(slices[0]) + mountsToUpdateRevised.Set(path, mountInfo) } else { switch volumeType := slices[0]; volumeType { case "config-map", "cm": generatedName := util.GenerateVolumeName(path) + mountInfo := getMountInfo(slices[1]) volumeSourceInfoByName.Set(generatedName, &volumeSourceInfo{ volumeSourceType: ConfigMapVolumeSourceType, - volumeSourceName: slices[1], + volumeSourceName: mountInfo.VolumeName, }) - mountsToUpdateRevised.Set(path, generatedName) + mountInfo.VolumeName = generatedName + mountsToUpdateRevised.Set(path, mountInfo) case "secret", "sc": generatedName := util.GenerateVolumeName(path) + mountInfo := getMountInfo(slices[1]) volumeSourceInfoByName.Set(generatedName, &volumeSourceInfo{ volumeSourceType: SecretVolumeSourceType, - volumeSourceName: slices[1], + volumeSourceName: mountInfo.VolumeName, }) - mountsToUpdateRevised.Set(path, generatedName) + mountInfo.VolumeName = generatedName + mountsToUpdateRevised.Set(path, mountInfo) default: return nil, nil, fmt.Errorf("unsupported volume type \"%q\"; supported volume types are \"config-map or cm\", \"secret or sc\", and \"volume or vo\"", slices[0]) diff --git a/pkg/kn/flags/podspec_test.go b/pkg/kn/flags/podspec_test.go index bf90daf12b..005371ffa6 100644 --- a/pkg/kn/flags/podspec_test.go +++ b/pkg/kn/flags/podspec_test.go @@ -136,6 +136,54 @@ func TestPodSpecResolve(t *testing.T) { testCmd.Execute() } +func TestPodSpecMountResolveSubPath(t *testing.T) { + args := map[string]*MountInfo{ + "/mydir=cm:my-cm": { + VolumeName: "my-cm", + }, + "/mydir=sc:my-sec": { + VolumeName: "my-sec", + }, + "/mydir=myvol": { + VolumeName: "myvol", + }, + + "/mydir=cm:my-cm/subpath/to/mount": { + VolumeName: "my-cm", + SubPath: "subpath/to/mount", + }, + "/mydir=sc:my-sec/subpath/to/mount": { + VolumeName: "my-sec", + SubPath: "subpath/to/mount", + }, + "/mydir=myvol/subpath/to/mount": { + VolumeName: "myvol", + SubPath: "subpath/to/mount", + }, + } + + for arg, mountInfo := range args { + outBuf := bytes.Buffer{} + flags := &PodSpecFlags{} + inputArgs := append([]string{"--image", "repo/user/imageID:tag", "--mount"}, arg) + testCmd := &cobra.Command{ + Use: "test", + Run: func(cmd *cobra.Command, args []string) { + podSpec := &corev1.PodSpec{Containers: []corev1.Container{{}}} + err := flags.ResolvePodSpec(podSpec, cmd.Flags(), inputArgs) + assert.NilError(t, err) + assert.Equal(t, podSpec.Containers[0].VolumeMounts[0].SubPath, mountInfo.SubPath) + }, + } + testCmd.SetOut(&outBuf) + + testCmd.SetArgs(inputArgs) + flags.AddFlags(testCmd.Flags()) + flags.AddCreateFlags(testCmd.Flags()) + testCmd.Execute() + } +} + func TestPodSpecResolveContainers(t *testing.T) { rawInput := ` containers: diff --git a/test/e2e/service_test.go b/test/e2e/service_test.go index c971ac0941..2ca4bccb26 100644 --- a/test/e2e/service_test.go +++ b/test/e2e/service_test.go @@ -18,17 +18,19 @@ package e2e import ( + "encoding/json" "fmt" "strings" "testing" "gotest.tools/v3/assert" - + v1 "k8s.io/api/core/v1" "knative.dev/client/lib/test" "knative.dev/client/pkg/util" network "knative.dev/networking/pkg" pkgtest "knative.dev/pkg/test" "knative.dev/serving/pkg/apis/serving" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" ) func TestService(t *testing.T) { @@ -70,6 +72,9 @@ func TestService(t *testing.T) { test.ServiceCreate(r, "service2") test.ServiceCreate(r, "ksvc3") serviceDeleteAll(r) + + t.Log("create services with volume mounts and subpaths") + serviceCreateWithMount(r) } func serviceCreatePrivate(r *test.KnRunResultCollector, serviceName string) { @@ -170,3 +175,55 @@ func serviceDeleteAll(r *test.KnRunResultCollector) { // Check if no services present after kn service delete --all. assert.Check(r.T(), strings.Contains(out.Stdout, "No services found."), "Failed to show 'No services found' after kn service delete --all") } + +func serviceCreateWithMount(r *test.KnRunResultCollector) { + it := r.KnTest() + kubectl := test.NewKubectl(it.Namespace()) + + r.T().Log("create cm test-cm") + _, err := kubectl.Run("create", "configmap", "test-cm", "--from-literal=key=value") + assert.NilError(r.T(), err) + + r.T().Log("create service with configmap mounted") + out := r.KnTest().Kn().Run("service", "create", "test-svc", "--image", pkgtest.ImagePath("helloworld"), "--mount", "/mydir=cm:test-cm") + r.AssertNoError(out) + + r.T().Log("update the subpath in mounted cm") + out = r.KnTest().Kn().Run("service", "update", "test-svc", "--mount", "/mydir=cm:test-cm/key") + r.AssertNoError(out) + + r.T().Log("create secret test-sec") + _, err = kubectl.Run("create", "secret", "generic", "test-sec", "--from-literal", "key1=val1") + assert.NilError(r.T(), err) + + r.T().Log("update service with a new mount") + out = r.KnTest().Kn().Run("service", "update", "test-svc", "--mount", "/mydir2=sc:test-sec/key1") + r.AssertNoError(out) + + serviceDescribeMount(r, "test-svc", "/mydir", "key") +} + +func getVolumeMountWithHostPath(svc *servingv1.Service, hostPath string) *v1.VolumeMount { + vols := svc.Spec.Template.Spec.Containers[0].VolumeMounts + + for _, v := range vols { + if v.MountPath == hostPath { + return &v + } + } + return nil +} +func serviceDescribeMount(r *test.KnRunResultCollector, serviceName, hostPath, subPath string) { + out := r.KnTest().Kn().Run("service", "describe", serviceName, "-o=json") + r.AssertNoError(out) + + svc := &servingv1.Service{} + assert.NilError(r.T(), json.Unmarshal([]byte(out.Stdout), &svc)) + + r.T().Log("check volume mounts not nil") + volumeMount := getVolumeMountWithHostPath(svc, hostPath) + assert.Check(r.T(), volumeMount != nil) + + r.T().Log("check volume mount subpath is the same as given") + assert.Equal(r.T(), subPath, volumeMount.SubPath) +}