From 1ce285ad3dfc7e609d924cd91f1077bdb8bed667 Mon Sep 17 00:00:00 2001 From: Andy Zhang Date: Fri, 2 Jun 2023 10:20:53 +0800 Subject: [PATCH] Revert "removed the deprecated `azureFile` in-tree storage plugin" --- .../app/plugins_providers.go | 22 +- cmd/kubelet/app/plugins_providers.go | 2 + pkg/features/kube_features.go | 10 + pkg/volume/azure_file/OWNERS | 18 + pkg/volume/azure_file/azure_file.go | 414 ++++++++++++++++ pkg/volume/azure_file/azure_file_test.go | 454 ++++++++++++++++++ pkg/volume/azure_file/azure_provision.go | 284 +++++++++++ pkg/volume/azure_file/azure_util.go | 162 +++++++ pkg/volume/azure_file/doc.go | 19 + pkg/volume/csi/csi_plugin.go | 2 +- pkg/volume/csimigration/plugin_manager.go | 2 +- 11 files changed, 1386 insertions(+), 3 deletions(-) create mode 100644 pkg/volume/azure_file/OWNERS create mode 100644 pkg/volume/azure_file/azure_file.go create mode 100644 pkg/volume/azure_file/azure_file_test.go create mode 100644 pkg/volume/azure_file/azure_provision.go create mode 100644 pkg/volume/azure_file/azure_util.go create mode 100644 pkg/volume/azure_file/doc.go diff --git a/cmd/kube-controller-manager/app/plugins_providers.go b/cmd/kube-controller-manager/app/plugins_providers.go index b1552fd626a..3eee25b3cd6 100644 --- a/cmd/kube-controller-manager/app/plugins_providers.go +++ b/cmd/kube-controller-manager/app/plugins_providers.go @@ -25,6 +25,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/volume" + "k8s.io/kubernetes/pkg/volume/azure_file" "k8s.io/kubernetes/pkg/volume/csimigration" "k8s.io/kubernetes/pkg/volume/gcepd" "k8s.io/kubernetes/pkg/volume/portworx" @@ -80,5 +81,24 @@ func appendExpandableLegacyProviderVolumes(logger klog.Logger, allPlugins []volu } func appendLegacyProviderVolumes(logger klog.Logger, allPlugins []volume.VolumePlugin, featureGate featuregate.FeatureGate) ([]volume.VolumePlugin, error) { - return appendAttachableLegacyProviderVolumes(logger, allPlugins, featureGate) + var err error + // First append attachable volumes + allPlugins, err = appendAttachableLegacyProviderVolumes(logger, allPlugins, featureGate) + if err != nil { + return allPlugins, err + } + + // Then append non-attachable volumes + pluginName := plugins.AzureFileInTreePluginName + pluginInfo := pluginInfo{ + pluginMigrationFeature: features.CSIMigrationAzureFile, + pluginUnregisterFeature: features.InTreePluginAzureFileUnregister, + pluginProbeFunction: azure_file.ProbeVolumePlugins, + } + allPlugins, err = appendPluginBasedOnFeatureFlags(logger, allPlugins, pluginName, featureGate, pluginInfo) + if err != nil { + return allPlugins, err + } + + return allPlugins, nil } diff --git a/cmd/kubelet/app/plugins_providers.go b/cmd/kubelet/app/plugins_providers.go index bedbc0de834..aebb008b04c 100644 --- a/cmd/kubelet/app/plugins_providers.go +++ b/cmd/kubelet/app/plugins_providers.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/volume" + "k8s.io/kubernetes/pkg/volume/azure_file" "k8s.io/kubernetes/pkg/volume/csimigration" "k8s.io/kubernetes/pkg/volume/gcepd" "k8s.io/kubernetes/pkg/volume/portworx" @@ -66,6 +67,7 @@ type pluginInfo struct { func appendLegacyProviderVolumes(allPlugins []volume.VolumePlugin, featureGate featuregate.FeatureGate) ([]volume.VolumePlugin, error) { pluginMigrationStatus := make(map[string]pluginInfo) pluginMigrationStatus[plugins.GCEPDInTreePluginName] = pluginInfo{pluginMigrationFeature: features.CSIMigrationGCE, pluginUnregisterFeature: features.InTreePluginGCEUnregister, pluginProbeFunction: gcepd.ProbeVolumePlugins} + pluginMigrationStatus[plugins.AzureFileInTreePluginName] = pluginInfo{pluginMigrationFeature: features.CSIMigrationAzureFile, pluginUnregisterFeature: features.InTreePluginAzureFileUnregister, pluginProbeFunction: azure_file.ProbeVolumePlugins} pluginMigrationStatus[plugins.VSphereInTreePluginName] = pluginInfo{pluginMigrationFeature: features.CSIMigrationvSphere, pluginUnregisterFeature: features.InTreePluginvSphereUnregister, pluginProbeFunction: vsphere_volume.ProbeVolumePlugins} pluginMigrationStatus[plugins.PortworxVolumePluginName] = pluginInfo{pluginMigrationFeature: features.CSIMigrationPortworx, pluginUnregisterFeature: features.InTreePluginPortworxUnregister, pluginProbeFunction: portworx.ProbeVolumePlugins} pluginMigrationStatus[plugins.RBDVolumePluginName] = pluginInfo{pluginMigrationFeature: features.CSIMigrationRBD, pluginUnregisterFeature: features.InTreePluginRBDUnregister, pluginProbeFunction: rbd.ProbeVolumePlugins} diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 8ba3b4e52ce..c10ef871ac0 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -122,6 +122,14 @@ const ( // Allow the usage of options to fine-tune the cpumanager policies. CPUManagerPolicyOptions featuregate.Feature = "CPUManagerPolicyOptions" + // owner: @andyzhangx + // alpha: v1.15 + // beta: v1.21 + // GA: v1.26 + // + // Enables the Azure File in-tree driver to Azure File Driver migration feature. + CSIMigrationAzureFile featuregate.Feature = "CSIMigrationAzureFile" + // owner: @davidz627 // alpha: v1.14 // beta: v1.17 @@ -870,6 +878,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS CPUManagerPolicyOptions: {Default: true, PreRelease: featuregate.Beta}, + CSIMigrationAzureFile: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.28 + CSIMigrationGCE: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.27 CSIMigrationPortworx: {Default: false, PreRelease: featuregate.Beta}, // Off by default (requires Portworx CSI driver) diff --git a/pkg/volume/azure_file/OWNERS b/pkg/volume/azure_file/OWNERS new file mode 100644 index 00000000000..71675e1ce81 --- /dev/null +++ b/pkg/volume/azure_file/OWNERS @@ -0,0 +1,18 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +approvers: + - andyzhangx + - feiskyer + - khenidak +reviewers: + - andyzhangx + - feiskyer + - jsafrane + - jingxu97 + - khenidak + - msau42 + - saad-ali +emeritus_approvers: + - karataliu + - rootfs + - brendandburns diff --git a/pkg/volume/azure_file/azure_file.go b/pkg/volume/azure_file/azure_file.go new file mode 100644 index 00000000000..8c00c41b579 --- /dev/null +++ b/pkg/volume/azure_file/azure_file.go @@ -0,0 +1,414 @@ +//go:build !providerless +// +build !providerless + +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure_file + +import ( + "fmt" + "io/ioutil" + "os" + "runtime" + "time" + + "k8s.io/klog/v2" + "k8s.io/mount-utils" + utilstrings "k8s.io/utils/strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + cloudprovider "k8s.io/cloud-provider" + volumehelpers "k8s.io/cloud-provider/volume/helpers" + "k8s.io/kubernetes/pkg/volume" + volutil "k8s.io/kubernetes/pkg/volume/util" + "k8s.io/legacy-cloud-providers/azure" +) + +// ProbeVolumePlugins is the primary endpoint for volume plugins +func ProbeVolumePlugins() []volume.VolumePlugin { + return []volume.VolumePlugin{&azureFilePlugin{nil}} +} + +type azureFilePlugin struct { + host volume.VolumeHost +} + +var _ volume.VolumePlugin = &azureFilePlugin{} +var _ volume.PersistentVolumePlugin = &azureFilePlugin{} +var _ volume.ExpandableVolumePlugin = &azureFilePlugin{} + +const ( + azureFilePluginName = "kubernetes.io/azure-file" + minimumPremiumShareSize = 100 // GB +) + +func getPath(uid types.UID, volName string, host volume.VolumeHost) string { + return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(azureFilePluginName), volName) +} + +func (plugin *azureFilePlugin) Init(host volume.VolumeHost) error { + plugin.host = host + return nil +} + +func (plugin *azureFilePlugin) GetPluginName() string { + return azureFilePluginName +} + +func (plugin *azureFilePlugin) GetVolumeName(spec *volume.Spec) (string, error) { + share, _, err := getVolumeSource(spec) + if err != nil { + return "", err + } + + return share, nil +} + +func (plugin *azureFilePlugin) CanSupport(spec *volume.Spec) bool { + //TODO: check if mount.cifs is there + return (spec.PersistentVolume != nil && spec.PersistentVolume.Spec.AzureFile != nil) || + (spec.Volume != nil && spec.Volume.AzureFile != nil) +} + +func (plugin *azureFilePlugin) RequiresRemount(spec *volume.Spec) bool { + return false +} + +func (plugin *azureFilePlugin) SupportsMountOption() bool { + return true +} + +func (plugin *azureFilePlugin) SupportsBulkVolumeVerification() bool { + return false +} + +func (plugin *azureFilePlugin) SupportsSELinuxContextMount(spec *volume.Spec) (bool, error) { + return false, nil +} + +func (plugin *azureFilePlugin) GetAccessModes() []v1.PersistentVolumeAccessMode { + return []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + v1.ReadOnlyMany, + v1.ReadWriteMany, + } +} + +func (plugin *azureFilePlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) { + return plugin.newMounterInternal(spec, pod, &azureSvc{}, plugin.host.GetMounter(plugin.GetPluginName())) +} + +func (plugin *azureFilePlugin) newMounterInternal(spec *volume.Spec, pod *v1.Pod, util azureUtil, mounter mount.Interface) (volume.Mounter, error) { + share, readOnly, err := getVolumeSource(spec) + if err != nil { + return nil, err + } + secretName, secretNamespace, err := getSecretNameAndNamespace(spec, pod.Namespace) + if err != nil { + // Log-and-continue instead of returning an error for now + // due to unspecified backwards compatibility concerns (a subject to revise) + klog.Errorf("get secret name and namespace from pod(%s) return with error: %v", pod.Name, err) + } + return &azureFileMounter{ + azureFile: &azureFile{ + volName: spec.Name(), + mounter: mounter, + pod: pod, + plugin: plugin, + MetricsProvider: volume.NewMetricsStatFS(getPath(pod.UID, spec.Name(), plugin.host)), + }, + util: util, + secretNamespace: secretNamespace, + secretName: secretName, + shareName: share, + readOnly: readOnly, + mountOptions: volutil.MountOptionFromSpec(spec), + }, nil +} + +func (plugin *azureFilePlugin) NewUnmounter(volName string, podUID types.UID) (volume.Unmounter, error) { + return plugin.newUnmounterInternal(volName, podUID, plugin.host.GetMounter(plugin.GetPluginName())) +} + +func (plugin *azureFilePlugin) newUnmounterInternal(volName string, podUID types.UID, mounter mount.Interface) (volume.Unmounter, error) { + return &azureFileUnmounter{&azureFile{ + volName: volName, + mounter: mounter, + pod: &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: podUID}}, + plugin: plugin, + MetricsProvider: volume.NewMetricsStatFS(getPath(podUID, volName, plugin.host)), + }}, nil +} + +func (plugin *azureFilePlugin) RequiresFSResize() bool { + return false +} + +func (plugin *azureFilePlugin) ExpandVolumeDevice( + spec *volume.Spec, + newSize resource.Quantity, + oldSize resource.Quantity) (resource.Quantity, error) { + + if spec.PersistentVolume == nil || spec.PersistentVolume.Spec.AzureFile == nil { + return oldSize, fmt.Errorf("invalid PV spec") + } + shareName := spec.PersistentVolume.Spec.AzureFile.ShareName + azure, resourceGroup, err := getAzureCloudProvider(plugin.host.GetCloudProvider()) + if err != nil { + return oldSize, err + } + if spec.PersistentVolume.ObjectMeta.Annotations[resourceGroupAnnotation] != "" { + resourceGroup = spec.PersistentVolume.ObjectMeta.Annotations[resourceGroupAnnotation] + } + + secretName, secretNamespace, err := getSecretNameAndNamespace(spec, spec.PersistentVolume.Spec.ClaimRef.Namespace) + if err != nil { + return oldSize, err + } + + accountName, _, err := (&azureSvc{}).GetAzureCredentials(plugin.host, secretNamespace, secretName) + if err != nil { + return oldSize, err + } + + requestGiB, err := volumehelpers.RoundUpToGiBInt(newSize) + + if err != nil { + return oldSize, err + } + + if err := azure.ResizeFileShare(resourceGroup, accountName, shareName, requestGiB); err != nil { + return oldSize, err + } + + return newSize, nil +} + +func (plugin *azureFilePlugin) ConstructVolumeSpec(volName, mountPath string) (volume.ReconstructedVolume, error) { + azureVolume := &v1.Volume{ + Name: volName, + VolumeSource: v1.VolumeSource{ + AzureFile: &v1.AzureFileVolumeSource{ + SecretName: volName, + ShareName: volName, + }, + }, + } + return volume.ReconstructedVolume{ + Spec: volume.NewSpecFromVolume(azureVolume), + }, nil +} + +// azureFile volumes represent mount of an AzureFile share. +type azureFile struct { + volName string + podUID types.UID + pod *v1.Pod + mounter mount.Interface + plugin *azureFilePlugin + volume.MetricsProvider +} + +func (azureFileVolume *azureFile) GetPath() string { + return getPath(azureFileVolume.pod.UID, azureFileVolume.volName, azureFileVolume.plugin.host) +} + +type azureFileMounter struct { + *azureFile + util azureUtil + secretName string + secretNamespace string + shareName string + readOnly bool + mountOptions []string +} + +var _ volume.Mounter = &azureFileMounter{} + +func (b *azureFileMounter) GetAttributes() volume.Attributes { + return volume.Attributes{ + ReadOnly: b.readOnly, + Managed: !b.readOnly, + SELinuxRelabel: false, + } +} + +// SetUp attaches the disk and bind mounts to the volume path. +func (b *azureFileMounter) SetUp(mounterArgs volume.MounterArgs) error { + return b.SetUpAt(b.GetPath(), mounterArgs) +} + +func (b *azureFileMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error { + notMnt, err := b.mounter.IsLikelyNotMountPoint(dir) + klog.V(4).Infof("AzureFile mount set up: %s %v %v", dir, !notMnt, err) + if err != nil && !os.IsNotExist(err) { + return err + } + if !notMnt { + // testing original mount point, make sure the mount link is valid + if _, err := ioutil.ReadDir(dir); err == nil { + klog.V(4).Infof("azureFile - already mounted to target %s", dir) + return nil + } + // mount link is invalid, now unmount and remount later + klog.Warningf("azureFile - ReadDir %s failed with %v, unmount this directory", dir, err) + if err := b.mounter.Unmount(dir); err != nil { + klog.Errorf("azureFile - Unmount directory %s failed with %v", dir, err) + return err + } + } + + var accountKey, accountName string + if accountName, accountKey, err = b.util.GetAzureCredentials(b.plugin.host, b.secretNamespace, b.secretName); err != nil { + return err + } + + var mountOptions []string + var sensitiveMountOptions []string + source := "" + osSeparator := string(os.PathSeparator) + source = fmt.Sprintf("%s%s%s.file.%s%s%s", osSeparator, osSeparator, accountName, getStorageEndpointSuffix(b.plugin.host.GetCloudProvider()), osSeparator, b.shareName) + + if runtime.GOOS == "windows" { + mountOptions = []string{fmt.Sprintf("AZURE\\%s", accountName)} + sensitiveMountOptions = []string{accountKey} + } else { + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + // parameters suggested by https://azure.microsoft.com/en-us/documentation/articles/storage-how-to-use-files-linux/ + options := []string{} + sensitiveMountOptions = []string{fmt.Sprintf("username=%s,password=%s", accountName, accountKey)} + if b.readOnly { + options = append(options, "ro") + } + mountOptions = volutil.JoinMountOptions(b.mountOptions, options) + mountOptions = appendDefaultMountOptions(mountOptions, mounterArgs.FsGroup) + } + + mountComplete := false + err = wait.PollImmediate(1*time.Second, 2*time.Minute, func() (bool, error) { + err := b.mounter.MountSensitiveWithoutSystemd(source, dir, "cifs", mountOptions, sensitiveMountOptions) + mountComplete = true + return true, err + }) + if !mountComplete { + return fmt.Errorf("volume(%s) mount on %s timeout(10m)", source, dir) + } + if err != nil { + notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir) + if mntErr != nil { + klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr) + return err + } + if !notMnt { + if mntErr = b.mounter.Unmount(dir); mntErr != nil { + klog.Errorf("Failed to unmount: %v", mntErr) + return err + } + notMnt, mntErr := b.mounter.IsLikelyNotMountPoint(dir) + if mntErr != nil { + klog.Errorf("IsLikelyNotMountPoint check failed: %v", mntErr) + return err + } + if !notMnt { + // This is very odd, we don't expect it. We'll try again next sync loop. + klog.Errorf("%s is still mounted, despite call to unmount(). Will try again next sync loop.", dir) + return err + } + } + os.Remove(dir) + return err + } + return nil +} + +var _ volume.Unmounter = &azureFileUnmounter{} + +type azureFileUnmounter struct { + *azureFile +} + +func (c *azureFileUnmounter) TearDown() error { + return c.TearDownAt(c.GetPath()) +} + +func (c *azureFileUnmounter) TearDownAt(dir string) error { + return mount.CleanupMountPoint(dir, c.mounter, false) +} + +func getVolumeSource(spec *volume.Spec) (string, bool, error) { + if spec.Volume != nil && spec.Volume.AzureFile != nil { + share := spec.Volume.AzureFile.ShareName + readOnly := spec.Volume.AzureFile.ReadOnly + return share, readOnly, nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.AzureFile != nil { + share := spec.PersistentVolume.Spec.AzureFile.ShareName + readOnly := spec.ReadOnly + return share, readOnly, nil + } + return "", false, fmt.Errorf("Spec does not reference an AzureFile volume type") +} + +func getSecretNameAndNamespace(spec *volume.Spec, defaultNamespace string) (string, string, error) { + secretName := "" + secretNamespace := "" + if spec.Volume != nil && spec.Volume.AzureFile != nil { + secretName = spec.Volume.AzureFile.SecretName + secretNamespace = defaultNamespace + + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.AzureFile != nil { + secretNamespace = defaultNamespace + if spec.PersistentVolume.Spec.AzureFile.SecretNamespace != nil { + secretNamespace = *spec.PersistentVolume.Spec.AzureFile.SecretNamespace + } + secretName = spec.PersistentVolume.Spec.AzureFile.SecretName + } else { + return "", "", fmt.Errorf("Spec does not reference an AzureFile volume type") + } + + if len(secretNamespace) == 0 { + return "", "", fmt.Errorf("invalid Azure volume: nil namespace") + } + return secretName, secretNamespace, nil + +} + +func getAzureCloud(cloudProvider cloudprovider.Interface) (*azure.Cloud, error) { + azure, ok := cloudProvider.(*azure.Cloud) + if !ok || azure == nil { + return nil, fmt.Errorf("failed to get Azure Cloud Provider. GetCloudProvider returned %v instead", cloudProvider) + } + + return azure, nil +} + +func getStorageEndpointSuffix(cloudprovider cloudprovider.Interface) string { + const publicCloudStorageEndpointSuffix = "core.windows.net" + azure, err := getAzureCloud(cloudprovider) + if err != nil { + klog.Warningf("No Azure cloud provider found. Using the Azure public cloud endpoint: %s", publicCloudStorageEndpointSuffix) + return publicCloudStorageEndpointSuffix + } + return azure.Environment.StorageEndpointSuffix +} diff --git a/pkg/volume/azure_file/azure_file_test.go b/pkg/volume/azure_file/azure_file_test.go new file mode 100644 index 00000000000..59096837206 --- /dev/null +++ b/pkg/volume/azure_file/azure_file_test.go @@ -0,0 +1,454 @@ +//go:build !providerless +// +build !providerless + +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure_file + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + goruntime "runtime" + "strings" + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/fake" + fakecloud "k8s.io/cloud-provider/fake" + "k8s.io/mount-utils" + "k8s.io/utils/pointer" + + "k8s.io/kubernetes/pkg/volume" + volumetest "k8s.io/kubernetes/pkg/volume/testing" + "k8s.io/legacy-cloud-providers/azure" +) + +func TestCanSupport(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "azureFileTest") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/azure-file") + if err != nil { + t.Fatal("Can't find the plugin by name") + } + if plug.GetPluginName() != "kubernetes.io/azure-file" { + t.Errorf("Wrong name: %s", plug.GetPluginName()) + } + if !plug.CanSupport(&volume.Spec{Volume: &v1.Volume{VolumeSource: v1.VolumeSource{AzureFile: &v1.AzureFileVolumeSource{}}}}) { + t.Errorf("Expected true") + } + if !plug.CanSupport(&volume.Spec{PersistentVolume: &v1.PersistentVolume{Spec: v1.PersistentVolumeSpec{PersistentVolumeSource: v1.PersistentVolumeSource{AzureFile: &v1.AzureFilePersistentVolumeSource{}}}}}) { + t.Errorf("Expected true") + } +} + +func TestGetAccessModes(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "azureFileTest") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)) + + plug, err := plugMgr.FindPersistentPluginByName("kubernetes.io/azure-file") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if !volumetest.ContainsAccessMode(plug.GetAccessModes(), v1.ReadWriteOnce) || !volumetest.ContainsAccessMode(plug.GetAccessModes(), v1.ReadOnlyMany) || !volumetest.ContainsAccessMode(plug.GetAccessModes(), v1.ReadWriteMany) { + t.Errorf("Expected three AccessModeTypes: %s, %s, and %s", v1.ReadWriteOnce, v1.ReadOnlyMany, v1.ReadWriteMany) + } +} + +func getAzureTestCloud(t *testing.T) *azure.Cloud { + config := `{ + "aadClientId": "--aad-client-id--", + "aadClientSecret": "--aad-client-secret--" + }` + configReader := strings.NewReader(config) + azureCloud, err := azure.NewCloudWithoutFeatureGates(configReader) + if err != nil { + t.Error(err) + } + return azureCloud +} + +func getTestTempDir(t *testing.T) string { + tmpDir, err := ioutil.TempDir(os.TempDir(), "azurefileTest") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + return tmpDir +} + +func TestPluginAzureCloudProvider(t *testing.T) { + tmpDir := getTestTempDir(t) + defer os.RemoveAll(tmpDir) + testPlugin(t, tmpDir, volumetest.NewFakeVolumeHostWithCloudProvider(t, tmpDir, nil, nil, getAzureTestCloud(t))) +} + +func TestPluginWithoutCloudProvider(t *testing.T) { + tmpDir := getTestTempDir(t) + defer os.RemoveAll(tmpDir) + testPlugin(t, tmpDir, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)) +} + +func TestPluginWithOtherCloudProvider(t *testing.T) { + tmpDir := getTestTempDir(t) + defer os.RemoveAll(tmpDir) + cloud := &fakecloud.Cloud{} + testPlugin(t, tmpDir, volumetest.NewFakeVolumeHostWithCloudProvider(t, tmpDir, nil, nil, cloud)) +} + +func testPlugin(t *testing.T, tmpDir string, volumeHost volume.VolumeHost) { + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumeHost) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/azure-file") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + spec := &v1.Volume{ + Name: "vol1", + VolumeSource: v1.VolumeSource{ + AzureFile: &v1.AzureFileVolumeSource{ + SecretName: "secret", + ShareName: "share", + }, + }, + } + fake := mount.NewFakeMounter(nil) + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}} + mounter, err := plug.(*azureFilePlugin).newMounterInternal(volume.NewSpecFromVolume(spec), pod, &fakeAzureSvc{}, fake) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + volPath := filepath.Join(tmpDir, "pods/poduid/volumes/kubernetes.io~azure-file/vol1") + path := mounter.GetPath() + if path != volPath { + t.Errorf("Got unexpected path: %s", path) + } + + if err := mounter.SetUp(volume.MounterArgs{}); err != nil { + t.Errorf("Expected success, got: %v", err) + } + // On Windows, Mount will create the parent of dir and mklink (create a symbolic link) at the volume path later, + // so mounter.SetUp will not create the directory. Otherwise mklink will error: "Cannot create a file when that file already exists". + if goruntime.GOOS != "windows" { + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", path) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + } + + unmounter, err := plug.(*azureFilePlugin).newUnmounterInternal("vol1", types.UID("poduid"), mount.NewFakeMounter(nil)) + if err != nil { + t.Errorf("Failed to make a new Unmounter: %v", err) + } + if unmounter == nil { + t.Errorf("Got a nil Unmounter") + } + + if err := unmounter.TearDown(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(path); err == nil { + t.Errorf("TearDown() failed, volume path still exists: %s", path) + } else if !os.IsNotExist(err) { + t.Errorf("TearDown() failed: %v", err) + } +} + +func TestPersistentClaimReadOnlyFlag(t *testing.T) { + pv := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvA", + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + AzureFile: &v1.AzureFilePersistentVolumeSource{}, + }, + ClaimRef: &v1.ObjectReference{ + Name: "claimA", + }, + }, + } + + claim := &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "claimA", + Namespace: "nsA", + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "pvA", + }, + Status: v1.PersistentVolumeClaimStatus{ + Phase: v1.ClaimBound, + }, + } + + client := fake.NewSimpleClientset(pv, claim) + + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, filepath.Join(os.TempDir(), "fake"), client, nil)) + plug, _ := plugMgr.FindPluginByName(azureFilePluginName) + + // readOnly bool is supplied by persistent-claim volume source when its mounter creates other volumes + spec := volume.NewSpecFromPersistentVolume(pv, true) + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}} + mounter, _ := plug.NewMounter(spec, pod, volume.VolumeOptions{}) + if mounter == nil { + t.Fatalf("Got a nil Mounter") + } + + if !mounter.GetAttributes().ReadOnly { + t.Errorf("Expected true for mounter.IsReadOnly") + } +} + +type fakeAzureSvc struct{} + +func (s *fakeAzureSvc) GetAzureCredentials(host volume.VolumeHost, nameSpace, secretName string) (string, string, error) { + return "name", "key", nil +} +func (s *fakeAzureSvc) SetAzureCredentials(host volume.VolumeHost, nameSpace, accountName, accountKey string) (string, error) { + return "secret", nil +} + +func TestMounterAndUnmounterTypeAssert(t *testing.T) { + tmpDir, err := ioutil.TempDir(os.TempDir(), "azurefileTest") + if err != nil { + t.Fatalf("can't make a temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + plugMgr := volume.VolumePluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), nil /* prober */, volumetest.NewFakeVolumeHost(t, tmpDir, nil, nil)) + + plug, err := plugMgr.FindPluginByName("kubernetes.io/azure-file") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + spec := &v1.Volume{ + Name: "vol1", + VolumeSource: v1.VolumeSource{ + AzureFile: &v1.AzureFileVolumeSource{ + SecretName: "secret", + ShareName: "share", + }, + }, + } + fake := mount.NewFakeMounter(nil) + pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}} + mounter, err := plug.(*azureFilePlugin).newMounterInternal(volume.NewSpecFromVolume(spec), pod, &fakeAzureSvc{}, fake) + if err != nil { + t.Errorf("MounterInternal() failed: %v", err) + } + if _, ok := mounter.(volume.Unmounter); ok { + t.Errorf("Volume Mounter can be type-assert to Unmounter") + } + + unmounter, err := plug.(*azureFilePlugin).newUnmounterInternal("vol1", types.UID("poduid"), mount.NewFakeMounter(nil)) + if err != nil { + t.Errorf("MounterInternal() failed: %v", err) + } + if _, ok := unmounter.(volume.Mounter); ok { + t.Errorf("Volume Unmounter can be type-assert to Mounter") + } +} + +type testcase struct { + name string + defaultNs string + spec *volume.Spec + // Expected return of the test + expectedName string + expectedNs string + expectedError error +} + +func TestGetSecretNameAndNamespaceForPV(t *testing.T) { + secretNs := "ns" + tests := []testcase{ + { + name: "persistent volume source", + defaultNs: "default", + spec: &volume.Spec{ + PersistentVolume: &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + AzureFile: &v1.AzureFilePersistentVolumeSource{ + ShareName: "share", + SecretName: "name", + SecretNamespace: &secretNs, + }, + }, + }, + }, + }, + expectedName: "name", + expectedNs: "ns", + expectedError: nil, + }, + { + name: "persistent volume source without namespace", + defaultNs: "default", + spec: &volume.Spec{ + PersistentVolume: &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + AzureFile: &v1.AzureFilePersistentVolumeSource{ + ShareName: "share", + SecretName: "name", + }, + }, + }, + }, + }, + expectedName: "name", + expectedNs: "default", + expectedError: nil, + }, + { + name: "pod volume source", + defaultNs: "default", + spec: &volume.Spec{ + Volume: &v1.Volume{ + VolumeSource: v1.VolumeSource{ + AzureFile: &v1.AzureFileVolumeSource{ + ShareName: "share", + SecretName: "name", + }, + }, + }, + }, + expectedName: "name", + expectedNs: "default", + expectedError: nil, + }, + } + for _, testcase := range tests { + resultName, resultNs, err := getSecretNameAndNamespace(testcase.spec, testcase.defaultNs) + if err != testcase.expectedError || resultName != testcase.expectedName || resultNs != testcase.expectedNs { + t.Errorf("%s failed: expected err=%v ns=%q name=%q, got %v/%q/%q", testcase.name, testcase.expectedError, testcase.expectedNs, testcase.expectedName, + err, resultNs, resultName) + } + } + +} + +func TestAppendDefaultMountOptions(t *testing.T) { + tests := []struct { + options []string + fsGroup *int64 + expected []string + }{ + { + options: []string{"dir_mode=0777"}, + fsGroup: nil, + expected: []string{"dir_mode=0777", + fmt.Sprintf("%s=%s", fileMode, defaultFileMode), + fmt.Sprintf("%s=%s", vers, defaultVers), + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{"file_mode=0777"}, + fsGroup: pointer.Int64(0), + expected: []string{"file_mode=0777", + fmt.Sprintf("%s=%s", dirMode, defaultDirMode), + fmt.Sprintf("%s=%s", vers, defaultVers), + fmt.Sprintf("%s=0", gid), + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{"vers=2.1"}, + fsGroup: pointer.Int64(1000), + expected: []string{"vers=2.1", + fmt.Sprintf("%s=%s", fileMode, defaultFileMode), + fmt.Sprintf("%s=%s", dirMode, defaultDirMode), + fmt.Sprintf("%s=1000", gid), + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{""}, + expected: []string{"", fmt.Sprintf("%s=%s", + fileMode, defaultFileMode), + fmt.Sprintf("%s=%s", dirMode, defaultDirMode), + fmt.Sprintf("%s=%s", vers, defaultVers), + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{"file_mode=0777", "dir_mode=0777"}, + expected: []string{"file_mode=0777", "dir_mode=0777", + fmt.Sprintf("%s=%s", vers, defaultVers), + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{"gid=2000"}, + fsGroup: pointer.Int64(1000), + expected: []string{"gid=2000", + fmt.Sprintf("%s=%s", fileMode, defaultFileMode), + fmt.Sprintf("%s=%s", dirMode, defaultDirMode), + "vers=3.0", + fmt.Sprintf("%s=%s", actimeo, defaultActimeo), + mfsymlinks, + }, + }, + { + options: []string{"actimeo=3"}, + expected: []string{ + "actimeo=3", + fmt.Sprintf("%s=%s", fileMode, defaultFileMode), + fmt.Sprintf("%s=%s", dirMode, defaultDirMode), + fmt.Sprintf("%s=%s", vers, defaultVers), + mfsymlinks, + }, + }, + } + + for _, test := range tests { + result := appendDefaultMountOptions(test.options, test.fsGroup) + if !reflect.DeepEqual(result, test.expected) { + t.Errorf("input: %q, appendDefaultMountOptions result: %q, expected: %q", test.options, result, test.expected) + } + } +} diff --git a/pkg/volume/azure_file/azure_provision.go b/pkg/volume/azure_file/azure_provision.go new file mode 100644 index 00000000000..9635348b3da --- /dev/null +++ b/pkg/volume/azure_file/azure_provision.go @@ -0,0 +1,284 @@ +//go:build !providerless +// +build !providerless + +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure_file + +import ( + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cloudprovider "k8s.io/cloud-provider" + volumehelpers "k8s.io/cloud-provider/volume/helpers" + "k8s.io/klog/v2" + "k8s.io/kubernetes/pkg/volume" + "k8s.io/kubernetes/pkg/volume/util" + "k8s.io/legacy-cloud-providers/azure" + "k8s.io/legacy-cloud-providers/azure/clients/fileclient" + utilstrings "k8s.io/utils/strings" +) + +var ( + _ volume.DeletableVolumePlugin = &azureFilePlugin{} + _ volume.ProvisionableVolumePlugin = &azureFilePlugin{} + + resourceGroupAnnotation = "kubernetes.io/azure-file-resource-group" +) + +// Abstract interface to file share operations. +// azure cloud provider should implement it +type azureCloudProvider interface { + // create a file share + CreateFileShare(account *azure.AccountOptions, fileShare *fileclient.ShareOptions) (string, string, error) + // delete a file share + DeleteFileShare(resourceGroup, accountName, shareName string) error + // resize a file share + ResizeFileShare(resourceGroup, accountName, name string, sizeGiB int) error +} + +type azureFileDeleter struct { + *azureFile + resourceGroup, accountName, shareName string + azureProvider azureCloudProvider +} + +func (plugin *azureFilePlugin) NewDeleter(logger klog.Logger, spec *volume.Spec) (volume.Deleter, error) { + azure, resourceGroup, err := getAzureCloudProvider(plugin.host.GetCloudProvider()) + if err != nil { + klog.V(4).Infof("failed to get azure provider") + return nil, err + } + if spec.PersistentVolume != nil && spec.PersistentVolume.ObjectMeta.Annotations[resourceGroupAnnotation] != "" { + resourceGroup = spec.PersistentVolume.ObjectMeta.Annotations[resourceGroupAnnotation] + } + + return plugin.newDeleterInternal(spec, &azureSvc{}, azure, resourceGroup) +} + +func (plugin *azureFilePlugin) newDeleterInternal(spec *volume.Spec, util azureUtil, azure azureCloudProvider, resourceGroup string) (volume.Deleter, error) { + if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.AzureFile == nil { + return nil, fmt.Errorf("invalid PV spec") + } + + secretName, secretNamespace, err := getSecretNameAndNamespace(spec, spec.PersistentVolume.Spec.ClaimRef.Namespace) + if err != nil { + return nil, err + } + shareName := spec.PersistentVolume.Spec.AzureFile.ShareName + if accountName, _, err := util.GetAzureCredentials(plugin.host, secretNamespace, secretName); err != nil { + return nil, err + } else { + + return &azureFileDeleter{ + azureFile: &azureFile{ + volName: spec.Name(), + plugin: plugin, + }, + resourceGroup: resourceGroup, + shareName: shareName, + accountName: accountName, + azureProvider: azure, + }, nil + } +} + +func (plugin *azureFilePlugin) NewProvisioner(logger klog.Logger, options volume.VolumeOptions) (volume.Provisioner, error) { + azure, resourceGroup, err := getAzureCloudProvider(plugin.host.GetCloudProvider()) + if err != nil { + klog.V(4).Infof("failed to get azure provider") + return nil, err + } + if len(options.PVC.Spec.AccessModes) == 0 { + options.PVC.Spec.AccessModes = plugin.GetAccessModes() + } + if resourceGroup != "" { + options.PVC.ObjectMeta.Annotations[resourceGroupAnnotation] = resourceGroup + } + return plugin.newProvisionerInternal(options, azure) +} + +func (plugin *azureFilePlugin) newProvisionerInternal(options volume.VolumeOptions, azure azureCloudProvider) (volume.Provisioner, error) { + return &azureFileProvisioner{ + azureFile: &azureFile{ + plugin: plugin, + }, + azureProvider: azure, + util: &azureSvc{}, + options: options, + }, nil +} + +var _ volume.Deleter = &azureFileDeleter{} + +func (f *azureFileDeleter) GetPath() string { + name := azureFilePluginName + return f.plugin.host.GetPodVolumeDir(f.podUID, utilstrings.EscapeQualifiedName(name), f.volName) +} + +func (f *azureFileDeleter) Delete() error { + klog.V(4).Infof("deleting volume %s", f.shareName) + return f.azureProvider.DeleteFileShare(f.resourceGroup, f.accountName, f.shareName) +} + +type azureFileProvisioner struct { + *azureFile + azureProvider azureCloudProvider + util azureUtil + options volume.VolumeOptions +} + +var _ volume.Provisioner = &azureFileProvisioner{} + +func (a *azureFileProvisioner) Provision(selectedNode *v1.Node, allowedTopologies []v1.TopologySelectorTerm) (*v1.PersistentVolume, error) { + if !util.ContainsAllAccessModes(a.plugin.GetAccessModes(), a.options.PVC.Spec.AccessModes) { + return nil, fmt.Errorf("invalid AccessModes %v: only AccessModes %v are supported", a.options.PVC.Spec.AccessModes, a.plugin.GetAccessModes()) + } + if util.CheckPersistentVolumeClaimModeBlock(a.options.PVC) { + return nil, fmt.Errorf("%s does not support block volume provisioning", a.plugin.GetPluginName()) + } + + var sku, resourceGroup, location, account, shareName, customTags string + + capacity := a.options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)] + requestGiB, err := volumehelpers.RoundUpToGiBInt(capacity) + if err != nil { + return nil, err + } + + secretNamespace := a.options.PVC.Namespace + // Apply ProvisionerParameters (case-insensitive). We leave validation of + // the values to the cloud provider. + for k, v := range a.options.Parameters { + switch strings.ToLower(k) { + case "skuname": + sku = v + case "location": + location = v + case "storageaccount": + account = v + case "secretnamespace": + secretNamespace = v + case "resourcegroup": + resourceGroup = v + case "sharename": + shareName = v + case "tags": + customTags = v + default: + return nil, fmt.Errorf("invalid option %q for volume plugin %s", k, a.plugin.GetPluginName()) + } + } + // TODO: implement c.options.ProvisionerSelector parsing + if a.options.PVC.Spec.Selector != nil { + return nil, fmt.Errorf("claim.Spec.Selector is not supported for dynamic provisioning on Azure file") + } + + tags, err := azure.ConvertTagsToMap(customTags) + if err != nil { + return nil, err + } + + if shareName == "" { + // File share name has a length limit of 63, it cannot contain two consecutive '-'s, and all letters must be lower case. + name := util.GenerateVolumeName(a.options.ClusterName, a.options.PVName, 63) + shareName = strings.Replace(name, "--", "-", -1) + shareName = strings.ToLower(shareName) + } + + if resourceGroup == "" { + resourceGroup = a.options.PVC.ObjectMeta.Annotations[resourceGroupAnnotation] + } + + fileShareSize := int(requestGiB) + // when use azure file premium, account kind should be specified as FileStorage + accountKind := string(storage.StorageV2) + if strings.HasPrefix(strings.ToLower(sku), "premium") { + accountKind = string(storage.FileStorage) + // when using azure file premium, the shares have a minimum size + if fileShareSize < minimumPremiumShareSize { + fileShareSize = minimumPremiumShareSize + } + } + + accountOptions := &azure.AccountOptions{ + Name: account, + Type: sku, + Kind: accountKind, + ResourceGroup: resourceGroup, + Location: location, + Tags: tags, + } + + shareOptions := &fileclient.ShareOptions{ + Name: shareName, + Protocol: storage.SMB, + RequestGiB: fileShareSize, + } + + account, key, err := a.azureProvider.CreateFileShare(accountOptions, shareOptions) + if err != nil { + return nil, err + } + + // create a secret for storage account and key + secretName, err := a.util.SetAzureCredentials(a.plugin.host, secretNamespace, account, key) + if err != nil { + return nil, err + } + // create PV + pv := &v1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.options.PVName, + Labels: map[string]string{}, + Annotations: map[string]string{ + util.VolumeDynamicallyCreatedByKey: "azure-file-dynamic-provisioner", + resourceGroupAnnotation: resourceGroup, + }, + }, + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeReclaimPolicy: a.options.PersistentVolumeReclaimPolicy, + AccessModes: a.options.PVC.Spec.AccessModes, + Capacity: v1.ResourceList{ + v1.ResourceName(v1.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", fileShareSize)), + }, + PersistentVolumeSource: v1.PersistentVolumeSource{ + AzureFile: &v1.AzureFilePersistentVolumeSource{ + SecretName: secretName, + ShareName: shareName, + SecretNamespace: &secretNamespace, + }, + }, + MountOptions: a.options.MountOptions, + }, + } + return pv, nil +} + +// Return cloud provider +func getAzureCloudProvider(cloudProvider cloudprovider.Interface) (azureCloudProvider, string, error) { + azureCloudProvider, ok := cloudProvider.(*azure.Cloud) + if !ok || azureCloudProvider == nil { + return nil, "", fmt.Errorf("failed to get Azure Cloud Provider. GetCloudProvider returned %v instead", cloudProvider) + } + + return azureCloudProvider, azureCloudProvider.ResourceGroup, nil +} diff --git a/pkg/volume/azure_file/azure_util.go b/pkg/volume/azure_file/azure_util.go new file mode 100644 index 00000000000..f44757aaef5 --- /dev/null +++ b/pkg/volume/azure_file/azure_util.go @@ -0,0 +1,162 @@ +//go:build !providerless +// +build !providerless + +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package azure_file + +import ( + "context" + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubernetes/pkg/volume" +) + +const ( + fileMode = "file_mode" + dirMode = "dir_mode" + gid = "gid" + vers = "vers" + actimeo = "actimeo" + mfsymlinks = "mfsymlinks" + defaultFileMode = "0777" + defaultDirMode = "0777" + defaultVers = "3.0" + defaultActimeo = "30" +) + +// Abstract interface to azure file operations. +type azureUtil interface { + GetAzureCredentials(host volume.VolumeHost, nameSpace, secretName string) (string, string, error) + SetAzureCredentials(host volume.VolumeHost, nameSpace, accountName, accountKey string) (string, error) +} + +type azureSvc struct{} + +func (s *azureSvc) GetAzureCredentials(host volume.VolumeHost, nameSpace, secretName string) (string, string, error) { + var accountKey, accountName string + kubeClient := host.GetKubeClient() + if kubeClient == nil { + return "", "", fmt.Errorf("cannot get kube client") + } + + keys, err := kubeClient.CoreV1().Secrets(nameSpace).Get(context.TODO(), secretName, metav1.GetOptions{}) + if err != nil { + return "", "", fmt.Errorf("couldn't get secret %v/%v", nameSpace, secretName) + } + for name, data := range keys.Data { + if name == "azurestorageaccountname" { + accountName = string(data) + } + if name == "azurestorageaccountkey" { + accountKey = string(data) + } + } + if accountName == "" || accountKey == "" { + return "", "", fmt.Errorf("invalid %v/%v, couldn't extract azurestorageaccountname or azurestorageaccountkey", nameSpace, secretName) + } + accountName = strings.TrimSpace(accountName) + return accountName, accountKey, nil +} + +func (s *azureSvc) SetAzureCredentials(host volume.VolumeHost, nameSpace, accountName, accountKey string) (string, error) { + kubeClient := host.GetKubeClient() + if kubeClient == nil { + return "", fmt.Errorf("cannot get kube client") + } + secretName := "azure-storage-account-" + accountName + "-secret" + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: nameSpace, + Name: secretName, + }, + Data: map[string][]byte{ + "azurestorageaccountname": []byte(accountName), + "azurestorageaccountkey": []byte(accountKey), + }, + Type: "Opaque", + } + _, err := kubeClient.CoreV1().Secrets(nameSpace).Create(context.TODO(), secret, metav1.CreateOptions{}) + if errors.IsAlreadyExists(err) { + err = nil + } + if err != nil { + return "", fmt.Errorf("couldn't create secret %v", err) + } + return secretName, err +} + +// check whether mountOptions contain file_mode, dir_mode, vers, gid, if not, append default mode +func appendDefaultMountOptions(mountOptions []string, fsGroup *int64) []string { + fileModeFlag := false + dirModeFlag := false + versFlag := false + gidFlag := false + actimeoFlag := false + mfsymlinksFlag := false + + for _, mountOption := range mountOptions { + if strings.HasPrefix(mountOption, fileMode) { + fileModeFlag = true + } + if strings.HasPrefix(mountOption, dirMode) { + dirModeFlag = true + } + if strings.HasPrefix(mountOption, vers) { + versFlag = true + } + if strings.HasPrefix(mountOption, gid) { + gidFlag = true + } + if strings.HasPrefix(mountOption, actimeo) { + actimeoFlag = true + } + if strings.HasPrefix(mountOption, mfsymlinks) { + mfsymlinksFlag = true + } + } + + allMountOptions := mountOptions + if !fileModeFlag { + allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%s", fileMode, defaultFileMode)) + } + + if !dirModeFlag { + allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%s", dirMode, defaultDirMode)) + } + + if !versFlag { + allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%s", vers, defaultVers)) + } + + if !gidFlag && fsGroup != nil { + allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%d", gid, *fsGroup)) + } + + if !actimeoFlag { + allMountOptions = append(allMountOptions, fmt.Sprintf("%s=%s", actimeo, defaultActimeo)) + } + + if !mfsymlinksFlag { + allMountOptions = append(allMountOptions, mfsymlinks) + } + return allMountOptions +} diff --git a/pkg/volume/azure_file/doc.go b/pkg/volume/azure_file/doc.go new file mode 100644 index 00000000000..262683625a5 --- /dev/null +++ b/pkg/volume/azure_file/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package azure_file contains the internal representation of +// Azure File Service Volume +package azure_file // import "k8s.io/kubernetes/pkg/volume/azure_file" diff --git a/pkg/volume/csi/csi_plugin.go b/pkg/volume/csi/csi_plugin.go index f528e04952b..4c26662797c 100644 --- a/pkg/volume/csi/csi_plugin.go +++ b/pkg/volume/csi/csi_plugin.go @@ -229,7 +229,7 @@ func (p *csiPlugin) Init(host volume.VolumeHost) error { return true }, csitranslationplugins.AzureFileInTreePluginName: func() bool { - return true + return utilfeature.DefaultFeatureGate.Enabled(features.CSIMigrationAzureFile) }, csitranslationplugins.VSphereInTreePluginName: func() bool { return utilfeature.DefaultFeatureGate.Enabled(features.CSIMigrationvSphere) diff --git a/pkg/volume/csimigration/plugin_manager.go b/pkg/volume/csimigration/plugin_manager.go index 3a169601ac8..2eacf54cb30 100644 --- a/pkg/volume/csimigration/plugin_manager.go +++ b/pkg/volume/csimigration/plugin_manager.go @@ -93,7 +93,7 @@ func (pm PluginManager) IsMigrationEnabledForPlugin(pluginName string) bool { case csilibplugins.GCEPDInTreePluginName: return pm.featureGate.Enabled(features.CSIMigrationGCE) case csilibplugins.AzureFileInTreePluginName: - return true + return pm.featureGate.Enabled(features.CSIMigrationAzureFile) case csilibplugins.AzureDiskInTreePluginName: return true case csilibplugins.CinderInTreePluginName: