From 7a721348b3c0517d726bc8c8a0936e055ea9cf2c Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Tue, 18 Apr 2023 13:58:45 +0200 Subject: [PATCH] Automount volumes (#6698) * Simplify AddOdoProjectVolume and AddOdoMandatoryVolume * Rename / Clarify HandleEphemeralStorage function * Regroup volume-specific code * Move volume specific code to a separated function * Add a new module configAutomount * Automount PVC (without options) * Add unit tests * Separate functions * Mount secrets * Mount configmaps * Specific mount path * MountAs annotation * Mounting cm/secret as env * Refacto: use inAllContainers + replace result with volume * Mounting cm/secret as subpath * Read-only * Integration tests * Rename label * Automount during odo deploy Exec command * Add documentation * Fix TODO * Review * Fix indentation * Rename labels/annotations --- .../podman-support-limitations.md | 3 + .../advanced/air-gap-environment.md | 36 +- .../advanced/automounting-volumes.md | 39 ++ pkg/configAutomount/doc.go | 3 + pkg/configAutomount/interface.go | 38 ++ pkg/configAutomount/kubernetes.go | 160 ++++++++ pkg/configAutomount/kubernetes_test.go | 386 ++++++++++++++++++ pkg/configAutomount/mock.go | 49 +++ pkg/deploy/deploy.go | 55 ++- pkg/dev/kubedev/kubedev.go | 49 ++- pkg/dev/podmandev/pod.go | 4 +- .../adapters/kubernetes/component/adapter.go | 161 +++++--- .../kubernetes/component/adapter_test.go | 7 +- .../adapters/kubernetes/storage/utils.go | 218 +++++++++- .../adapters/kubernetes/storage/utils_test.go | 358 +++++++++++++++- .../adapters/kubernetes/utils/utils.go | 23 +- .../adapters/kubernetes/utils/utils_test.go | 12 +- pkg/kclient/configmap.go | 26 ++ pkg/kclient/interface.go | 3 + pkg/kclient/mock_Client.go | 15 + .../genericclioptions/clientset/clientset.go | 89 ++-- pkg/storage/kubernetes.go | 11 +- scripts/mockgen.sh | 4 + .../config-automount/as-env-configmap.yaml | 11 + .../config-automount/as-env-secret.yaml | 11 + .../config-automount/default-configmap.yaml | 9 + .../config-automount/default-pvc.yaml | 12 + .../config-automount/default-secret.yaml | 9 + .../mount-path-configmap.yaml | 11 + .../config-automount/mount-path-pvc.yaml | 14 + .../config-automount/mount-path-secret.yaml | 11 + .../config-automount/readonly-pvc.yaml | 14 + .../config-automount/subpath-configmap.yaml | 12 + .../config-automount/subpath-secret.yaml | 12 + tests/integration/cmd_dev_test.go | 90 ++++ tests/integration/cmd_devfile_deploy_test.go | 17 + 36 files changed, 1788 insertions(+), 194 deletions(-) create mode 100644 docs/website/docs/user-guides/advanced/automounting-volumes.md create mode 100644 pkg/configAutomount/doc.go create mode 100644 pkg/configAutomount/interface.go create mode 100644 pkg/configAutomount/kubernetes.go create mode 100644 pkg/configAutomount/kubernetes_test.go create mode 100644 pkg/configAutomount/mock.go create mode 100644 pkg/kclient/configmap.go create mode 100644 tests/examples/manifests/config-automount/as-env-configmap.yaml create mode 100644 tests/examples/manifests/config-automount/as-env-secret.yaml create mode 100644 tests/examples/manifests/config-automount/default-configmap.yaml create mode 100644 tests/examples/manifests/config-automount/default-pvc.yaml create mode 100644 tests/examples/manifests/config-automount/default-secret.yaml create mode 100644 tests/examples/manifests/config-automount/mount-path-configmap.yaml create mode 100644 tests/examples/manifests/config-automount/mount-path-pvc.yaml create mode 100644 tests/examples/manifests/config-automount/mount-path-secret.yaml create mode 100644 tests/examples/manifests/config-automount/readonly-pvc.yaml create mode 100644 tests/examples/manifests/config-automount/subpath-configmap.yaml create mode 100644 tests/examples/manifests/config-automount/subpath-secret.yaml diff --git a/docs/website/docs/development/architecture/podman-support-limitations.md b/docs/website/docs/development/architecture/podman-support-limitations.md index 160286d2868..19226bf88bc 100644 --- a/docs/website/docs/development/architecture/podman-support-limitations.md +++ b/docs/website/docs/development/architecture/podman-support-limitations.md @@ -58,3 +58,6 @@ When running `odo dev` on cluster, if you make changes to the Devfile affecting Pre-Stop events defined in the Devfile are not triggered when running `odo dev` on Podman. +## Auto-mounting volumes + +[Auto-mounting volumes](/docs/user-guides/advanced/automounting-volumes) is not supported when working on Podman. diff --git a/docs/website/docs/user-guides/advanced/air-gap-environment.md b/docs/website/docs/user-guides/advanced/air-gap-environment.md index 5e906f6c09c..11b7a642fff 100644 --- a/docs/website/docs/user-guides/advanced/air-gap-environment.md +++ b/docs/website/docs/user-guides/advanced/air-gap-environment.md @@ -127,31 +127,17 @@ commands: id: install ``` -You can also provide additional environment variables to the container: +You can also provide additional environment variables to the container, by creating an [auto-mounted configmap](/docs/user-guides/advanced/automounting-volumes): ```yaml -[...] -components: -- container: - args: - - tail - - -f - - /dev/null - endpoints: - - name: http-node - targetPort: 3000 - - exposure: none - name: debug - targetPort: 5858 - env: - - name: DEBUG_PORT - value: "5858" -# highlight-start - - name: npm_config_registry - value: https://your_local_registry -# highlight-end - image: your_secure_registry/nodejs-16:latest - memoryLimit: 1024Mi - mountSources: true - name: runtime +apiVersion: v1 +kind: ConfigMap +metadata: + name: proxy-config + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-as: env +data: + npm_config_registry: "https://your_local_registry" ``` diff --git a/docs/website/docs/user-guides/advanced/automounting-volumes.md b/docs/website/docs/user-guides/advanced/automounting-volumes.md new file mode 100644 index 00000000000..f9d0de64274 --- /dev/null +++ b/docs/website/docs/user-guides/advanced/automounting-volumes.md @@ -0,0 +1,39 @@ +--- +title: Automounting Volumes +sidebar_position: 8 +--- + +Existing [ConfigMaps](https://kubernetes.io/docs/concepts/configuration/configmap/), [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/), and [Persistent Volume Claims](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) on the cluster can be mounted automatically to all containers created by `odo`. These resources can be configured by applying the appropriate labels. + +To mark a resource for mounting to containers created by `odo`, apply the following label to the resource: + +```yaml +metadata: + labels: + devfile.io/auto-mount: "true" +``` + +By default, resources will be mounted based on the resource name: + +- Secrets will be mounted to `/etc/secret/` + +- Configmaps will be mounted to `/etc/config/` + +- Persistent volume claims will be mounted to `/tmp/` + +Mounting resources can be additionally configured via annotations: + +- `devfile.io/mount-path`: configure where the resource should be mounted + +- `devfile.io/mount-as`: for secrets and configmaps only, configure how the resource should be mounted to the container + + - If `devfile.io/mount-as: file`, the configmap/secret will be mounted as files within the mount path. This is the default behavior. + + - If `devfile.io/mount-as: subpath`, the keys and values in the configmap/secret will be mounted as files within the mount path using subpath volume mounts. + + - If `devfile.io/mount-as: env`, the keys and values in the configmap/secret will be mounted as environment variables in all containers. + + When `file` is used, the configmap is mounted as a directory within the containers, erasing any files/directories already present. When `subpath` is used, each key in the configmap/secret is mounted as a subpath volume mount in the mount path, leaving existing files intact but preventing changes to the secret/configmap from propagating into the containers without a restart. + +- `devfile.io/read-only`: for persistent volume claims, mount the resource as read-only + diff --git a/pkg/configAutomount/doc.go b/pkg/configAutomount/doc.go new file mode 100644 index 00000000000..6d02a7fa0a1 --- /dev/null +++ b/pkg/configAutomount/doc.go @@ -0,0 +1,3 @@ +// configAutomount package provides functions to work with automounted configuration resources (Configmap, Secret, PVC) +// Specified at https://github.com/devfile/devworkspace-operator/blob/main/docs/additional-configuration.adoc#automatically-mounting-volumes-configmaps-and-secrets +package configAutomount diff --git a/pkg/configAutomount/interface.go b/pkg/configAutomount/interface.go new file mode 100644 index 00000000000..97f81d7e104 --- /dev/null +++ b/pkg/configAutomount/interface.go @@ -0,0 +1,38 @@ +package configAutomount + +type MountAs int +type VolumeType int + +const ( + MountAsFile MountAs = iota + 1 + MountAsSubpath + MountAsEnv +) + +const ( + VolumeTypePVC VolumeType = iota + 1 + VolumeTypeConfigmap + VolumeTypeSecret +) + +type AutomountInfo struct { + // VolumeType gives the type of the volume (PVC, Secret, ConfigMap) + VolumeType VolumeType + // VolumeName is the name of the resource to mount + VolumeName string + // MountPath indicates on which path to mount the volume (empty if MountAs is Env) + MountPath string + // MountAs indicates how to mount the volume + // - File: by default + // - Env: As environment variables (for Secret and Configmap) + // - Subpath: As individual files in specific paths (For Secret and ConfigMap). Keys must be provided + MountAs MountAs + // ReadOnly indicates to mount the volume as Read-Only + ReadOnly bool + // Keys defines the list of keys to mount when MountAs is Subpath + Keys []string +} + +type Client interface { + GetAutomountingVolumes() ([]AutomountInfo, error) +} diff --git a/pkg/configAutomount/kubernetes.go b/pkg/configAutomount/kubernetes.go new file mode 100644 index 00000000000..787a9f24da4 --- /dev/null +++ b/pkg/configAutomount/kubernetes.go @@ -0,0 +1,160 @@ +package configAutomount + +import ( + "path/filepath" + "sort" + + "github.com/redhat-developer/odo/pkg/kclient" +) + +const ( + labelMountName = "devfile.io/auto-mount" + labelMountValue = "true" + + annotationMountPathName = "devfile.io/mount-path" + annotationMountAsName = "devfile.io/mount-as" + annotationReadOnlyName = "devfile.io/read-only" +) + +type KubernetesClient struct { + kubeClient kclient.ClientInterface +} + +func NewKubernetesClient(kubeClient kclient.ClientInterface) KubernetesClient { + return KubernetesClient{ + kubeClient: kubeClient, + } +} + +func (o KubernetesClient) GetAutomountingVolumes() ([]AutomountInfo, error) { + var result []AutomountInfo + + pvcs, err := o.getAutomountingPVCs() + if err != nil { + return nil, err + } + result = append(result, pvcs...) + + secrets, err := o.getAutomountingSecrets() + if err != nil { + return nil, err + } + result = append(result, secrets...) + + cms, err := o.getAutomountingConfigmaps() + if err != nil { + return nil, err + } + result = append(result, cms...) + + return result, nil +} + +func (o KubernetesClient) getAutomountingPVCs() ([]AutomountInfo, error) { + pvcs, err := o.kubeClient.ListPVCs(labelMountName + "=" + labelMountValue) + if err != nil { + return nil, err + } + + var result []AutomountInfo + for _, pvc := range pvcs { + mountPath := filepath.ToSlash(filepath.Join("/", "tmp", pvc.Name)) + if val, found := getMountPathFromAnnotation(pvc.Annotations); found { + mountPath = val + } + result = append(result, AutomountInfo{ + VolumeType: VolumeTypePVC, + VolumeName: pvc.Name, + MountPath: mountPath, + MountAs: MountAsFile, + ReadOnly: pvc.Annotations[annotationReadOnlyName] == "true", + }) + } + return result, nil +} + +func (o KubernetesClient) getAutomountingSecrets() ([]AutomountInfo, error) { + secrets, err := o.kubeClient.ListSecrets(labelMountName + "=" + labelMountValue) + if err != nil { + return nil, err + } + + var result []AutomountInfo + for _, secret := range secrets { + mountAs := getMountAsFromAnnotation(secret.Annotations) + mountPath := filepath.ToSlash(filepath.Join("/", "etc", "secret", secret.Name)) + var keys []string + if val, found := getMountPathFromAnnotation(secret.Annotations); found { + mountPath = val + } + if mountAs == MountAsEnv { + mountPath = "" + } + if mountAs == MountAsSubpath { + for k := range secret.Data { + keys = append(keys, k) + } + sort.Strings(keys) + } + result = append(result, AutomountInfo{ + VolumeType: VolumeTypeSecret, + VolumeName: secret.Name, + MountPath: mountPath, + MountAs: mountAs, + ReadOnly: secret.Annotations[annotationReadOnlyName] == "true", + Keys: keys, + }) + } + return result, nil +} + +func (o KubernetesClient) getAutomountingConfigmaps() ([]AutomountInfo, error) { + cms, err := o.kubeClient.ListConfigMaps(labelMountName + "=" + labelMountValue) + if err != nil { + return nil, err + } + + var result []AutomountInfo + for _, cm := range cms { + mountAs := getMountAsFromAnnotation(cm.Annotations) + mountPath := filepath.ToSlash(filepath.Join("/", "etc", "config", cm.Name)) + var keys []string + if val, found := getMountPathFromAnnotation(cm.Annotations); found { + mountPath = val + } + if mountAs == MountAsEnv { + mountPath = "" + } + if mountAs == MountAsSubpath { + for k := range cm.Data { + keys = append(keys, k) + } + sort.Strings(keys) + } + result = append(result, AutomountInfo{ + VolumeType: VolumeTypeConfigmap, + VolumeName: cm.Name, + MountPath: mountPath, + MountAs: mountAs, + ReadOnly: cm.Annotations[annotationReadOnlyName] == "true", + Keys: keys, + }) + } + return result, nil +} + +func getMountPathFromAnnotation(annotations map[string]string) (string, bool) { + val, found := annotations[annotationMountPathName] + return val, found +} + +func getMountAsFromAnnotation(annotations map[string]string) MountAs { + switch annotations[annotationMountAsName] { + case "subpath": + return MountAsSubpath + case "env": + return MountAsEnv + default: + return MountAsFile + } +} diff --git a/pkg/configAutomount/kubernetes_test.go b/pkg/configAutomount/kubernetes_test.go new file mode 100644 index 00000000000..57be9c657a0 --- /dev/null +++ b/pkg/configAutomount/kubernetes_test.go @@ -0,0 +1,386 @@ +package configAutomount + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-cmp/cmp" + "github.com/redhat-developer/odo/pkg/kclient" + corev1 "k8s.io/api/core/v1" +) + +func TestKubernetesClient_GetAutomountingVolumes(t *testing.T) { + + defaultPVC1 := corev1.PersistentVolumeClaim{} + defaultPVC1.SetName("defaultPVC1") + defaultPVC1.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + defaultPVC2 := corev1.PersistentVolumeClaim{} + defaultPVC2.SetName("defaultPVC2") + defaultPVC2.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + pvcMountPath := corev1.PersistentVolumeClaim{} + pvcMountPath.SetName("pvcMountPath") + pvcMountPath.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + pvcMountPath.SetAnnotations(map[string]string{ + annotationMountPathName: "/specific/pvc/mount/path", + }) + + roPVC := corev1.PersistentVolumeClaim{} + roPVC.SetName("roPVC") + roPVC.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + roPVC.SetAnnotations(map[string]string{ + annotationReadOnlyName: "true", + }) + + defaultSecret1 := corev1.Secret{} + defaultSecret1.SetName("defaultSecret1") + defaultSecret1.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + defaultSecret2 := corev1.Secret{} + defaultSecret2.SetName("defaultSecret2") + defaultSecret2.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + secretMountPath := corev1.Secret{} + secretMountPath.SetName("secretMountPath") + secretMountPath.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + secretMountPath.SetAnnotations(map[string]string{ + annotationMountPathName: "/specific/secret/mount/path", + }) + + secretMountAsSubpath := corev1.Secret{} + secretMountAsSubpath.Data = map[string][]byte{ + "secretKey1": []byte(""), + "secretKey2": []byte(""), + } + secretMountAsSubpath.SetName("secretMountAsSubpath") + secretMountAsSubpath.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + secretMountAsSubpath.SetAnnotations(map[string]string{ + annotationMountAsName: "subpath", + }) + + secretMountAsEnv := corev1.Secret{} + secretMountAsEnv.SetName("secretMountAsEnv") + secretMountAsEnv.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + secretMountAsEnv.SetAnnotations(map[string]string{ + annotationMountAsName: "env", + }) + + roSecret := corev1.Secret{} + roSecret.SetName("roSecret") + roSecret.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + roSecret.SetAnnotations(map[string]string{ + annotationReadOnlyName: "true", + }) + + defaultCM1 := corev1.ConfigMap{} + defaultCM1.SetName("defaultCM1") + defaultCM1.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + defaultCM2 := corev1.ConfigMap{} + defaultCM2.SetName("defaultCM2") + defaultCM2.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + + cmMountPath := corev1.ConfigMap{} + cmMountPath.SetName("cmMountPath") + cmMountPath.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + cmMountPath.SetAnnotations(map[string]string{ + annotationMountPathName: "/specific/configmap/mount/path", + }) + + cmMountAsSubpath := corev1.ConfigMap{} + cmMountAsSubpath.Data = map[string]string{ + "cmKey1": "", + "cmKey2": "", + } + cmMountAsSubpath.SetName("cmMountAsSubpath") + cmMountAsSubpath.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + cmMountAsSubpath.SetAnnotations(map[string]string{ + annotationMountAsName: "subpath", + }) + + cmMountAsEnv := corev1.ConfigMap{} + cmMountAsEnv.SetName("cmMountAsEnv") + cmMountAsEnv.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + cmMountAsEnv.SetAnnotations(map[string]string{ + annotationMountAsName: "env", + }) + + roCM := corev1.ConfigMap{} + roCM.SetName("roCM") + roCM.SetLabels(map[string]string{ + labelMountName: labelMountValue, + }) + roCM.SetAnnotations(map[string]string{ + annotationReadOnlyName: "true", + }) + + type fields struct { + kubeClient func(ctrl *gomock.Controller) kclient.ClientInterface + } + tests := []struct { + name string + fields fields + want []AutomountInfo + wantErr bool + }{ + { + name: "Single default PVC", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{defaultPVC1}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypePVC, + VolumeName: "defaultPVC1", + MountPath: "/tmp/defaultPVC1", + MountAs: MountAsFile, + }, + }, + wantErr: false, + }, + { + name: "Two default PVCs", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{defaultPVC1, defaultPVC2}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypePVC, + VolumeName: "defaultPVC1", + MountPath: "/tmp/defaultPVC1", + MountAs: MountAsFile, + }, + { + VolumeType: VolumeTypePVC, + VolumeName: "defaultPVC2", + MountPath: "/tmp/defaultPVC2", + MountAs: MountAsFile, + }, + }, + wantErr: false, + }, + { + name: "Two default secrets", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{defaultSecret1, defaultSecret2}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypeSecret, + VolumeName: "defaultSecret1", + MountPath: "/etc/secret/defaultSecret1", + MountAs: MountAsFile, + }, + { + VolumeType: VolumeTypeSecret, + VolumeName: "defaultSecret2", + MountPath: "/etc/secret/defaultSecret2", + MountAs: MountAsFile, + }, + }, + wantErr: false, + }, + { + name: "Two default configmaps", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{defaultCM1, defaultCM2}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "defaultCM1", + MountPath: "/etc/config/defaultCM1", + MountAs: MountAsFile, + }, + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "defaultCM2", + MountPath: "/etc/config/defaultCM2", + MountAs: MountAsFile, + }, + }, + wantErr: false, + }, + { + name: "PVC, Secret and ConfigMap with non default mount paths", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{pvcMountPath}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{secretMountPath}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{cmMountPath}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypePVC, + VolumeName: "pvcMountPath", + MountPath: "/specific/pvc/mount/path", + MountAs: MountAsFile, + }, + { + VolumeType: VolumeTypeSecret, + VolumeName: "secretMountPath", + MountPath: "/specific/secret/mount/path", + MountAs: MountAsFile, + }, + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "cmMountPath", + MountPath: "/specific/configmap/mount/path", + MountAs: MountAsFile, + }, + }, + wantErr: false, + }, + { + name: "Secret and ConfigMap with mount-as annotations", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{secretMountAsSubpath, secretMountAsEnv}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{cmMountAsSubpath, cmMountAsEnv}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypeSecret, + VolumeName: "secretMountAsSubpath", + MountPath: "/etc/secret/secretMountAsSubpath", + MountAs: MountAsSubpath, + Keys: []string{"secretKey1", "secretKey2"}, + }, + { + VolumeType: VolumeTypeSecret, + VolumeName: "secretMountAsEnv", + MountPath: "", + MountAs: MountAsEnv, + }, + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "cmMountAsSubpath", + MountPath: "/etc/config/cmMountAsSubpath", + MountAs: MountAsSubpath, + Keys: []string{"cmKey1", "cmKey2"}, + }, + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "cmMountAsEnv", + MountPath: "", + MountAs: MountAsEnv, + }, + }, + wantErr: false, + }, + { + name: "PVC, Secret and ConfigMap read-only", + fields: fields{ + kubeClient: func(ctrl *gomock.Controller) kclient.ClientInterface { + client := kclient.NewMockClientInterface(ctrl) + client.EXPECT().ListPVCs(gomock.Any()).Return([]corev1.PersistentVolumeClaim{roPVC}, nil).AnyTimes() + client.EXPECT().ListSecrets(gomock.Any()).Return([]corev1.Secret{roSecret}, nil).AnyTimes() + client.EXPECT().ListConfigMaps(gomock.Any()).Return([]corev1.ConfigMap{roCM}, nil).AnyTimes() + return client + }, + }, + want: []AutomountInfo{ + { + VolumeType: VolumeTypePVC, + VolumeName: "roPVC", + MountPath: "/tmp/roPVC", + MountAs: MountAsFile, + ReadOnly: true, + }, + { + VolumeType: VolumeTypeSecret, + VolumeName: "roSecret", + MountPath: "/etc/secret/roSecret", + MountAs: MountAsFile, + ReadOnly: true, + }, + { + VolumeType: VolumeTypeConfigmap, + VolumeName: "roCM", + MountPath: "/etc/config/roCM", + MountAs: MountAsFile, + ReadOnly: true, + }, + }, + wantErr: false, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + o := KubernetesClient{ + kubeClient: tt.fields.kubeClient(ctrl), + } + got, err := o.GetAutomountingVolumes() + if (err != nil) != tt.wantErr { + t.Errorf("KubernetesClient.GetAutomountingVolumes() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("KubernetesClient.GetAutomountingVolumes() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/configAutomount/mock.go b/pkg/configAutomount/mock.go new file mode 100644 index 00000000000..0931cf8b8b2 --- /dev/null +++ b/pkg/configAutomount/mock.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: pkg/configAutomount/interface.go + +// Package configAutomount is a generated GoMock package. +package configAutomount + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// GetAutomountingVolumes mocks base method. +func (m *MockClient) GetAutomountingVolumes() ([]AutomountInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAutomountingVolumes") + ret0, _ := ret[0].([]AutomountInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAutomountingVolumes indicates an expected call of GetAutomountingVolumes. +func (mr *MockClientMockRecorder) GetAutomountingVolumes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAutomountingVolumes", reflect.TypeOf((*MockClient)(nil).GetAutomountingVolumes)) +} diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 9f3580ebdcc..6ebeddb6120 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -17,6 +17,8 @@ import ( "k8s.io/utils/pointer" "github.com/redhat-developer/odo/pkg/component" + "github.com/redhat-developer/odo/pkg/configAutomount" + "github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/storage" "github.com/redhat-developer/odo/pkg/devfile/image" "github.com/redhat-developer/odo/pkg/kclient" odolabels "github.com/redhat-developer/odo/pkg/labels" @@ -29,16 +31,18 @@ import ( ) type DeployClient struct { - kubeClient kclient.ClientInterface - fs filesystem.Filesystem + kubeClient kclient.ClientInterface + configAutomountClient configAutomount.Client + fs filesystem.Filesystem } var _ Client = (*DeployClient)(nil) -func NewDeployClient(kubeClient kclient.ClientInterface, fs filesystem.Filesystem) *DeployClient { +func NewDeployClient(kubeClient kclient.ClientInterface, configAutomountClient configAutomount.Client, fs filesystem.Filesystem) *DeployClient { return &DeployClient{ - kubeClient: kubeClient, - fs: fs, + kubeClient: kubeClient, + configAutomountClient: configAutomountClient, + fs: fs, } } @@ -51,7 +55,7 @@ func (o *DeployClient) Deploy(ctx context.Context) error { appName = odocontext.GetApplication(ctx) ) - handler := newDeployHandler(ctx, o.fs, *devfileObj, path, o.kubeClient, appName, componentName) + handler := newDeployHandler(ctx, o.fs, *devfileObj, path, o.kubeClient, o.configAutomountClient, appName, componentName) err := o.buildPushAutoImageComponents(handler, *devfileObj) if err != nil { @@ -105,26 +109,28 @@ func (o *DeployClient) applyAutoK8sOrOcComponents(handler *deployHandler, devfil } type deployHandler struct { - ctx context.Context - fs filesystem.Filesystem - devfileObj parser.DevfileObj - path string - kubeClient kclient.ClientInterface - appName string - componentName string + ctx context.Context + fs filesystem.Filesystem + devfileObj parser.DevfileObj + path string + kubeClient kclient.ClientInterface + configAutomountClient configAutomount.Client + appName string + componentName string } var _ libdevfile.Handler = (*deployHandler)(nil) -func newDeployHandler(ctx context.Context, fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, kubeClient kclient.ClientInterface, appName string, componentName string) *deployHandler { +func newDeployHandler(ctx context.Context, fs filesystem.Filesystem, devfileObj parser.DevfileObj, path string, kubeClient kclient.ClientInterface, configAutomountClient configAutomount.Client, appName string, componentName string) *deployHandler { return &deployHandler{ - ctx: ctx, - fs: fs, - devfileObj: devfileObj, - path: path, - kubeClient: kubeClient, - appName: appName, - componentName: componentName, + ctx: ctx, + fs: fs, + devfileObj: devfileObj, + path: path, + kubeClient: kubeClient, + configAutomountClient: configAutomountClient, + appName: appName, + componentName: componentName, } } @@ -168,6 +174,13 @@ func (o *deployHandler) Execute(command v1alpha2.Command) error { podTemplateSpec.Spec.Containers[0].Command = []string{"/bin/sh"} podTemplateSpec.Spec.Containers[0].Args = getCmdline(command) + volumes, err := storage.GetAutomountVolumes(o.configAutomountClient, podTemplateSpec.Spec.Containers, podTemplateSpec.Spec.InitContainers) + if err != nil { + return err + } + + podTemplateSpec.Spec.Volumes = volumes + // Create a Kubernetes Job and use the container image referenced by command.Exec.Component // Get the component for the command with command.Exec.Component getJobName := func() string { diff --git a/pkg/dev/kubedev/kubedev.go b/pkg/dev/kubedev/kubedev.go index d52052c6bbd..47972932461 100644 --- a/pkg/dev/kubedev/kubedev.go +++ b/pkg/dev/kubedev/kubedev.go @@ -10,6 +10,7 @@ import ( "github.com/redhat-developer/odo/pkg/binding" _delete "github.com/redhat-developer/odo/pkg/component/delete" + "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/dev" "github.com/redhat-developer/odo/pkg/devfile" "github.com/redhat-developer/odo/pkg/exec" @@ -36,15 +37,16 @@ const ( ) type DevClient struct { - kubernetesClient kclient.ClientInterface - prefClient preference.Client - portForwardClient portForward.Client - watchClient watch.Client - bindingClient binding.Client - syncClient sync.Client - filesystem filesystem.Filesystem - execClient exec.Client - deleteClient _delete.Client + kubernetesClient kclient.ClientInterface + prefClient preference.Client + portForwardClient portForward.Client + watchClient watch.Client + bindingClient binding.Client + syncClient sync.Client + filesystem filesystem.Filesystem + execClient exec.Client + deleteClient _delete.Client + configAutomountClient configAutomount.Client } var _ dev.Client = (*DevClient)(nil) @@ -59,17 +61,19 @@ func NewDevClient( filesystem filesystem.Filesystem, execClient exec.Client, deleteClient _delete.Client, + configAutomountClient configAutomount.Client, ) *DevClient { return &DevClient{ - kubernetesClient: kubernetesClient, - prefClient: prefClient, - portForwardClient: portForwardClient, - watchClient: watchClient, - bindingClient: bindingClient, - syncClient: syncClient, - filesystem: filesystem, - execClient: execClient, - deleteClient: deleteClient, + kubernetesClient: kubernetesClient, + prefClient: prefClient, + portForwardClient: portForwardClient, + watchClient: watchClient, + bindingClient: bindingClient, + syncClient: syncClient, + filesystem: filesystem, + execClient: execClient, + deleteClient: deleteClient, + configAutomountClient: configAutomountClient, } } @@ -89,7 +93,13 @@ func (o *DevClient) Start( ) adapter := component.NewKubernetesAdapter( - o.kubernetesClient, o.prefClient, o.portForwardClient, o.bindingClient, o.syncClient, o.execClient, + o.kubernetesClient, + o.prefClient, + o.portForwardClient, + o.bindingClient, + o.syncClient, + o.execClient, + o.configAutomountClient, component.AdapterContext{ ComponentName: componentName, Context: path, @@ -172,6 +182,7 @@ func (o *DevClient) regenerateComponentAdapterFromWatchParams(parameters watch.W o.bindingClient, o.syncClient, o.execClient, + o.configAutomountClient, component.AdapterContext{ ComponentName: parameters.ComponentName, Context: parameters.Path, diff --git a/pkg/dev/podmandev/pod.go b/pkg/dev/podmandev/pod.go index 787715d14b7..f841dd4bdb4 100644 --- a/pkg/dev/podmandev/pod.go +++ b/pkg/dev/podmandev/pod.go @@ -55,8 +55,8 @@ func createPodFromComponent( return nil, nil, err } - utils.AddOdoProjectVolume(&containers) - utils.AddOdoMandatoryVolume(&containers) + utils.AddOdoProjectVolume(containers) + utils.AddOdoMandatoryVolume(containers) volumes := []corev1.Volume{ { diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 8b8879b60c2..cafad970e2f 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -16,6 +16,7 @@ import ( "github.com/redhat-developer/odo/pkg/binding" "github.com/redhat-developer/odo/pkg/component" + "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/dev/common" "github.com/redhat-developer/odo/pkg/devfile/adapters" "github.com/redhat-developer/odo/pkg/devfile/adapters/kubernetes/storage" @@ -50,12 +51,13 @@ import ( // Adapter is a component adapter implementation for Kubernetes type Adapter struct { - kubeClient kclient.ClientInterface - prefClient preference.Client - portForwardClient portForward.Client - bindingClient binding.Client - syncClient sync.Client - execClient exec.Client + kubeClient kclient.ClientInterface + prefClient preference.Client + portForwardClient portForward.Client + bindingClient binding.Client + syncClient sync.Client + execClient exec.Client + configAutomountClient configAutomount.Client AdapterContext logger machineoutput.MachineEventLoggingClient @@ -80,17 +82,19 @@ func NewKubernetesAdapter( bindingClient binding.Client, syncClient sync.Client, execClient exec.Client, + configAutomountClient configAutomount.Client, context AdapterContext, ) Adapter { return Adapter{ - kubeClient: kubernetesClient, - prefClient: prefClient, - portForwardClient: portForwardClient, - bindingClient: bindingClient, - syncClient: syncClient, - execClient: execClient, - AdapterContext: context, - logger: machineoutput.NewMachineEventLoggingClient(), + kubeClient: kubernetesClient, + prefClient: prefClient, + portForwardClient: portForwardClient, + bindingClient: bindingClient, + syncClient: syncClient, + execClient: execClient, + configAutomountClient: configAutomountClient, + AdapterContext: context, + logger: machineoutput.NewMachineEventLoggingClient(), } } @@ -381,29 +385,10 @@ func (a *Adapter) createOrUpdateComponent( deployment *appsv1.Deployment, ) (*appsv1.Deployment, bool, error) { - isMainStorageEphemeral := a.prefClient.GetEphemeralSourceVolume() - componentName := a.ComponentName runtime := component.GetComponentRuntimeFromDevfileMetadata(a.Devfile.Data.GetMetadata()) - storageClient := storagepkg.NewClient(componentName, a.AppName, storagepkg.ClientOptions{ - Client: a.kubeClient, - Runtime: runtime, - }) - - // handle the ephemeral storage - err := storage.HandleEphemeralStorage(a.kubeClient, storageClient, componentName, isMainStorageEphemeral) - if err != nil { - return nil, false, err - } - - // From devfile info, create PVCs and return ephemeral storages - ephemerals, err := storagepkg.Push(storageClient, a.Devfile) - if err != nil { - return nil, false, err - } - // Set the labels labels := odolabels.GetLabels(componentName, a.AppName, runtime, odolabels.ComponentDevMode, true) @@ -433,52 +418,24 @@ func (a *Adapter) createOrUpdateComponent( return nil, false, fmt.Errorf("no valid components found in the devfile") } - // Add the project volume before generating init containers - utils.AddOdoProjectVolume(&containers) - utils.AddOdoMandatoryVolume(&containers) - - containers, err = utils.UpdateContainersEntrypointsIfNeeded(a.Devfile, containers, commands.BuildCmd, commands.RunCmd, commands.DebugCmd) - if err != nil { - return nil, false, err - } - initContainers := podTemplateSpec.Spec.InitContainers - // list all the pvcs for the component - pvcs, err := a.kubeClient.ListPVCs(fmt.Sprintf("%v=%v", "component", componentName)) - if err != nil { - return nil, false, err - } - - odoSourcePVCName, volumeNameToVolInfo, err := storage.GetVolumeInfos(pvcs) - if err != nil { - return nil, false, err - } - - var allVolumes []corev1.Volume - - // Get PVC volumes and Volume Mounts - pvcVolumes, err := storage.GetPersistentVolumesAndVolumeMounts(a.Devfile, containers, initContainers, volumeNameToVolInfo, parsercommon.DevfileOptions{}) + containers, err = utils.UpdateContainersEntrypointsIfNeeded(a.Devfile, containers, commands.BuildCmd, commands.RunCmd, commands.DebugCmd) if err != nil { return nil, false, err } - allVolumes = append(allVolumes, pvcVolumes...) - ephemeralVolumes, err := storage.GetEphemeralVolumesAndVolumeMounts(a.Devfile, containers, initContainers, ephemerals, parsercommon.DevfileOptions{}) + // Returns the volumes to add to the PodTemplate and adds volumeMounts to the containers and initContainers + volumes, err := a.buildVolumes(containers, initContainers) if err != nil { return nil, false, err } - allVolumes = append(allVolumes, ephemeralVolumes...) - - odoMandatoryVolumes := utils.GetOdoContainerVolumes(odoSourcePVCName) - allVolumes = append(allVolumes, odoMandatoryVolumes...) + podTemplateSpec.Spec.Volumes = volumes selectorLabels := map[string]string{ "component": componentName, } - podTemplateSpec.Spec.Volumes = allVolumes - deployParams := generator.DeploymentParams{ TypeMeta: generator.GetTypeMeta(kclient.DeploymentKind, kclient.DeploymentAPIVersion), ObjectMeta: deploymentObjectMeta, @@ -577,6 +534,80 @@ func (a *Adapter) createOrUpdateComponent( return deployment, newGeneration != originalGeneration, nil } +// buildVolumes: +// - (side effect on cluster) creates the PVC for the project sources if Epehemeral preference is false +// - (side effect on cluster) creates the PVCs for non-ephemeral volumes defined in the Devfile +// - (side effect on input parameters) adds volumeMounts to containers and initContainers for the PVCs and Ephemeral volumes +// - (side effect on input parameters) adds volumeMounts for automounted volumes +// => Returns the list of Volumes to add to the PodTemplate +func (a *Adapter) buildVolumes(containers, initContainers []corev1.Container) ([]corev1.Volume, error) { + + runtime := component.GetComponentRuntimeFromDevfileMetadata(a.Devfile.Data.GetMetadata()) + + storageClient := storagepkg.NewClient(a.ComponentName, a.AppName, storagepkg.ClientOptions{ + Client: a.kubeClient, + Runtime: runtime, + }) + + // Create the PVC for the project sources, if not ephemeral + err := storage.HandleOdoSourceStorage(a.kubeClient, storageClient, a.ComponentName, a.prefClient.GetEphemeralSourceVolume()) + if err != nil { + return nil, err + } + + // Create PVCs for non-ephemeral Volumes defined in the Devfile + // and returns the Ephemeral volumes defined in the Devfile + ephemerals, err := storagepkg.Push(storageClient, a.Devfile) + if err != nil { + return nil, err + } + + // get all the PVCs from the cluster belonging to the component + // These PVCs have been created earlier with `storage.HandleOdoSourceStorage` and `storagepkg.Push` + pvcs, err := a.kubeClient.ListPVCs(fmt.Sprintf("%v=%v", "component", a.ComponentName)) + if err != nil { + return nil, err + } + + var allVolumes []corev1.Volume + + // Get the name of the PVC for project sources + a map of (storageName => VolumeInfo) + // odoSourcePVCName will be empty when Ephemeral preference is true + odoSourcePVCName, volumeNameToVolInfo, err := storage.GetVolumeInfos(pvcs) + if err != nil { + return nil, err + } + + // Add the volumes for the projects source and the Odo-specific directory + odoMandatoryVolumes := utils.GetOdoContainerVolumes(odoSourcePVCName) + allVolumes = append(allVolumes, odoMandatoryVolumes...) + + // Add the volumeMounts for the project sources volume and the Odo-specific volume into the containers + utils.AddOdoProjectVolume(containers) + utils.AddOdoMandatoryVolume(containers) + + // Get PVC volumes and Volume Mounts + pvcVolumes, err := storage.GetPersistentVolumesAndVolumeMounts(a.Devfile, containers, initContainers, volumeNameToVolInfo, parsercommon.DevfileOptions{}) + if err != nil { + return nil, err + } + allVolumes = append(allVolumes, pvcVolumes...) + + ephemeralVolumes, err := storage.GetEphemeralVolumesAndVolumeMounts(a.Devfile, containers, initContainers, ephemerals, parsercommon.DevfileOptions{}) + if err != nil { + return nil, err + } + allVolumes = append(allVolumes, ephemeralVolumes...) + + automountVolumes, err := storage.GetAutomountVolumes(a.configAutomountClient, containers, initContainers) + if err != nil { + return nil, err + } + allVolumes = append(allVolumes, automountVolumes...) + + return allVolumes, nil +} + func (a *Adapter) createOrUpdateServiceForComponent(svc *corev1.Service, componentName string, ownerReference metav1.OwnerReference) error { oldSvc, err := a.kubeClient.GetOneService(a.ComponentName, a.AppName, true) originOwnerReferences := svc.OwnerReferences diff --git a/pkg/devfile/adapters/kubernetes/component/adapter_test.go b/pkg/devfile/adapters/kubernetes/component/adapter_test.go index 3607997f4cd..d987508b6f8 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter_test.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter_test.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/libdevfile" "github.com/redhat-developer/odo/pkg/preference" "github.com/redhat-developer/odo/pkg/util" @@ -130,8 +131,10 @@ func TestCreateOrUpdateComponent(t *testing.T) { }) ctrl := gomock.NewController(t) fakePrefClient := preference.NewMockClient(ctrl) - fakePrefClient.EXPECT().GetEphemeralSourceVolume() - componentAdapter := NewKubernetesAdapter(fkclient, fakePrefClient, nil, nil, nil, nil, adapterCtx) + fakePrefClient.EXPECT().GetEphemeralSourceVolume().AnyTimes() + fakeConfigAutomount := configAutomount.NewMockClient(ctrl) + fakeConfigAutomount.EXPECT().GetAutomountingVolumes().AnyTimes() + componentAdapter := NewKubernetesAdapter(fkclient, fakePrefClient, nil, nil, nil, nil, fakeConfigAutomount, adapterCtx) _, _, err := componentAdapter.createOrUpdateComponent(tt.running, libdevfile.DevfileCommands{}, nil) // Checks for unexpected error cases diff --git a/pkg/devfile/adapters/kubernetes/storage/utils.go b/pkg/devfile/adapters/kubernetes/storage/utils.go index 4bf0810a9a0..88867adf723 100644 --- a/pkg/devfile/adapters/kubernetes/storage/utils.go +++ b/pkg/devfile/adapters/kubernetes/storage/utils.go @@ -2,6 +2,7 @@ package storage import ( "fmt" + "path/filepath" "sort" "strings" @@ -10,10 +11,10 @@ import ( parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" dfutil "github.com/devfile/library/v2/pkg/util" + "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/kclient" odolabels "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/pkg/storage" - storagepkg "github.com/redhat-developer/odo/pkg/storage" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -43,7 +44,7 @@ func GetVolumeInfos(pvcs []corev1.PersistentVolumeClaim) (odoSourcePVCName strin } storageName := odolabels.GetStorageName(pvc.Labels) - if storageName == storagepkg.OdoSourceVolume { + if storageName == storage.OdoSourceVolume { odoSourcePVCName = pvc.Name continue } @@ -204,8 +205,10 @@ func generateVolumeNameFromPVC(pvc string) (volumeName string, err error) { return } -// HandleEphemeralStorage creates or deletes the ephemeral volume based on the preference setting -func HandleEphemeralStorage(client kclient.ClientInterface, storageClient storage.Client, componentName string, isEphemeral bool) error { +// HandleOdoSourceStorage creates or deletes the volume containing project sources, based on the preference setting +// - if Ephemeral preference is true, any PVC with labels "component=..." and "odo-source-pvc=odo-projects" is removed +// - if Ephemeral preference is false and no PVC with matching labels exists, it is created +func HandleOdoSourceStorage(client kclient.ClientInterface, storageClient storage.Client, componentName string, isEphemeral bool) error { selector := odolabels.Builder().WithComponentName(componentName).WithSourcePVC(storage.OdoSourceVolume).Selector() pvcs, err := client.ListPVCs(selector) if err != nil && !kerrors.IsNotFound(err) { @@ -241,3 +244,210 @@ func HandleEphemeralStorage(client kclient.ClientInterface, storageClient storag } return nil } + +func GetAutomountVolumes(configAutomountClient configAutomount.Client, containers, initContainers []corev1.Container) ([]corev1.Volume, error) { + volumesInfos, err := configAutomountClient.GetAutomountingVolumes() + if err != nil { + return nil, err + } + + var volumes []corev1.Volume + for _, volumeInfo := range volumesInfos { + switch volumeInfo.VolumeType { + case configAutomount.VolumeTypePVC: + volumes = mountPVC(volumeInfo, containers, initContainers, volumes) + case configAutomount.VolumeTypeSecret: + volumes = mountSecret(volumeInfo, containers, initContainers, volumes) + case configAutomount.VolumeTypeConfigmap: + volumes = mountConfigMap(volumeInfo, containers, initContainers, volumes) + } + } + return volumes, nil +} + +func mountPVC(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + volumeName := "auto-pvc-" + volumeInfo.VolumeName + + inAllContainers(containers, initContainers, func(container *corev1.Container) { + addVolumeMountToContainer(container, corev1.VolumeMount{ + Name: volumeName, + MountPath: volumeInfo.MountPath, + ReadOnly: volumeInfo.ReadOnly, + }) + }) + + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: volumeInfo.VolumeName, + }, + }, + }) + return volumes +} + +func mountSecret(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + switch volumeInfo.MountAs { + case configAutomount.MountAsFile: + return mountSecretAsFile(volumeInfo, containers, initContainers, volumes) + case configAutomount.MountAsEnv: + return mountSecretAsEnv(volumeInfo, containers, initContainers, volumes) + case configAutomount.MountAsSubpath: + return mountSecretAsSubpath(volumeInfo, containers, initContainers, volumes) + } + return volumes +} + +func mountSecretAsFile(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + volumeName := "auto-secret-" + volumeInfo.VolumeName + + inAllContainers(containers, initContainers, func(container *corev1.Container) { + addVolumeMountToContainer(container, corev1.VolumeMount{ + Name: volumeName, + MountPath: volumeInfo.MountPath, + ReadOnly: volumeInfo.ReadOnly, + }) + }) + + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: volumeInfo.VolumeName, + }, + }, + }) + return volumes +} + +func mountSecretAsEnv(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + inAllContainers(containers, initContainers, func(container *corev1.Container) { + addEnvFromToContainer(container, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: volumeInfo.VolumeName, + }, + }, + }) + }) + return volumes +} + +func mountSecretAsSubpath(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + volumeName := "auto-secret-" + volumeInfo.VolumeName + + inAllContainers(containers, initContainers, func(container *corev1.Container) { + for _, key := range volumeInfo.Keys { + addVolumeMountToContainer(container, corev1.VolumeMount{ + Name: volumeName, + MountPath: filepath.ToSlash(filepath.Join(volumeInfo.MountPath, key)), + SubPath: key, + ReadOnly: volumeInfo.ReadOnly, + }) + } + }) + + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: volumeInfo.VolumeName, + }, + }, + }) + return volumes +} + +func mountConfigMap(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + switch volumeInfo.MountAs { + case configAutomount.MountAsFile: + return mountConfigMapAsFile(volumeInfo, containers, initContainers, volumes) + case configAutomount.MountAsEnv: + return mountConfigMapAsEnv(volumeInfo, containers, initContainers, volumes) + case configAutomount.MountAsSubpath: + return mountConfigMapAsSubpath(volumeInfo, containers, initContainers, volumes) + } + return volumes +} + +func mountConfigMapAsFile(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + volumeName := "auto-cm-" + volumeInfo.VolumeName + + inAllContainers(containers, initContainers, func(container *corev1.Container) { + addVolumeMountToContainer(container, corev1.VolumeMount{ + Name: volumeName, + MountPath: volumeInfo.MountPath, + ReadOnly: volumeInfo.ReadOnly, + }) + }) + + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: volumeInfo.VolumeName, + }, + }, + }, + }) + return volumes +} + +func mountConfigMapAsEnv(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + inAllContainers(containers, initContainers, func(container *corev1.Container) { + addEnvFromToContainer(container, corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: volumeInfo.VolumeName, + }, + }, + }) + }) + return volumes +} + +func mountConfigMapAsSubpath(volumeInfo configAutomount.AutomountInfo, containers, initContainers []corev1.Container, volumes []corev1.Volume) []corev1.Volume { + volumeName := "auto-cm-" + volumeInfo.VolumeName + + inAllContainers(containers, initContainers, func(container *corev1.Container) { + for _, key := range volumeInfo.Keys { + addVolumeMountToContainer(container, corev1.VolumeMount{ + Name: volumeName, + MountPath: filepath.ToSlash(filepath.Join(volumeInfo.MountPath, key)), + SubPath: key, + ReadOnly: volumeInfo.ReadOnly, + }) + } + }) + + volumes = append(volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: volumeInfo.VolumeName, + }, + }, + }, + }) + return volumes +} + +func inAllContainers(containers, initContainers []corev1.Container, f func(container *corev1.Container)) { + for i := range containers { + f(&containers[i]) + } + for i := range initContainers { + f(&initContainers[i]) + } +} + +func addVolumeMountToContainer(container *corev1.Container, volumeMount corev1.VolumeMount) { + container.VolumeMounts = append(container.VolumeMounts, volumeMount) +} + +func addEnvFromToContainer(container *corev1.Container, envFrom corev1.EnvFromSource) { + container.EnvFrom = append(container.EnvFrom, envFrom) +} diff --git a/pkg/devfile/adapters/kubernetes/storage/utils_test.go b/pkg/devfile/adapters/kubernetes/storage/utils_test.go index 01e4c54304f..13d360c0fd6 100644 --- a/pkg/devfile/adapters/kubernetes/storage/utils_test.go +++ b/pkg/devfile/adapters/kubernetes/storage/utils_test.go @@ -1,6 +1,7 @@ package storage import ( + "fmt" "testing" devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -9,12 +10,13 @@ import ( devfileParser "github.com/devfile/library/v2/pkg/devfile/parser" "github.com/devfile/library/v2/pkg/devfile/parser/data" parsercommon "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" + "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" + "github.com/redhat-developer/odo/pkg/configAutomount" + "github.com/redhat-developer/odo/pkg/testingutil" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/redhat-developer/odo/pkg/testingutil" ) func TestGetPVC(t *testing.T) { @@ -472,3 +474,355 @@ func TestGetVolumesAndVolumeMounts(t *testing.T) { }) } } + +func TestGetAutomountVolumes(t *testing.T) { + + container1 := corev1.Container{ + Name: "container1", + Image: "image1", + } + container2 := corev1.Container{ + Name: "container2", + Image: "image2", + } + initContainer1 := corev1.Container{ + Name: "initContainer1", + Image: "image1", + } + initContainer2 := corev1.Container{ + Name: "initContainer2", + Image: "image2", + } + + type args struct { + configAutomountClient func(ctrl *gomock.Controller) configAutomount.Client + containers []corev1.Container + initContainers []corev1.Container + } + tests := []struct { + name string + args args + want []corev1.Volume + wantVolumeMounts []corev1.VolumeMount + wantEnvFroms []corev1.EnvFromSource + wantErr bool + }{ + { + name: "No automounting volume", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: nil, + wantVolumeMounts: nil, + wantErr: false, + }, + { + name: "One PVC", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + info1 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypePVC, + VolumeName: "pvc1", + MountPath: "/path/to/mount1", + MountAs: configAutomount.MountAsFile, + } + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{info1}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: []v1.Volume{ + { + Name: "auto-pvc-pvc1", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc1", + }, + }, + }, + }, + wantVolumeMounts: []v1.VolumeMount{ + { + Name: "auto-pvc-pvc1", + MountPath: "/path/to/mount1", + }, + }, + wantErr: false, + }, + { + name: "One PVC and one secret", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + info1 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypePVC, + VolumeName: "pvc1", + MountPath: "/path/to/mount1", + MountAs: configAutomount.MountAsFile, + } + info2 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeSecret, + VolumeName: "secret2", + MountPath: "/path/to/mount2", + MountAs: configAutomount.MountAsFile, + } + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{info1, info2}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: []v1.Volume{ + { + Name: "auto-pvc-pvc1", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc1", + }, + }, + }, + { + Name: "auto-secret-secret2", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "secret2", + }, + }, + }, + }, + wantVolumeMounts: []v1.VolumeMount{ + { + Name: "auto-pvc-pvc1", + MountPath: "/path/to/mount1", + }, + { + Name: "auto-secret-secret2", + MountPath: "/path/to/mount2", + }, + }, + wantErr: false, + }, + { + name: "One PVC, one secret and one configmap", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + info1 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypePVC, + VolumeName: "pvc1", + MountPath: "/path/to/mount1", + MountAs: configAutomount.MountAsFile, + } + info2 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeSecret, + VolumeName: "secret2", + MountPath: "/path/to/mount2", + MountAs: configAutomount.MountAsFile, + } + info3 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeConfigmap, + VolumeName: "cm3", + MountPath: "/path/to/mount3", + MountAs: configAutomount.MountAsFile, + } + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{info1, info2, info3}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: []v1.Volume{ + { + Name: "auto-pvc-pvc1", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "pvc1", + }, + }, + }, + { + Name: "auto-secret-secret2", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "secret2", + }, + }, + }, + { + Name: "auto-cm-cm3", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "cm3", + }, + }, + }, + }, + }, + wantVolumeMounts: []v1.VolumeMount{ + { + Name: "auto-pvc-pvc1", + MountPath: "/path/to/mount1", + }, + { + Name: "auto-secret-secret2", + MountPath: "/path/to/mount2", + }, + { + Name: "auto-cm-cm3", + MountPath: "/path/to/mount3", + }, + }, + wantErr: false, + }, + { + name: "One secret and one configmap mounted as Env", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + info1 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeSecret, + VolumeName: "secret1", + MountAs: configAutomount.MountAsEnv, + } + info2 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeConfigmap, + VolumeName: "cm2", + MountAs: configAutomount.MountAsEnv, + } + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{info1, info2}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: nil, + wantVolumeMounts: nil, + wantEnvFroms: []corev1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "secret1", + }, + }, + }, + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "cm2", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "One secret and one configmap mounted as Subpath", + args: args{ + configAutomountClient: func(ctrl *gomock.Controller) configAutomount.Client { + info1 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeSecret, + VolumeName: "secret1", + MountPath: "/path/to/secret1", + MountAs: configAutomount.MountAsSubpath, + Keys: []string{"secretKey1", "secretKey2"}, + } + info2 := configAutomount.AutomountInfo{ + VolumeType: configAutomount.VolumeTypeConfigmap, + VolumeName: "cm2", + MountPath: "/path/to/cm2", + MountAs: configAutomount.MountAsSubpath, + Keys: []string{"cmKey1", "cmKey2"}, + } + client := configAutomount.NewMockClient(ctrl) + client.EXPECT().GetAutomountingVolumes().Return([]configAutomount.AutomountInfo{info1, info2}, nil) + return client + }, + containers: []corev1.Container{container1, container2}, + initContainers: []corev1.Container{initContainer1, initContainer2}, + }, + want: []corev1.Volume{ + { + Name: "auto-secret-secret1", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "secret1", + }, + }, + }, + { + Name: "auto-cm-cm2", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "cm2", + }, + }, + }, + }, + }, + wantVolumeMounts: []corev1.VolumeMount{ + { + Name: "auto-secret-secret1", + MountPath: "/path/to/secret1/secretKey1", + SubPath: "secretKey1", + }, + { + Name: "auto-secret-secret1", + MountPath: "/path/to/secret1/secretKey2", + SubPath: "secretKey2", + }, + { + Name: "auto-cm-cm2", + MountPath: "/path/to/cm2/cmKey1", + SubPath: "cmKey1", + }, + { + Name: "auto-cm-cm2", + MountPath: "/path/to/cm2/cmKey2", + SubPath: "cmKey2", + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + got, err := GetAutomountVolumes(tt.args.configAutomountClient(ctrl), tt.args.containers, tt.args.initContainers) + if (err != nil) != tt.wantErr { + t.Errorf("GetAutomountVolumes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("GetAutomountVolumes() mismatch (-want +got):\n%s", diff) + } + + checkContainers := func(containers, initContainers []corev1.Container) error { + allContainers := containers + allContainers = append(allContainers, initContainers...) + for _, container := range allContainers { + if diff := cmp.Diff(tt.wantVolumeMounts, container.VolumeMounts); diff != "" { + return fmt.Errorf(diff) + } + if diff := cmp.Diff(tt.wantEnvFroms, container.EnvFrom); diff != "" { + return fmt.Errorf(diff) + } + } + return nil + } + + if err := checkContainers(tt.args.containers, tt.args.initContainers); err != nil { + t.Errorf("GetAutomountVolumes() containers error: %v", err) + } + }) + } +} diff --git a/pkg/devfile/adapters/kubernetes/utils/utils.go b/pkg/devfile/adapters/kubernetes/utils/utils.go index ec95294b378..77a048d5b07 100644 --- a/pkg/devfile/adapters/kubernetes/utils/utils.go +++ b/pkg/devfile/adapters/kubernetes/utils/utils.go @@ -21,6 +21,7 @@ func GetOdoContainerVolumes(sourcePVCName string) []corev1.Volume { var sourceVolume corev1.Volume if sourcePVCName != "" { + // Define a Persistent volume using the found PVC volume source sourceVolume = corev1.Volume{ Name: storage.OdoSourceVolume, VolumeSource: corev1.VolumeSource{ @@ -28,8 +29,12 @@ func GetOdoContainerVolumes(sourcePVCName string) []corev1.Volume { }, } } else { + // Define an Ephemeral volume using an EmptyDir volume source sourceVolume = corev1.Volume{ Name: storage.OdoSourceVolume, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, } } @@ -47,35 +52,33 @@ func GetOdoContainerVolumes(sourcePVCName string) []corev1.Volume { } // AddOdoProjectVolume adds the odo project volume to the containers -func AddOdoProjectVolume(containers *[]corev1.Container) { +func AddOdoProjectVolume(containers []corev1.Container) { if containers == nil { return } - for i, container := range *containers { - for _, env := range container.Env { + for i := range containers { + for _, env := range containers[i].Env { if env.Name == _envProjectsRoot { - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + containers[i].VolumeMounts = append(containers[i].VolumeMounts, corev1.VolumeMount{ Name: storage.OdoSourceVolume, MountPath: env.Value, }) - (*containers)[i] = container } } } } // AddOdoMandatoryVolume adds the odo mandatory volumes to the containers -func AddOdoMandatoryVolume(containers *[]corev1.Container) { +func AddOdoMandatoryVolume(containers []corev1.Container) { if containers == nil { return } - for i, container := range *containers { - klog.V(2).Infof("Updating container %v with mandatory volume mounts", container.Name) - container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{ + for i := range containers { + klog.V(2).Infof("Updating container %v with mandatory volume mounts", containers[i].Name) + containers[i].VolumeMounts = append(containers[i].VolumeMounts, corev1.VolumeMount{ Name: storage.SharedDataVolumeName, MountPath: storage.SharedDataMountPath, }) - (*containers)[i] = container } } diff --git a/pkg/devfile/adapters/kubernetes/utils/utils_test.go b/pkg/devfile/adapters/kubernetes/utils/utils_test.go index d941a7f3eff..1a896d0beda 100644 --- a/pkg/devfile/adapters/kubernetes/utils/utils_test.go +++ b/pkg/devfile/adapters/kubernetes/utils/utils_test.go @@ -67,11 +67,7 @@ func TestAddOdoProjectVolume(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.containers == nil { - AddOdoProjectVolume(nil) - } else { - AddOdoProjectVolume(&tt.containers) - } + AddOdoProjectVolume(tt.containers) for wantContainerName, wantMountPath := range tt.volMount { matched := false @@ -180,11 +176,7 @@ func TestAddOdoMandatoryVolume(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - if tt.containers == nil { - AddOdoMandatoryVolume(nil) - } else { - AddOdoMandatoryVolume(&tt.containers) - } + AddOdoMandatoryVolume(tt.containers) for containerName, volMounts := range tt.wantVolumeMounts { c, ok := findContainerByName(tt.containers, containerName) diff --git a/pkg/kclient/configmap.go b/pkg/kclient/configmap.go new file mode 100644 index 00000000000..f05f82fd9ec --- /dev/null +++ b/pkg/kclient/configmap.go @@ -0,0 +1,26 @@ +package kclient + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ListConfigMaps lists all the configmaps based on the given label selector +func (c *Client) ListConfigMaps(labelSelector string) ([]corev1.ConfigMap, error) { + listOptions := metav1.ListOptions{} + if len(labelSelector) > 0 { + listOptions = metav1.ListOptions{ + LabelSelector: labelSelector, + } + } + + cmList, err := c.KubeClient.CoreV1().ConfigMaps(c.Namespace).List(context.TODO(), listOptions) + if err != nil { + return nil, fmt.Errorf("unable to get configmap list: %w", err) + } + + return cmList.Items, nil +} diff --git a/pkg/kclient/interface.go b/pkg/kclient/interface.go index 1bf53c6f198..1849f90f881 100644 --- a/pkg/kclient/interface.go +++ b/pkg/kclient/interface.go @@ -43,6 +43,9 @@ type ClientInterface interface { NewServiceBindingServiceObject(serviceNs string, unstructuredService unstructured.Unstructured, bindingName string) (bindingApi.Service, error) GetWorkloadKinds() ([]string, []schema.GroupVersionKind, error) + // configmap.go + ListConfigMaps(labelSelector string) ([]corev1.ConfigMap, error) + // deployment.go GetDeploymentByName(name string) (*appsv1.Deployment, error) GetOneDeployment(componentName, appName string, isPartOfComponent bool) (*appsv1.Deployment, error) diff --git a/pkg/kclient/mock_Client.go b/pkg/kclient/mock_Client.go index d12be1e2447..779f5a73ef4 100644 --- a/pkg/kclient/mock_Client.go +++ b/pkg/kclient/mock_Client.go @@ -1118,6 +1118,21 @@ func (mr *MockClientInterfaceMockRecorder) ListClusterServiceVersions() *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListClusterServiceVersions", reflect.TypeOf((*MockClientInterface)(nil).ListClusterServiceVersions)) } +// ListConfigMaps mocks base method. +func (m *MockClientInterface) ListConfigMaps(labelSelector string) ([]v12.ConfigMap, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListConfigMaps", labelSelector) + ret0, _ := ret[0].([]v12.ConfigMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListConfigMaps indicates an expected call of ListConfigMaps. +func (mr *MockClientInterfaceMockRecorder) ListConfigMaps(labelSelector interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListConfigMaps", reflect.TypeOf((*MockClientInterface)(nil).ListConfigMaps), labelSelector) +} + // ListDynamicResources mocks base method. func (m *MockClientInterface) ListDynamicResources(namespace string, gvr schema.GroupVersionResource, selector string) (*unstructured.UnstructuredList, error) { m.ctrl.T.Helper() diff --git a/pkg/odo/genericclioptions/clientset/clientset.go b/pkg/odo/genericclioptions/clientset/clientset.go index b7d39c453e4..fceb428adc4 100644 --- a/pkg/odo/genericclioptions/clientset/clientset.go +++ b/pkg/odo/genericclioptions/clientset/clientset.go @@ -14,6 +14,7 @@ package clientset import ( "github.com/spf13/cobra" + "github.com/redhat-developer/odo/pkg/configAutomount" "github.com/redhat-developer/odo/pkg/dev/kubedev" "github.com/redhat-developer/odo/pkg/dev/podmandev" "github.com/redhat-developer/odo/pkg/exec" @@ -46,6 +47,8 @@ const ( ALIZER = "DEP_ALIZER" // BINDING instantiates client for pkg/binding BINDING = "DEP_BINDING" + // CONFIG_AUTOMOUNT instantiates client for pkg/configAutomount + CONFIG_AUTOMOUNT = "DEP_CONFIG_AUTOMOUNT" // DELETE_COMPONENT instantiates client for pkg/component/delete DELETE_COMPONENT = "DEP_DELETE_COMPONENT" // DEPLOY instantiates client for pkg/deploy @@ -89,41 +92,56 @@ const ( // Clients will be created only once and be reused for sub-dependencies var subdeps map[string][]string = map[string][]string{ ALIZER: {REGISTRY}, + CONFIG_AUTOMOUNT: {KUBERNETES_NULLABLE, PODMAN_NULLABLE}, DELETE_COMPONENT: {KUBERNETES_NULLABLE, PODMAN_NULLABLE, EXEC}, - DEPLOY: {KUBERNETES, FILESYSTEM}, - DEV: {BINDING, DELETE_COMPONENT, EXEC, FILESYSTEM, KUBERNETES_NULLABLE, PODMAN_NULLABLE, PORT_FORWARD, PREFERENCE, STATE, SYNC, WATCH}, - EXEC: {KUBERNETES_NULLABLE, PODMAN_NULLABLE}, - INIT: {ALIZER, FILESYSTEM, PREFERENCE, REGISTRY}, - LOGS: {KUBERNETES_NULLABLE, PODMAN_NULLABLE}, - PORT_FORWARD: {KUBERNETES_NULLABLE, EXEC, STATE}, - PROJECT: {KUBERNETES}, - REGISTRY: {FILESYSTEM, PREFERENCE, KUBERNETES_NULLABLE}, - STATE: {FILESYSTEM}, - SYNC: {EXEC}, - WATCH: {KUBERNETES_NULLABLE}, - BINDING: {PROJECT, KUBERNETES_NULLABLE}, + DEPLOY: {KUBERNETES, FILESYSTEM, CONFIG_AUTOMOUNT}, + DEV: { + BINDING, + DELETE_COMPONENT, + CONFIG_AUTOMOUNT, + EXEC, + FILESYSTEM, + KUBERNETES_NULLABLE, + PODMAN_NULLABLE, + PORT_FORWARD, + PREFERENCE, + STATE, + SYNC, + WATCH, + }, + EXEC: {KUBERNETES_NULLABLE, PODMAN_NULLABLE}, + INIT: {ALIZER, FILESYSTEM, PREFERENCE, REGISTRY}, + LOGS: {KUBERNETES_NULLABLE, PODMAN_NULLABLE}, + PORT_FORWARD: {KUBERNETES_NULLABLE, EXEC, STATE}, + PROJECT: {KUBERNETES}, + REGISTRY: {FILESYSTEM, PREFERENCE, KUBERNETES_NULLABLE}, + STATE: {FILESYSTEM}, + SYNC: {EXEC}, + WATCH: {KUBERNETES_NULLABLE}, + BINDING: {PROJECT, KUBERNETES_NULLABLE}, /* Add sub-dependencies here, if any */ } type Clientset struct { - AlizerClient alizer.Client - BindingClient binding.Client - DeleteClient _delete.Client - DeployClient deploy.Client - DevClient dev.Client - ExecClient exec.Client - FS filesystem.Filesystem - InitClient _init.Client - KubernetesClient kclient.ClientInterface - LogsClient logs.Client - PodmanClient podman.Client - PortForwardClient portForward.Client - PreferenceClient preference.Client - ProjectClient project.Client - RegistryClient registry.Client - StateClient state.Client - SyncClient sync.Client - WatchClient watch.Client + AlizerClient alizer.Client + BindingClient binding.Client + ConfigAutomountClient configAutomount.Client + DeleteClient _delete.Client + DeployClient deploy.Client + DevClient dev.Client + ExecClient exec.Client + FS filesystem.Filesystem + InitClient _init.Client + KubernetesClient kclient.ClientInterface + LogsClient logs.Client + PodmanClient podman.Client + PortForwardClient portForward.Client + PreferenceClient preference.Client + ProjectClient project.Client + RegistryClient registry.Client + StateClient state.Client + SyncClient sync.Client + WatchClient watch.Client /* Add client by alphabetic order */ } @@ -200,11 +218,19 @@ func Fetch(command *cobra.Command, platform string) (*Clientset, error) { dep.ExecClient = exec.NewExecClient(dep.KubernetesClient) } } + if isDefined(command, CONFIG_AUTOMOUNT) { + switch platform { + case commonflags.PlatformPodman: + dep.ConfigAutomountClient = nil // Not supported + default: + dep.ConfigAutomountClient = configAutomount.NewKubernetesClient(dep.KubernetesClient) + } + } if isDefined(command, DELETE_COMPONENT) { dep.DeleteClient = _delete.NewDeleteComponentClient(dep.KubernetesClient, dep.PodmanClient, dep.ExecClient) } if isDefined(command, DEPLOY) { - dep.DeployClient = deploy.NewDeployClient(dep.KubernetesClient, dep.FS) + dep.DeployClient = deploy.NewDeployClient(dep.KubernetesClient, dep.ConfigAutomountClient, dep.FS) } if isDefined(command, INIT) { dep.InitClient = _init.NewInitClient(dep.FS, dep.PreferenceClient, dep.RegistryClient, dep.AlizerClient) @@ -268,6 +294,7 @@ func Fetch(command *cobra.Command, platform string) (*Clientset, error) { dep.FS, dep.ExecClient, dep.DeleteClient, + dep.ConfigAutomountClient, ) } } diff --git a/pkg/storage/kubernetes.go b/pkg/storage/kubernetes.go index d43235097fe..85c34f8601f 100644 --- a/pkg/storage/kubernetes.go +++ b/pkg/storage/kubernetes.go @@ -114,11 +114,16 @@ func (k kubernetesClient) List() (StorageList, error) { for _, container := range k.deployment.Spec.Template.Spec.Containers { for _, volumeMount := range container.VolumeMounts { - // avoid the volume mounts only from the init containers - // and the source volume mount + // avoid the volume mounts: + // - only from the init containers + // - of source volume + // - of automounted volumes _, initOK := initContainerVolumeMounts[volumeMount.Name] _, ok := containerVolumeMounts[volumeMount.Name] - if (!ok && initOK) || volumeMount.Name == OdoSourceVolume || volumeMount.Name == SharedDataVolumeName { + if (!ok && initOK) || + volumeMount.Name == OdoSourceVolume || + volumeMount.Name == SharedDataVolumeName || + strings.HasPrefix(volumeMount.Name, "auto-") { continue } diff --git a/scripts/mockgen.sh b/scripts/mockgen.sh index 9bcc9373304..7509516ccd3 100755 --- a/scripts/mockgen.sh +++ b/scripts/mockgen.sh @@ -95,3 +95,7 @@ $mockgen -source=pkg/exec/interface.go \ $mockgen -source=pkg/podman/interface.go \ -package podman \ -destination pkg/podman/mock.go + +$mockgen -source=pkg/configAutomount/interface.go \ + -package configAutomount \ + -destination pkg/configAutomount/mock.go diff --git a/tests/examples/manifests/config-automount/as-env-configmap.yaml b/tests/examples/manifests/config-automount/as-env-configmap.yaml new file mode 100644 index 00000000000..23cd280c46d --- /dev/null +++ b/tests/examples/manifests/config-automount/as-env-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: automount-env-configmap + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-as: env +data: + foo4: bar4 + ping4: pong4 diff --git a/tests/examples/manifests/config-automount/as-env-secret.yaml b/tests/examples/manifests/config-automount/as-env-secret.yaml new file mode 100644 index 00000000000..28021f19cbf --- /dev/null +++ b/tests/examples/manifests/config-automount/as-env-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: automount-env-secret + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-as: env +stringData: + code4: "4567" + secret4: "PassWd4" diff --git a/tests/examples/manifests/config-automount/default-configmap.yaml b/tests/examples/manifests/config-automount/default-configmap.yaml new file mode 100644 index 00000000000..224e636c15b --- /dev/null +++ b/tests/examples/manifests/config-automount/default-configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: automount-default-configmap + labels: + devfile.io/auto-mount: "true" +data: + foo1: bar1 + ping1: pong1 diff --git a/tests/examples/manifests/config-automount/default-pvc.yaml b/tests/examples/manifests/config-automount/default-pvc.yaml new file mode 100644 index 00000000000..2d196d536a7 --- /dev/null +++ b/tests/examples/manifests/config-automount/default-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + devfile.io/auto-mount: "true" + name: automount-default-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/tests/examples/manifests/config-automount/default-secret.yaml b/tests/examples/manifests/config-automount/default-secret.yaml new file mode 100644 index 00000000000..6a6ee6bc550 --- /dev/null +++ b/tests/examples/manifests/config-automount/default-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: automount-default-secret + labels: + devfile.io/auto-mount: "true" +stringData: + code1: "1234" + secret1: "PassWd1" diff --git a/tests/examples/manifests/config-automount/mount-path-configmap.yaml b/tests/examples/manifests/config-automount/mount-path-configmap.yaml new file mode 100644 index 00000000000..1a04c869db6 --- /dev/null +++ b/tests/examples/manifests/config-automount/mount-path-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: automount-mount-path-configmap + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-path: "/mnt/mount-path/configmap" +data: + foo2: bar2 + ping2: pong2 diff --git a/tests/examples/manifests/config-automount/mount-path-pvc.yaml b/tests/examples/manifests/config-automount/mount-path-pvc.yaml new file mode 100644 index 00000000000..157c5ff8701 --- /dev/null +++ b/tests/examples/manifests/config-automount/mount-path-pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-path: "/mnt/mount-path/pvc" + name: automount-mount-path-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/tests/examples/manifests/config-automount/mount-path-secret.yaml b/tests/examples/manifests/config-automount/mount-path-secret.yaml new file mode 100644 index 00000000000..f812bc51365 --- /dev/null +++ b/tests/examples/manifests/config-automount/mount-path-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: automount-mount-path-secret + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-path: "/mnt/mount-path/secret" +stringData: + code2: "2345" + secret2: "PassWd2" diff --git a/tests/examples/manifests/config-automount/readonly-pvc.yaml b/tests/examples/manifests/config-automount/readonly-pvc.yaml new file mode 100644 index 00000000000..a368bfd872f --- /dev/null +++ b/tests/examples/manifests/config-automount/readonly-pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/read-only: "true" + name: automount-readonly-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi diff --git a/tests/examples/manifests/config-automount/subpath-configmap.yaml b/tests/examples/manifests/config-automount/subpath-configmap.yaml new file mode 100644 index 00000000000..a2d446c6b86 --- /dev/null +++ b/tests/examples/manifests/config-automount/subpath-configmap.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: automount-subpath-configmap + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-path: "/mnt/subpaths" + devfile.io/mount-as: subpath +data: + foo5: bar5 + ping5: pong5 diff --git a/tests/examples/manifests/config-automount/subpath-secret.yaml b/tests/examples/manifests/config-automount/subpath-secret.yaml new file mode 100644 index 00000000000..c0e56a3f053 --- /dev/null +++ b/tests/examples/manifests/config-automount/subpath-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: automount-subpath-secret + labels: + devfile.io/auto-mount: "true" + annotations: + devfile.io/mount-path: "/mnt/subpaths" + devfile.io/mount-as: subpath +stringData: + code5: "5678" + secret5: "PassWd5" diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index 2b034cf2e07..9477ef0a254 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -596,6 +596,96 @@ ComponentSettings: helper.MatchAllInOutput(string(body), []string{"Hello from Node.js Starter Application!"}) }) }) + + When("Automount volumes are present in the namespace", func() { + + BeforeEach(func() { + commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("manifests", "config-automount/")) + }) + + When("odo dev is executed", func() { + + var devSession helper.DevSession + + BeforeEach(func() { + var err error + devSession, _, _, _, err = helper.StartDevMode(helper.DevSessionOpts{}) + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should mount the volumes", func() { + component := helper.NewComponent(cmpName, "app", labels.ComponentDevMode, commonVar.Project, commonVar.CliRunner) + + // Check volumes are mounted + for _, path := range []string{ + "/tmp/automount-default-pvc", + "/etc/config/automount-default-configmap", + "/etc/secret/automount-default-secret", + + "/tmp/automount-readonly-pvc", + + "/mnt/mount-path/pvc", + "/mnt/mount-path/configmap", + "/mnt/mount-path/secret", + } { + var output string + Eventually(func() bool { + output, _ = component.Exec("runtime", []string{"df", path}, nil) + return len(output) > 0 + }).WithPolling(1 * time.Second).WithTimeout(60 * time.Second).Should(BeTrue()) + // This checks this is really a mount + Expect(output).ToNot(ContainSubstring("overlay")) + } + + // Check files are present for configmap / secret + files := map[string]string{ + "/etc/config/automount-default-configmap/foo1": "bar1", + "/etc/config/automount-default-configmap/ping1": "pong1", + "/etc/secret/automount-default-secret/code1": "1234", + "/etc/secret/automount-default-secret/secret1": "PassWd1", + + "/mnt/mount-path/configmap/foo2": "bar2", + "/mnt/mount-path/configmap/ping2": "pong2", + "/mnt/mount-path/secret/code2": "2345", + "/mnt/mount-path/secret/secret2": "PassWd2", + + "/mnt/subpaths/foo5": "bar5", + "/mnt/subpaths/ping5": "pong5", + "/mnt/subpaths/code5": "5678", + "/mnt/subpaths/secret5": "PassWd5", + } + for file, content := range files { + output, _ := component.Exec("runtime", []string{"cat", file}, pointer.Bool(true)) + Expect(output).To(Equal(content)) + } + + envVars := map[string]string{ + "foo4": "bar4", + "ping4": "pong4", + + "code4": "4567", + "secret4": "PassWd4", + } + for name, value := range envVars { + output, _ := component.Exec("runtime", []string{"bash", "-c", "echo -n $" + name}, pointer.Bool(true)) + Expect(output).To(Equal(value)) + } + + // Default PVC is not read-only + component.Exec("runtime", []string{"touch", "/tmp/automount-default-pvc/newfile"}, pointer.Bool(true)) + + // Read-only PVC is read-only + _, stderr := component.Exec("runtime", []string{"touch", "/tmp/automount-readonly-pvc/newfile"}, pointer.Bool(false)) + Expect(stderr).To(ContainSubstring("Read-only file system")) + + }) + }) + }) }) Context("checking if odo dev matches local Devfile K8s resources and remote resources", func() { for _, devfile := range []struct { diff --git a/tests/integration/cmd_devfile_deploy_test.go b/tests/integration/cmd_devfile_deploy_test.go index 88c5083dddf..c8c231e9df7 100644 --- a/tests/integration/cmd_devfile_deploy_test.go +++ b/tests/integration/cmd_devfile_deploy_test.go @@ -653,6 +653,23 @@ CMD ["npm", "start"] }) }) }) + + When("Automount volumes are present in the namespace", func() { + + BeforeEach(func() { + commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("manifests", "config-automount/")) + }) + + It("should mount the volumes", func() { + helper.Cmd("odo", "deploy").Should(func(session *gexec.Session) { + component := helper.NewComponent(cmpName, "app", labels.ComponentDeployMode, commonVar.Project, commonVar.CliRunner) + jobDef := component.GetJobDef() + // We only check that at least one volume is automounted + // More tests are executed on `odo dev`, see "Automount volumes are present in the namespace" on odo dev tests. + Expect(jobDef.Spec.Template.Spec.Volumes[0].Name).To(Equal("auto-pvc-automount-default-pvc")) + }) + }) + }) }) // More details on https://github.com/devfile/api/issues/852#issuecomment-1211928487