1/*
2Copyright 2018 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package csi
18
19import (
20	"context"
21	"encoding/json"
22	"errors"
23	"fmt"
24	"os"
25	"path/filepath"
26	"strconv"
27	"time"
28
29	api "k8s.io/api/core/v1"
30	storage "k8s.io/api/storage/v1"
31	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
32	utilfeature "k8s.io/apiserver/pkg/util/feature"
33	"k8s.io/client-go/kubernetes"
34	"k8s.io/klog/v2"
35	"k8s.io/kubernetes/pkg/features"
36	"k8s.io/kubernetes/pkg/volume"
37	utilstrings "k8s.io/utils/strings"
38)
39
40const (
41	// TestInformerSyncPeriod is informer sync period duration for testing
42	TestInformerSyncPeriod = 100 * time.Millisecond
43	// TestInformerSyncTimeout is informer timeout duration for testing
44	TestInformerSyncTimeout = 30 * time.Second
45)
46
47func getCredentialsFromSecret(k8s kubernetes.Interface, secretRef *api.SecretReference) (map[string]string, error) {
48	credentials := map[string]string{}
49	secret, err := k8s.CoreV1().Secrets(secretRef.Namespace).Get(context.TODO(), secretRef.Name, meta.GetOptions{})
50	if err != nil {
51		return credentials, errors.New(log("failed to find the secret %s in the namespace %s with error: %v", secretRef.Name, secretRef.Namespace, err))
52	}
53	for key, value := range secret.Data {
54		credentials[key] = string(value)
55	}
56
57	return credentials, nil
58}
59
60// saveVolumeData persists parameter data as json file at the provided location
61func saveVolumeData(dir string, fileName string, data map[string]string) error {
62	dataFilePath := filepath.Join(dir, fileName)
63	klog.V(4).Info(log("saving volume data file [%s]", dataFilePath))
64	file, err := os.Create(dataFilePath)
65	if err != nil {
66		return errors.New(log("failed to save volume data file %s: %v", dataFilePath, err))
67	}
68	defer file.Close()
69	if err := json.NewEncoder(file).Encode(data); err != nil {
70		return errors.New(log("failed to save volume data file %s: %v", dataFilePath, err))
71	}
72	klog.V(4).Info(log("volume data file saved successfully [%s]", dataFilePath))
73	return nil
74}
75
76// loadVolumeData loads volume info from specified json file/location
77func loadVolumeData(dir string, fileName string) (map[string]string, error) {
78	// remove /mount at the end
79	dataFileName := filepath.Join(dir, fileName)
80	klog.V(4).Info(log("loading volume data file [%s]", dataFileName))
81
82	file, err := os.Open(dataFileName)
83	if err != nil {
84		return nil, errors.New(log("failed to open volume data file [%s]: %v", dataFileName, err))
85	}
86	defer file.Close()
87	data := map[string]string{}
88	if err := json.NewDecoder(file).Decode(&data); err != nil {
89		return nil, errors.New(log("failed to parse volume data file [%s]: %v", dataFileName, err))
90	}
91
92	return data, nil
93}
94
95func getCSISourceFromSpec(spec *volume.Spec) (*api.CSIPersistentVolumeSource, error) {
96	return getPVSourceFromSpec(spec)
97}
98
99func getReadOnlyFromSpec(spec *volume.Spec) (bool, error) {
100	if spec.PersistentVolume != nil &&
101		spec.PersistentVolume.Spec.CSI != nil {
102		return spec.ReadOnly, nil
103	}
104
105	return false, fmt.Errorf("CSIPersistentVolumeSource not defined in spec")
106}
107
108// log prepends log string with `kubernetes.io/csi`
109func log(msg string, parts ...interface{}) string {
110	return fmt.Sprintf(fmt.Sprintf("%s: %s", CSIPluginName, msg), parts...)
111}
112
113// getVolumePluginDir returns the path where CSI plugin keeps metadata for given volume
114func getVolumePluginDir(specVolID string, host volume.VolumeHost) string {
115	sanitizedSpecVolID := utilstrings.EscapeQualifiedName(specVolID)
116	return filepath.Join(host.GetVolumeDevicePluginDir(CSIPluginName), sanitizedSpecVolID)
117}
118
119// getVolumeDevicePluginDir returns the path where the CSI plugin keeps the
120// symlink for a block device associated with a given specVolumeID.
121// path: plugins/kubernetes.io/csi/volumeDevices/{specVolumeID}/dev
122func getVolumeDevicePluginDir(specVolID string, host volume.VolumeHost) string {
123	return filepath.Join(getVolumePluginDir(specVolID, host), "dev")
124}
125
126// getVolumeDeviceDataDir returns the path where the CSI plugin keeps the
127// volume data for a block device associated with a given specVolumeID.
128// path: plugins/kubernetes.io/csi/volumeDevices/{specVolumeID}/data
129func getVolumeDeviceDataDir(specVolID string, host volume.VolumeHost) string {
130	return filepath.Join(getVolumePluginDir(specVolID, host), "data")
131}
132
133// hasReadWriteOnce returns true if modes contains v1.ReadWriteOnce
134func hasReadWriteOnce(modes []api.PersistentVolumeAccessMode) bool {
135	if modes == nil {
136		return false
137	}
138	for _, mode := range modes {
139		if mode == api.ReadWriteOnce {
140			return true
141		}
142	}
143	return false
144}
145
146// getSourceFromSpec returns either CSIVolumeSource or CSIPersistentVolumeSource, but not both
147func getSourceFromSpec(spec *volume.Spec) (*api.CSIVolumeSource, *api.CSIPersistentVolumeSource, error) {
148	if spec == nil {
149		return nil, nil, fmt.Errorf("volume.Spec nil")
150	}
151	if spec.Volume != nil && spec.PersistentVolume != nil {
152		return nil, nil, fmt.Errorf("volume.Spec has both volume and persistent volume sources")
153	}
154	if spec.Volume != nil && spec.Volume.CSI != nil && utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
155		return spec.Volume.CSI, nil, nil
156	}
157	if spec.PersistentVolume != nil &&
158		spec.PersistentVolume.Spec.CSI != nil {
159		return nil, spec.PersistentVolume.Spec.CSI, nil
160	}
161
162	return nil, nil, fmt.Errorf("volume source not found in volume.Spec")
163}
164
165// getPVSourceFromSpec ensures only CSIPersistentVolumeSource is present in volume.Spec
166func getPVSourceFromSpec(spec *volume.Spec) (*api.CSIPersistentVolumeSource, error) {
167	volSrc, pvSrc, err := getSourceFromSpec(spec)
168	if err != nil {
169		return nil, err
170	}
171	if volSrc != nil {
172		return nil, fmt.Errorf("unexpected api.CSIVolumeSource found in volume.Spec")
173	}
174	return pvSrc, nil
175}
176
177// GetCSIMounterPath returns the mounter path given the base path.
178func GetCSIMounterPath(path string) string {
179	return filepath.Join(path, "/mount")
180}
181
182// GetCSIDriverName returns the csi driver name
183func GetCSIDriverName(spec *volume.Spec) (string, error) {
184	volSrc, pvSrc, err := getSourceFromSpec(spec)
185	if err != nil {
186		return "", err
187	}
188
189	switch {
190	case volSrc != nil && utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume):
191		return volSrc.Driver, nil
192	case pvSrc != nil:
193		return pvSrc.Driver, nil
194	default:
195		return "", errors.New(log("volume source not found in volume.Spec"))
196	}
197}
198
199func createCSIOperationContext(volumeSpec *volume.Spec, timeout time.Duration) (context.Context, context.CancelFunc) {
200	migrated := false
201	if volumeSpec != nil {
202		migrated = volumeSpec.Migrated
203	}
204	ctx := context.WithValue(context.Background(), additionalInfoKey, additionalInfo{Migrated: strconv.FormatBool(migrated)})
205	return context.WithTimeout(ctx, timeout)
206}
207
208// getPodInfoAttrs returns pod info for NodePublish
209func getPodInfoAttrs(pod *api.Pod, volumeMode storage.VolumeLifecycleMode) map[string]string {
210	attrs := map[string]string{
211		"csi.storage.k8s.io/pod.name":            pod.Name,
212		"csi.storage.k8s.io/pod.namespace":       pod.Namespace,
213		"csi.storage.k8s.io/pod.uid":             string(pod.UID),
214		"csi.storage.k8s.io/serviceAccount.name": pod.Spec.ServiceAccountName,
215	}
216	if utilfeature.DefaultFeatureGate.Enabled(features.CSIInlineVolume) {
217		attrs["csi.storage.k8s.io/ephemeral"] = strconv.FormatBool(volumeMode == storage.VolumeLifecycleEphemeral)
218	}
219	return attrs
220}
221