1/*
2Copyright 2019 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 plugins
18
19import (
20	"fmt"
21	"regexp"
22	"strings"
23
24	v1 "k8s.io/api/core/v1"
25	storage "k8s.io/api/storage/v1"
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/klog/v2"
28)
29
30const (
31	// AzureFileDriverName is the name of the CSI driver for Azure File
32	AzureFileDriverName = "file.csi.azure.com"
33	// AzureFileInTreePluginName is the name of the intree plugin for Azure file
34	AzureFileInTreePluginName = "kubernetes.io/azure-file"
35
36	separator        = "#"
37	volumeIDTemplate = "%s#%s#%s#%s"
38	// Parameter names defined in azure file CSI driver, refer to
39	// https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md
40	shareNameField          = "sharename"
41	secretNameField         = "secretname"
42	secretNamespaceField    = "secretnamespace"
43	secretNameTemplate      = "azure-storage-account-%s-secret"
44	defaultSecretNamespace  = "default"
45	resourceGroupAnnotation = "kubernetes.io/azure-file-resource-group"
46)
47
48var _ InTreePlugin = &azureFileCSITranslator{}
49
50var secretNameFormatRE = regexp.MustCompile(`azure-storage-account-(.+)-secret`)
51
52// azureFileCSITranslator handles translation of PV spec from In-tree
53// Azure File to CSI Azure File and vice versa
54type azureFileCSITranslator struct{}
55
56// NewAzureFileCSITranslator returns a new instance of azureFileTranslator
57func NewAzureFileCSITranslator() InTreePlugin {
58	return &azureFileCSITranslator{}
59}
60
61// TranslateInTreeStorageClassParametersToCSI translates InTree Azure File storage class parameters to CSI storage class
62func (t *azureFileCSITranslator) TranslateInTreeStorageClassToCSI(sc *storage.StorageClass) (*storage.StorageClass, error) {
63	return sc, nil
64}
65
66// TranslateInTreeInlineVolumeToCSI takes a Volume with AzureFile set from in-tree
67// and converts the AzureFile source to a CSIPersistentVolumeSource
68func (t *azureFileCSITranslator) TranslateInTreeInlineVolumeToCSI(volume *v1.Volume, podNamespace string) (*v1.PersistentVolume, error) {
69	if volume == nil || volume.AzureFile == nil {
70		return nil, fmt.Errorf("volume is nil or Azure File not defined on volume")
71	}
72
73	azureSource := volume.AzureFile
74	accountName, err := getStorageAccountName(azureSource.SecretName)
75	if err != nil {
76		klog.Warningf("getStorageAccountName(%s) returned with error: %v", azureSource.SecretName, err)
77		accountName = azureSource.SecretName
78	}
79
80	secretNamespace := defaultSecretNamespace
81	if podNamespace != "" {
82		secretNamespace = podNamespace
83	}
84
85	var (
86		pv = &v1.PersistentVolume{
87			ObjectMeta: metav1.ObjectMeta{
88				// Must be unique per disk as it is used as the unique part of the
89				// staging path
90				Name: fmt.Sprintf("%s-%s", AzureFileDriverName, azureSource.ShareName),
91			},
92			Spec: v1.PersistentVolumeSpec{
93				PersistentVolumeSource: v1.PersistentVolumeSource{
94					CSI: &v1.CSIPersistentVolumeSource{
95						Driver:           AzureFileDriverName,
96						VolumeHandle:     fmt.Sprintf(volumeIDTemplate, "", accountName, azureSource.ShareName, ""),
97						ReadOnly:         azureSource.ReadOnly,
98						VolumeAttributes: map[string]string{shareNameField: azureSource.ShareName},
99						NodeStageSecretRef: &v1.SecretReference{
100							Name:      azureSource.SecretName,
101							Namespace: secretNamespace,
102						},
103					},
104				},
105				AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteMany},
106			},
107		}
108	)
109
110	return pv, nil
111}
112
113// TranslateInTreePVToCSI takes a PV with AzureFile set from in-tree
114// and converts the AzureFile source to a CSIPersistentVolumeSource
115func (t *azureFileCSITranslator) TranslateInTreePVToCSI(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) {
116	if pv == nil || pv.Spec.AzureFile == nil {
117		return nil, fmt.Errorf("pv is nil or Azure File source not defined on pv")
118	}
119
120	azureSource := pv.Spec.PersistentVolumeSource.AzureFile
121	accountName, err := getStorageAccountName(azureSource.SecretName)
122	if err != nil {
123		klog.Warningf("getStorageAccountName(%s) returned with error: %v", azureSource.SecretName, err)
124		accountName = azureSource.SecretName
125	}
126	resourceGroup := ""
127	if pv.ObjectMeta.Annotations != nil {
128		if v, ok := pv.ObjectMeta.Annotations[resourceGroupAnnotation]; ok {
129			resourceGroup = v
130		}
131	}
132	volumeID := fmt.Sprintf(volumeIDTemplate, resourceGroup, accountName, azureSource.ShareName, "")
133
134	var (
135		// refer to https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md
136		csiSource = &v1.CSIPersistentVolumeSource{
137			Driver: AzureFileDriverName,
138			NodeStageSecretRef: &v1.SecretReference{
139				Name:      azureSource.SecretName,
140				Namespace: defaultSecretNamespace,
141			},
142			ReadOnly:         azureSource.ReadOnly,
143			VolumeAttributes: map[string]string{shareNameField: azureSource.ShareName},
144			VolumeHandle:     volumeID,
145		}
146	)
147
148	if azureSource.SecretNamespace != nil {
149		csiSource.NodeStageSecretRef.Namespace = *azureSource.SecretNamespace
150	}
151
152	pv.Spec.PersistentVolumeSource.AzureFile = nil
153	pv.Spec.PersistentVolumeSource.CSI = csiSource
154
155	return pv, nil
156}
157
158// TranslateCSIPVToInTree takes a PV with CSIPersistentVolumeSource set and
159// translates the Azure File CSI source to a AzureFile source.
160func (t *azureFileCSITranslator) TranslateCSIPVToInTree(pv *v1.PersistentVolume) (*v1.PersistentVolume, error) {
161	if pv == nil || pv.Spec.CSI == nil {
162		return nil, fmt.Errorf("pv is nil or CSI source not defined on pv")
163	}
164	csiSource := pv.Spec.CSI
165
166	// refer to https://github.com/kubernetes-sigs/azurefile-csi-driver/blob/master/docs/driver-parameters.md
167	azureSource := &v1.AzureFilePersistentVolumeSource{
168		ReadOnly: csiSource.ReadOnly,
169	}
170
171	for k, v := range csiSource.VolumeAttributes {
172		switch strings.ToLower(k) {
173		case shareNameField:
174			azureSource.ShareName = v
175		case secretNameField:
176			azureSource.SecretName = v
177		case secretNamespaceField:
178			ns := v
179			azureSource.SecretNamespace = &ns
180		}
181	}
182
183	resourceGroup := ""
184	if csiSource.NodeStageSecretRef != nil && csiSource.NodeStageSecretRef.Name != "" {
185		azureSource.SecretName = csiSource.NodeStageSecretRef.Name
186		azureSource.SecretNamespace = &csiSource.NodeStageSecretRef.Namespace
187	}
188	if azureSource.ShareName == "" || azureSource.SecretName == "" {
189		rg, storageAccount, fileShareName, _, err := getFileShareInfo(csiSource.VolumeHandle)
190		if err != nil {
191			return nil, err
192		}
193		if azureSource.ShareName == "" {
194			azureSource.ShareName = fileShareName
195		}
196		if azureSource.SecretName == "" {
197			azureSource.SecretName = fmt.Sprintf(secretNameTemplate, storageAccount)
198		}
199		resourceGroup = rg
200	}
201
202	if azureSource.SecretNamespace == nil {
203		ns := defaultSecretNamespace
204		azureSource.SecretNamespace = &ns
205	}
206
207	pv.Spec.CSI = nil
208	pv.Spec.AzureFile = azureSource
209	if pv.ObjectMeta.Annotations == nil {
210		pv.ObjectMeta.Annotations = map[string]string{}
211	}
212	if resourceGroup != "" {
213		pv.ObjectMeta.Annotations[resourceGroupAnnotation] = resourceGroup
214	}
215
216	return pv, nil
217}
218
219// CanSupport tests whether the plugin supports a given volume
220// specification from the API.  The spec pointer should be considered
221// const.
222func (t *azureFileCSITranslator) CanSupport(pv *v1.PersistentVolume) bool {
223	return pv != nil && pv.Spec.AzureFile != nil
224}
225
226// CanSupportInline tests whether the plugin supports a given inline volume
227// specification from the API.  The spec pointer should be considered
228// const.
229func (t *azureFileCSITranslator) CanSupportInline(volume *v1.Volume) bool {
230	return volume != nil && volume.AzureFile != nil
231}
232
233// GetInTreePluginName returns the name of the intree plugin driver
234func (t *azureFileCSITranslator) GetInTreePluginName() string {
235	return AzureFileInTreePluginName
236}
237
238// GetCSIPluginName returns the name of the CSI plugin
239func (t *azureFileCSITranslator) GetCSIPluginName() string {
240	return AzureFileDriverName
241}
242
243func (t *azureFileCSITranslator) RepairVolumeHandle(volumeHandle, nodeID string) (string, error) {
244	return volumeHandle, nil
245}
246
247// get file share info according to volume id, e.g.
248// input: "rg#f5713de20cde511e8ba4900#pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41#diskname.vhd"
249// output: rg, f5713de20cde511e8ba4900, pvc-file-dynamic-17e43f84-f474-11e8-acd0-000d3a00df41, diskname.vhd
250func getFileShareInfo(id string) (string, string, string, string, error) {
251	segments := strings.Split(id, separator)
252	if len(segments) < 3 {
253		return "", "", "", "", fmt.Errorf("error parsing volume id: %q, should at least contain two #", id)
254	}
255	var diskName string
256	if len(segments) > 3 {
257		diskName = segments[3]
258	}
259	return segments[0], segments[1], segments[2], diskName, nil
260}
261
262// get storage account name from secret name
263func getStorageAccountName(secretName string) (string, error) {
264	matches := secretNameFormatRE.FindStringSubmatch(secretName)
265	if len(matches) != 2 {
266		return "", fmt.Errorf("could not get account name from %s, correct format: %s", secretName, secretNameFormatRE)
267	}
268	return matches[1], nil
269}
270