diff --git a/pkg/api/persistentvolume/util.go b/pkg/api/persistentvolume/util.go index ede89525f58..fe6dc9b2c6f 100644 --- a/pkg/api/persistentvolume/util.go +++ b/pkg/api/persistentvolume/util.go @@ -88,8 +88,16 @@ func VisitPVSecretNames(pv *api.PersistentVolume, visitor Visitor) bool { } } case source.ISCSI != nil: - if source.ISCSI.SecretRef != nil && !visitor(getClaimRefNamespace(pv), source.ISCSI.SecretRef.Name) { - return false + if source.ISCSI.SecretRef != nil { + // previously persisted PV objects use claimRef namespace + ns := getClaimRefNamespace(pv) + if len(source.ISCSI.SecretRef.Namespace) > 0 { + // use the secret namespace if namespace is set + ns = source.ISCSI.SecretRef.Namespace + } + if !visitor(ns, source.ISCSI.SecretRef.Name) { + return false + } } case source.StorageOS != nil: if source.StorageOS.SecretRef != nil && !visitor(source.StorageOS.SecretRef.Namespace, source.StorageOS.SecretRef.Name) { diff --git a/pkg/api/persistentvolume/util_test.go b/pkg/api/persistentvolume/util_test.go index bd536e3cea0..ef8932d3403 100644 --- a/pkg/api/persistentvolume/util_test.go +++ b/pkg/api/persistentvolume/util_test.go @@ -93,8 +93,15 @@ func TestPVSecrets(t *testing.T) { {Spec: api.PersistentVolumeSpec{ ClaimRef: &api.ObjectReference{Namespace: "claimrefns", Name: "claimrefname"}, PersistentVolumeSource: api.PersistentVolumeSource{ - ISCSI: &api.ISCSIVolumeSource{ - SecretRef: &api.LocalObjectReference{ + ISCSI: &api.ISCSIPersistentVolumeSource{ + SecretRef: &api.SecretReference{ + Name: "Spec.PersistentVolumeSource.ISCSI.SecretRef", + Namespace: "iscsi"}}}}}, + {Spec: api.PersistentVolumeSpec{ + ClaimRef: &api.ObjectReference{Namespace: "claimrefns", Name: "claimrefname"}, + PersistentVolumeSource: api.PersistentVolumeSource{ + ISCSI: &api.ISCSIPersistentVolumeSource{ + SecretRef: &api.SecretReference{ Name: "Spec.PersistentVolumeSource.ISCSI.SecretRef"}}}}}, {Spec: api.PersistentVolumeSpec{ ClaimRef: &api.ObjectReference{Namespace: "claimrefns", Name: "claimrefname"}, @@ -161,6 +168,7 @@ func TestPVSecrets(t *testing.T) { "claimrefns/Spec.PersistentVolumeSource.ScaleIO.SecretRef", "scaleions/Spec.PersistentVolumeSource.ScaleIO.SecretRef", "claimrefns/Spec.PersistentVolumeSource.ISCSI.SecretRef", + "iscsi/Spec.PersistentVolumeSource.ISCSI.SecretRef", "storageosns/Spec.PersistentVolumeSource.StorageOS.SecretRef", ) if missingNames := expectedNamespacedNames.Difference(extractedNamesWithNamespace); len(missingNames) > 0 { diff --git a/pkg/apis/core/fuzzer/fuzzer.go b/pkg/apis/core/fuzzer/fuzzer.go index 4fd1e4c1b18..e3483b9a22e 100644 --- a/pkg/apis/core/fuzzer/fuzzer.go +++ b/pkg/apis/core/fuzzer/fuzzer.go @@ -245,6 +245,12 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { i.ISCSIInterface = "default" } }, + func(i *core.ISCSIPersistentVolumeSource, c fuzz.Continue) { + i.ISCSIInterface = c.RandString() + if i.ISCSIInterface == "" { + i.ISCSIInterface = "default" + } + }, func(d *core.DNSPolicy, c fuzz.Continue) { policies := []core.DNSPolicy{core.DNSClusterFirst, core.DNSDefault} *d = policies[c.Rand.Intn(len(policies))] diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index 2fe5e50a03b..ea30db48b2a 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -347,10 +347,10 @@ type PersistentVolumeSource struct { // Quobyte represents a Quobyte mount on the host that shares a pod's lifetime // +optional Quobyte *QuobyteVolumeSource - // ISCSIVolumeSource represents an ISCSI resource that is attached to a + // ISCSIPersistentVolumeSource represents an ISCSI resource that is attached to a // kubelet's host machine and then exposed to the pod. // +optional - ISCSI *ISCSIVolumeSource + ISCSI *ISCSIPersistentVolumeSource // FlexVolume represents a generic volume resource that is // provisioned/attached using an exec based plugin. This is an alpha feature and may change in future. // +optional @@ -793,6 +793,54 @@ type ISCSIVolumeSource struct { InitiatorName *string } +// ISCSIPersistentVolumeSource represents an ISCSI disk. +// ISCSI volumes can only be mounted as read/write once. +// ISCSI volumes support ownership management and SELinux relabeling. +type ISCSIPersistentVolumeSource struct { + // Required: iSCSI target portal + // the portal is either an IP or ip_addr:port if port is other than default (typically TCP ports 860 and 3260) + // +optional + TargetPortal string + // Required: target iSCSI Qualified Name + // +optional + IQN string + // Required: iSCSI target lun number + // +optional + Lun int32 + // Optional: Defaults to 'default' (tcp). iSCSI interface name that uses an iSCSI transport. + // +optional + ISCSIInterface string + // Filesystem type to mount. + // Must be a filesystem type supported by the host operating system. + // Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + // TODO: how do we prevent errors in the filesystem from compromising the machine + // +optional + FSType string + // Optional: Defaults to false (read/write). ReadOnly here will force + // the ReadOnly setting in VolumeMounts. + // +optional + ReadOnly bool + // Optional: list of iSCSI target portal ips for high availability. + // the portal is either an IP or ip_addr:port if port is other than default (typically TCP ports 860 and 3260) + // +optional + Portals []string + // Optional: whether support iSCSI Discovery CHAP authentication + // +optional + DiscoveryCHAPAuth bool + // Optional: whether support iSCSI Session CHAP authentication + // +optional + SessionCHAPAuth bool + // Optional: CHAP secret for iSCSI target and initiator authentication. + // The secret is used if either DiscoveryCHAPAuth or SessionCHAPAuth is true + // +optional + SecretRef *SecretReference + // Optional: Custom initiator name per volume. + // If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + // : will be created for the connection. + // +optional + InitiatorName *string +} + // Represents a Fibre Channel volume. // Fibre Channel volumes can only be mounted as read/write once. // Fibre Channel volumes support ownership management and SELinux relabeling. diff --git a/pkg/apis/core/v1/defaults.go b/pkg/apis/core/v1/defaults.go index 236905db55b..c2aeafc3d2f 100644 --- a/pkg/apis/core/v1/defaults.go +++ b/pkg/apis/core/v1/defaults.go @@ -249,6 +249,11 @@ func SetDefaults_ISCSIVolumeSource(obj *v1.ISCSIVolumeSource) { obj.ISCSIInterface = "default" } } +func SetDefaults_ISCSIPersistentVolumeSource(obj *v1.ISCSIPersistentVolumeSource) { + if obj.ISCSIInterface == "" { + obj.ISCSIInterface = "default" + } +} func SetDefaults_AzureDiskVolumeSource(obj *v1.AzureDiskVolumeSource) { if obj.CachingMode == nil { obj.CachingMode = new(v1.AzureDataDiskCachingMode) diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index c67f2c49066..aaccd4d2ca4 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -712,6 +712,46 @@ func validateGitRepoVolumeSource(gitRepo *core.GitRepoVolumeSource, fldPath *fie } func validateISCSIVolumeSource(iscsi *core.ISCSIVolumeSource, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(iscsi.TargetPortal) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("targetPortal"), "")) + } + if len(iscsi.IQN) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("iqn"), "")) + } else { + if !strings.HasPrefix(iscsi.IQN, "iqn") && !strings.HasPrefix(iscsi.IQN, "eui") && !strings.HasPrefix(iscsi.IQN, "naa") { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format starting with iqn, eui, or naa")) + } else if strings.HasPrefix(iscsi.IQN, "iqn") && !iscsiInitiatorIqnRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } else if strings.HasPrefix(iscsi.IQN, "eui") && !iscsiInitiatorEuiRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } else if strings.HasPrefix(iscsi.IQN, "naa") && !iscsiInitiatorNaaRegex.MatchString(iscsi.IQN) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("iqn"), iscsi.IQN, "must be valid format")) + } + } + if iscsi.Lun < 0 || iscsi.Lun > 255 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("lun"), iscsi.Lun, validation.InclusiveRangeError(0, 255))) + } + if (iscsi.DiscoveryCHAPAuth || iscsi.SessionCHAPAuth) && iscsi.SecretRef == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("secretRef"), "")) + } + if iscsi.InitiatorName != nil { + initiator := *iscsi.InitiatorName + if !strings.HasPrefix(initiator, "iqn") && !strings.HasPrefix(initiator, "eui") && !strings.HasPrefix(initiator, "naa") { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format starting with iqn, eui, or naa")) + } + if strings.HasPrefix(initiator, "iqn") && !iscsiInitiatorIqnRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } else if strings.HasPrefix(initiator, "eui") && !iscsiInitiatorEuiRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } else if strings.HasPrefix(initiator, "naa") && !iscsiInitiatorNaaRegex.MatchString(initiator) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("initiatorname"), initiator, "must be valid format")) + } + } + return allErrs +} + +func validateISCSIPersistentVolumeSource(iscsi *core.ISCSIPersistentVolumeSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if len(iscsi.TargetPortal) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("targetPortal"), "")) @@ -735,6 +775,11 @@ func validateISCSIVolumeSource(iscsi *core.ISCSIVolumeSource, fldPath *field.Pat if (iscsi.DiscoveryCHAPAuth || iscsi.SessionCHAPAuth) && iscsi.SecretRef == nil { allErrs = append(allErrs, field.Required(fldPath.Child("secretRef"), "")) } + if iscsi.SecretRef != nil { + if len(iscsi.SecretRef.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("secretRef", "name"), "")) + } + } if iscsi.InitiatorName != nil { initiator := *iscsi.InitiatorName if !strings.HasPrefix(initiator, "iqn") && !strings.HasPrefix(initiator, "eui") && !strings.HasPrefix(initiator, "naa") { @@ -1517,7 +1562,7 @@ func ValidatePersistentVolume(pv *core.PersistentVolume) field.ErrorList { allErrs = append(allErrs, field.Forbidden(specPath.Child("iscsi"), "may not specify more than 1 volume type")) } else { numVolumes++ - allErrs = append(allErrs, validateISCSIVolumeSource(pv.Spec.ISCSI, specPath.Child("iscsi"))...) + allErrs = append(allErrs, validateISCSIPersistentVolumeSource(pv.Spec.ISCSI, specPath.Child("iscsi"))...) } if pv.Spec.ISCSI.InitiatorName != nil && len(pv.ObjectMeta.Name+":"+pv.Spec.ISCSI.TargetPortal) > 64 { tooLongErr := "Total length of : must be under 64 characters if iscsi.initiatorName is specified." diff --git a/pkg/printers/internalversion/describe.go b/pkg/printers/internalversion/describe.go index 4d660de81ec..b8f5a6d82ac 100644 --- a/pkg/printers/internalversion/describe.go +++ b/pkg/printers/internalversion/describe.go @@ -856,6 +856,26 @@ func printISCSIVolumeSource(iscsi *api.ISCSIVolumeSource, w PrefixWriter) { iscsi.TargetPortal, iscsi.IQN, iscsi.Lun, iscsi.ISCSIInterface, iscsi.FSType, iscsi.ReadOnly, iscsi.Portals, iscsi.DiscoveryCHAPAuth, iscsi.SessionCHAPAuth, iscsi.SecretRef, initiator) } +func printISCSIPersistentVolumeSource(iscsi *api.ISCSIPersistentVolumeSource, w PrefixWriter) { + initiatorName := "" + if iscsi.InitiatorName != nil { + initiatorName = *iscsi.InitiatorName + } + w.Write(LEVEL_2, "Type:\tISCSI (an ISCSI Disk resource that is attached to a kubelet's host machine and then exposed to the pod)\n"+ + " TargetPortal:\t%v\n"+ + " IQN:\t%v\n"+ + " Lun:\t%v\n"+ + " ISCSIInterface\t%v\n"+ + " FSType:\t%v\n"+ + " ReadOnly:\t%v\n"+ + " Portals:\t%v\n"+ + " DiscoveryCHAPAuth:\t%v\n"+ + " SessionCHAPAuth:\t%v\n"+ + " SecretRef:\t%v\n"+ + " InitiatorName:\t%v\n", + iscsi.TargetPortal, iscsi.IQN, iscsi.Lun, iscsi.ISCSIInterface, iscsi.FSType, iscsi.ReadOnly, iscsi.Portals, iscsi.DiscoveryCHAPAuth, iscsi.SessionCHAPAuth, iscsi.SecretRef, initiatorName) +} + func printGlusterfsVolumeSource(glusterfs *api.GlusterfsVolumeSource, w PrefixWriter) { w.Write(LEVEL_2, "Type:\tGlusterfs (a Glusterfs mount on the host that shares a pod's lifetime)\n"+ " EndpointsName:\t%v\n"+ @@ -1131,7 +1151,7 @@ func describePersistentVolume(pv *api.PersistentVolume, events *api.EventList) ( case pv.Spec.NFS != nil: printNFSVolumeSource(pv.Spec.NFS, w) case pv.Spec.ISCSI != nil: - printISCSIVolumeSource(pv.Spec.ISCSI, w) + printISCSIPersistentVolumeSource(pv.Spec.ISCSI, w) case pv.Spec.Glusterfs != nil: printGlusterfsVolumeSource(pv.Spec.Glusterfs, w) case pv.Spec.RBD != nil: diff --git a/pkg/printers/internalversion/describe_test.go b/pkg/printers/internalversion/describe_test.go index 75030fea5df..20891b2aae7 100644 --- a/pkg/printers/internalversion/describe_test.go +++ b/pkg/printers/internalversion/describe_test.go @@ -853,7 +853,7 @@ func TestPersistentVolumeDescriber(t *testing.T) { ObjectMeta: metav1.ObjectMeta{Name: "bar"}, Spec: api.PersistentVolumeSpec{ PersistentVolumeSource: api.PersistentVolumeSource{ - ISCSI: &api.ISCSIVolumeSource{}, + ISCSI: &api.ISCSIPersistentVolumeSource{}, }, }, }, diff --git a/pkg/volume/iscsi/BUILD b/pkg/volume/iscsi/BUILD index 8e75e24cae8..3c7a7598136 100644 --- a/pkg/volume/iscsi/BUILD +++ b/pkg/volume/iscsi/BUILD @@ -23,6 +23,7 @@ go_library( "//pkg/volume/util:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", ], ) diff --git a/pkg/volume/iscsi/attacher.go b/pkg/volume/iscsi/attacher.go index 8158a0ab042..c039a70db8c 100644 --- a/pkg/volume/iscsi/attacher.go +++ b/pkg/volume/iscsi/attacher.go @@ -24,6 +24,7 @@ import ( "github.com/golang/glog" "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/volume" @@ -100,7 +101,7 @@ func (attacher *iscsiAttacher) MountDevice(spec *volume.Spec, devicePath string, return err } } - volumeSource, readOnly, err := getVolumeSource(spec) + readOnly, fsType, err := getISCSIVolumeInfo(spec) if err != nil { return err } @@ -112,7 +113,7 @@ func (attacher *iscsiAttacher) MountDevice(spec *volume.Spec, devicePath string, if notMnt { diskMounter := &mount.SafeFormatAndMount{Interface: mounter, Exec: attacher.host.GetExec(iscsiPluginName)} mountOptions := volume.MountOptionFromSpec(spec, options...) - err = diskMounter.FormatAndMount(devicePath, deviceMountPath, volumeSource.FSType, mountOptions) + err = diskMounter.FormatAndMount(devicePath, deviceMountPath, fsType, mountOptions) if err != nil { os.Remove(deviceMountPath) return err @@ -159,29 +160,75 @@ func (detacher *iscsiDetacher) UnmountDevice(deviceMountPath string) error { func (attacher *iscsiAttacher) volumeSpecToMounter(spec *volume.Spec, host volume.VolumeHost, pod *v1.Pod) (*iscsiDiskMounter, error) { var secret map[string]string var bkportal []string - iscsi, readOnly, err := getVolumeSource(spec) + readOnly, fsType, err := getISCSIVolumeInfo(spec) if err != nil { return nil, err } - // Obtain secret for AttachDisk - if iscsi.SecretRef != nil && pod != nil { - if secret, err = volumeutil.GetSecretForPod(pod, iscsi.SecretRef.Name, host.GetKubeClient()); err != nil { - glog.Errorf("Couldn't get secret from %v/%v", pod.Namespace, iscsi.SecretRef) + if pod != nil { + chapDiscovery, err := getISCSIDiscoveryCHAPInfo(spec) + if err != nil { return nil, err } + chapSession, err := getISCSISessionCHAPInfo(spec) + if err != nil { + return nil, err + } + if chapDiscovery || chapSession { + secretName, secretNamespace, err := getISCSISecretNameAndNamespace(spec, pod.Namespace) + if err != nil { + return nil, err + } + if len(secretNamespace) == 0 || len(secretName) == 0 { + return nil, fmt.Errorf("CHAP enabled but secret name or namespace is empty") + } + // if secret is provided, retrieve it + kubeClient := host.GetKubeClient() + if kubeClient == nil { + return nil, fmt.Errorf("Cannot get kube client") + } + secretObj, err := kubeClient.Core().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + err = fmt.Errorf("Couldn't get secret %v/%v error: %v", secretNamespace, secretName, err) + return nil, err + } + secret = make(map[string]string) + for name, data := range secretObj.Data { + glog.V(6).Infof("retrieving CHAP secret name: %s", name) + secret[name] = string(data) + } + } + } - lun := strconv.Itoa(int(iscsi.Lun)) - portal := portalMounter(iscsi.TargetPortal) + tp, portals, iqn, lunStr, err := getISCSITargetInfo(spec) + if err != nil { + return nil, err + } + + lun := strconv.Itoa(int(lunStr)) + portal := portalMounter(tp) bkportal = append(bkportal, portal) - for _, tp := range iscsi.Portals { - bkportal = append(bkportal, portalMounter(string(tp))) + for _, p := range portals { + bkportal = append(bkportal, portalMounter(string(p))) } - iface := iscsi.ISCSIInterface - exec := attacher.host.GetExec(iscsiPluginName) + + iface, initiatorNamePtr, err := getISCSIInitiatorInfo(spec) + if err != nil { + return nil, err + } + var initiatorName string - if iscsi.InitiatorName != nil { - initiatorName = *iscsi.InitiatorName + if initiatorNamePtr != nil { + initiatorName = *initiatorNamePtr } + chapDiscovery, err := getISCSIDiscoveryCHAPInfo(spec) + if err != nil { + return nil, err + } + chapSession, err := getISCSISessionCHAPInfo(spec) + if err != nil { + return nil, err + } + exec := attacher.host.GetExec(iscsiPluginName) return &iscsiDiskMounter{ iscsiDisk: &iscsiDisk{ @@ -190,15 +237,15 @@ func (attacher *iscsiAttacher) volumeSpecToMounter(spec *volume.Spec, host volum }, VolName: spec.Name(), Portals: bkportal, - Iqn: iscsi.IQN, + Iqn: iqn, lun: lun, Iface: iface, - chap_discovery: iscsi.DiscoveryCHAPAuth, - chap_session: iscsi.SessionCHAPAuth, + chap_discovery: chapDiscovery, + chap_session: chapSession, secret: secret, InitiatorName: initiatorName, manager: &ISCSIUtil{}}, - fsType: iscsi.FSType, + fsType: fsType, readOnly: readOnly, mounter: &mount.SafeFormatAndMount{Interface: host.GetMounter(iscsiPluginName), Exec: exec}, exec: exec, diff --git a/pkg/volume/iscsi/iscsi.go b/pkg/volume/iscsi/iscsi.go index 055fbf8a9c1..a7893ec7218 100644 --- a/pkg/volume/iscsi/iscsi.go +++ b/pkg/volume/iscsi/iscsi.go @@ -23,6 +23,7 @@ import ( "github.com/golang/glog" "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/util/mount" utilstrings "k8s.io/kubernetes/pkg/util/strings" @@ -56,16 +57,12 @@ func (plugin *iscsiPlugin) GetPluginName() string { } func (plugin *iscsiPlugin) GetVolumeName(spec *volume.Spec) (string, error) { - volumeSource, _, err := getVolumeSource(spec) + tp, _, iqn, lun, err := getISCSITargetInfo(spec) if err != nil { return "", err } - return fmt.Sprintf( - "%v:%v:%v", - volumeSource.TargetPortal, - volumeSource.IQN, - volumeSource.Lun), nil + return fmt.Sprintf("%v:%v:%v", tp, iqn, lun), nil } func (plugin *iscsiPlugin) CanSupport(spec *volume.Spec) bool { @@ -98,41 +95,80 @@ func (plugin *iscsiPlugin) GetAccessModes() []v1.PersistentVolumeAccessMode { func (plugin *iscsiPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) { // Inject real implementations here, test through the internal function. var secret map[string]string - source, _, err := getVolumeSource(spec) + if pod == nil { + return nil, fmt.Errorf("nil pod") + } + chapDiscover, err := getISCSIDiscoveryCHAPInfo(spec) if err != nil { return nil, err } - - if source.SecretRef != nil { - if secret, err = ioutil.GetSecretForPod(pod, source.SecretRef.Name, plugin.host.GetKubeClient()); err != nil { - glog.Errorf("Couldn't get secret from %v/%v", pod.Namespace, source.SecretRef) + chapSession, err := getISCSISessionCHAPInfo(spec) + if err != nil { + return nil, err + } + if chapDiscover || chapSession { + secretName, secretNamespace, err := getISCSISecretNameAndNamespace(spec, pod.Namespace) + if err != nil { return nil, err } - } + if len(secretName) > 0 && len(secretNamespace) > 0 { + // if secret is provideded, retrieve it + kubeClient := plugin.host.GetKubeClient() + if kubeClient == nil { + return nil, fmt.Errorf("Cannot get kube client") + } + secretObj, err := kubeClient.Core().Secrets(secretNamespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + err = fmt.Errorf("Couldn't get secret %v/%v error: %v", secretNamespace, secretName, err) + return nil, err + } + secret = make(map[string]string) + for name, data := range secretObj.Data { + glog.V(4).Infof("retrieving CHAP secret name: %s", name) + secret[name] = string(data) + } + } + } return plugin.newMounterInternal(spec, pod.UID, &ISCSIUtil{}, plugin.host.GetMounter(plugin.GetPluginName()), plugin.host.GetExec(plugin.GetPluginName()), secret) } func (plugin *iscsiPlugin) newMounterInternal(spec *volume.Spec, podUID types.UID, manager diskManager, mounter mount.Interface, exec mount.Exec, secret map[string]string) (volume.Mounter, error) { // iscsi volumes used directly in a pod have a ReadOnly flag set by the pod author. // iscsi volumes used as a PersistentVolume gets the ReadOnly flag indirectly through the persistent-claim volume used to mount the PV - iscsi, readOnly, err := getVolumeSource(spec) + readOnly, fsType, err := getISCSIVolumeInfo(spec) + if err != nil { + return nil, err + } + tp, portals, iqn, lunStr, err := getISCSITargetInfo(spec) if err != nil { return nil, err } - lun := strconv.Itoa(int(iscsi.Lun)) - portal := portalMounter(iscsi.TargetPortal) + lun := strconv.Itoa(int(lunStr)) + portal := portalMounter(tp) var bkportal []string bkportal = append(bkportal, portal) - for _, tp := range iscsi.Portals { - bkportal = append(bkportal, portalMounter(string(tp))) + for _, p := range portals { + bkportal = append(bkportal, portalMounter(string(p))) + } + + iface, initiatorNamePtr, err := getISCSIInitiatorInfo(spec) + if err != nil { + return nil, err } - iface := iscsi.ISCSIInterface var initiatorName string - if iscsi.InitiatorName != nil { - initiatorName = *iscsi.InitiatorName + if initiatorNamePtr != nil { + initiatorName = *initiatorNamePtr + } + chapDiscovery, err := getISCSIDiscoveryCHAPInfo(spec) + if err != nil { + return nil, err + } + chapSession, err := getISCSISessionCHAPInfo(spec) + if err != nil { + return nil, err } return &iscsiDiskMounter{ @@ -140,16 +176,16 @@ func (plugin *iscsiPlugin) newMounterInternal(spec *volume.Spec, podUID types.UI podUID: podUID, VolName: spec.Name(), Portals: bkportal, - Iqn: iscsi.IQN, + Iqn: iqn, lun: lun, Iface: iface, - chap_discovery: iscsi.DiscoveryCHAPAuth, - chap_session: iscsi.SessionCHAPAuth, + chap_discovery: chapDiscovery, + chap_session: chapSession, secret: secret, InitiatorName: initiatorName, manager: manager, plugin: plugin}, - fsType: iscsi.FSType, + fsType: fsType, readOnly: readOnly, mounter: &mount.SafeFormatAndMount{Interface: mounter, Exec: exec}, exec: exec, @@ -277,13 +313,87 @@ func portalMounter(portal string) string { return portal } -func getVolumeSource(spec *volume.Spec) (*v1.ISCSIVolumeSource, bool, error) { +// get iSCSI volume info: readOnly and fstype +func getISCSIVolumeInfo(spec *volume.Spec) (bool, string, error) { + // for volume source, readonly is in volume spec + // for PV, readonly is in PV spec if spec.Volume != nil && spec.Volume.ISCSI != nil { - return spec.Volume.ISCSI, spec.Volume.ISCSI.ReadOnly, nil + return spec.Volume.ISCSI.ReadOnly, spec.Volume.ISCSI.FSType, nil } else if spec.PersistentVolume != nil && spec.PersistentVolume.Spec.ISCSI != nil { - return spec.PersistentVolume.Spec.ISCSI, spec.ReadOnly, nil + return spec.ReadOnly, spec.PersistentVolume.Spec.ISCSI.FSType, nil } - return nil, false, fmt.Errorf("Spec does not reference an ISCSI volume type") + return false, "", fmt.Errorf("Spec does not reference an ISCSI volume type") +} + +// get iSCSI target info: target portal, portals, iqn, and lun +func getISCSITargetInfo(spec *volume.Spec) (string, []string, string, int32, error) { + if spec.Volume != nil && spec.Volume.ISCSI != nil { + return spec.Volume.ISCSI.TargetPortal, spec.Volume.ISCSI.Portals, spec.Volume.ISCSI.IQN, spec.Volume.ISCSI.Lun, nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.ISCSI != nil { + return spec.PersistentVolume.Spec.ISCSI.TargetPortal, spec.PersistentVolume.Spec.ISCSI.Portals, spec.PersistentVolume.Spec.ISCSI.IQN, spec.PersistentVolume.Spec.ISCSI.Lun, nil + } + + return "", nil, "", 0, fmt.Errorf("Spec does not reference an ISCSI volume type") +} + +// get iSCSI initiator info: iface and initiator name +func getISCSIInitiatorInfo(spec *volume.Spec) (string, *string, error) { + if spec.Volume != nil && spec.Volume.ISCSI != nil { + return spec.Volume.ISCSI.ISCSIInterface, spec.Volume.ISCSI.InitiatorName, nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.ISCSI != nil { + return spec.PersistentVolume.Spec.ISCSI.ISCSIInterface, spec.PersistentVolume.Spec.ISCSI.InitiatorName, nil + } + + return "", nil, fmt.Errorf("Spec does not reference an ISCSI volume type") +} + +// get iSCSI Discovery CHAP boolean +func getISCSIDiscoveryCHAPInfo(spec *volume.Spec) (bool, error) { + if spec.Volume != nil && spec.Volume.ISCSI != nil { + return spec.Volume.ISCSI.DiscoveryCHAPAuth, nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.ISCSI != nil { + return spec.PersistentVolume.Spec.ISCSI.DiscoveryCHAPAuth, nil + } + + return false, fmt.Errorf("Spec does not reference an ISCSI volume type") +} + +// get iSCSI Session CHAP boolean +func getISCSISessionCHAPInfo(spec *volume.Spec) (bool, error) { + if spec.Volume != nil && spec.Volume.ISCSI != nil { + return spec.Volume.ISCSI.SessionCHAPAuth, nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.ISCSI != nil { + return spec.PersistentVolume.Spec.ISCSI.SessionCHAPAuth, nil + } + + return false, fmt.Errorf("Spec does not reference an ISCSI volume type") +} + +// get iSCSI CHAP Secret info: secret name and namespace +func getISCSISecretNameAndNamespace(spec *volume.Spec, defaultSecretNamespace string) (string, string, error) { + if spec.Volume != nil && spec.Volume.ISCSI != nil { + if spec.Volume.ISCSI.SecretRef != nil { + return spec.Volume.ISCSI.SecretRef.Name, defaultSecretNamespace, nil + } + return "", "", nil + } else if spec.PersistentVolume != nil && + spec.PersistentVolume.Spec.ISCSI != nil { + secretRef := spec.PersistentVolume.Spec.ISCSI.SecretRef + secretNs := defaultSecretNamespace + if secretRef != nil { + if len(secretRef.Namespace) != 0 { + secretNs = secretRef.Namespace + } + return secretRef.Name, secretNs, nil + } + return "", "", nil + } + + return "", "", fmt.Errorf("Spec does not reference an ISCSI volume type") } diff --git a/pkg/volume/iscsi/iscsi_test.go b/pkg/volume/iscsi/iscsi_test.go index d91564740fb..831cd564395 100644 --- a/pkg/volume/iscsi/iscsi_test.go +++ b/pkg/volume/iscsi/iscsi_test.go @@ -205,7 +205,7 @@ func TestPluginPersistentVolume(t *testing.T) { }, Spec: v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ - ISCSI: &v1.ISCSIVolumeSource{ + ISCSI: &v1.ISCSIPersistentVolumeSource{ TargetPortal: "127.0.0.1:3260", IQN: "iqn.2014-12.server:storage.target01", FSType: "ext4", @@ -230,7 +230,7 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { }, Spec: v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ - ISCSI: &v1.ISCSIVolumeSource{ + ISCSI: &v1.ISCSIPersistentVolumeSource{ TargetPortal: "127.0.0.1:3260", IQN: "iqn.2014-12.server:storage.target01", FSType: "ext4", @@ -283,3 +283,146 @@ func TestPortalMounter(t *testing.T) { t.Errorf("wrong portal: %s", portal) } } + +type testcase struct { + name string + defaultNs string + spec *volume.Spec + // Expected return of the test + expectedName string + expectedNs string + expectedIface string + expectedError error +} + +func TestGetSecretNameAndNamespaceForPV(t *testing.T) { + tests := []testcase{ + { + name: "persistent volume source", + defaultNs: "default", + spec: &volume.Spec{ + PersistentVolume: &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + ISCSI: &v1.ISCSIPersistentVolumeSource{ + TargetPortal: "127.0.0.1:3260", + IQN: "iqn.2014-12.server:storage.target01", + FSType: "ext4", + Lun: 0, + SecretRef: &v1.SecretReference{ + Name: "name", + Namespace: "ns", + }, + }, + }, + }, + }, + }, + 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{ + ISCSI: &v1.ISCSIPersistentVolumeSource{ + TargetPortal: "127.0.0.1:3260", + IQN: "iqn.2014-12.server:storage.target01", + FSType: "ext4", + Lun: 0, + SecretRef: &v1.SecretReference{ + Name: "name", + }, + }, + }, + }, + }, + }, + expectedName: "name", + expectedNs: "default", + expectedError: nil, + }, + { + name: "pod volume source", + defaultNs: "default", + spec: &volume.Spec{ + Volume: &v1.Volume{ + VolumeSource: v1.VolumeSource{ + ISCSI: &v1.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1:3260", + IQN: "iqn.2014-12.server:storage.target01", + FSType: "ext4", + Lun: 0, + }, + }, + }, + }, + expectedName: "", + expectedNs: "", + expectedError: nil, + }, + } + for _, testcase := range tests { + resultName, resultNs, err := getISCSISecretNameAndNamespace(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 TestGetISCSIInitiatorInfo(t *testing.T) { + tests := []testcase{ + { + name: "persistent volume source", + spec: &volume.Spec{ + PersistentVolume: &v1.PersistentVolume{ + Spec: v1.PersistentVolumeSpec{ + PersistentVolumeSource: v1.PersistentVolumeSource{ + ISCSI: &v1.ISCSIPersistentVolumeSource{ + TargetPortal: "127.0.0.1:3260", + IQN: "iqn.2014-12.server:storage.target01", + FSType: "ext4", + Lun: 0, + ISCSIInterface: "tcp", + }, + }, + }, + }, + }, + expectedIface: "tcp", + expectedError: nil, + }, + { + name: "pod volume source", + spec: &volume.Spec{ + Volume: &v1.Volume{ + VolumeSource: v1.VolumeSource{ + ISCSI: &v1.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1:3260", + IQN: "iqn.2014-12.server:storage.target01", + FSType: "ext4", + Lun: 0, + ISCSIInterface: "tcp", + }, + }, + }, + }, + expectedIface: "tcp", + expectedError: nil, + }, + } + for _, testcase := range tests { + resultIface, _, err := getISCSIInitiatorInfo(testcase.spec) + if err != testcase.expectedError || resultIface != testcase.expectedIface { + t.Errorf("%s failed: expected err=%v iface=%s, got %v/%s", testcase.name, testcase.expectedError, testcase.expectedIface, + err, resultIface) + } + } + +} diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 900454c6963..5ba5c6682f6 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -402,7 +402,7 @@ type PersistentVolumeSource struct { // ISCSI represents an ISCSI Disk resource that is attached to a // kubelet's host machine and then exposed to the pod. Provisioned by an admin. // +optional - ISCSI *ISCSIVolumeSource `json:"iscsi,omitempty" protobuf:"bytes,7,opt,name=iscsi"` + ISCSI *ISCSIPersistentVolumeSource `json:"iscsi,omitempty" protobuf:"bytes,7,opt,name=iscsi"` // Cinder represents a cinder volume attached and mounted on kubelets host machine // More info: https://releases.k8s.io/HEAD/examples/mysql-cinder-pd/README.md // +optional @@ -1236,14 +1236,15 @@ type NFSVolumeSource struct { // ISCSI volumes can only be mounted as read/write once. // ISCSI volumes support ownership management and SELinux relabeling. type ISCSIVolumeSource struct { - // iSCSI target portal. The portal is either an IP or ip_addr:port if the port + // iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port // is other than default (typically TCP ports 860 and 3260). TargetPortal string `json:"targetPortal" protobuf:"bytes,1,opt,name=targetPortal"` // Target iSCSI Qualified Name. IQN string `json:"iqn" protobuf:"bytes,2,opt,name=iqn"` - // iSCSI target lun number. + // iSCSI Target Lun number. Lun int32 `json:"lun" protobuf:"varint,3,opt,name=lun"` - // Optional: Defaults to 'default' (tcp). iSCSI interface name that uses an iSCSI transport. + // iSCSI Interface Name that uses an iSCSI transport. + // Defaults to 'default' (tcp). // +optional ISCSIInterface string `json:"iscsiInterface,omitempty" protobuf:"bytes,4,opt,name=iscsiInterface"` // Filesystem type of the volume that you want to mount. @@ -1257,7 +1258,7 @@ type ISCSIVolumeSource struct { // Defaults to false. // +optional ReadOnly bool `json:"readOnly,omitempty" protobuf:"varint,6,opt,name=readOnly"` - // iSCSI target portal List. The portal is either an IP or ip_addr:port if the port + // iSCSI Target Portal List. The portal is either an IP or ip_addr:port if the port // is other than default (typically TCP ports 860 and 3260). // +optional Portals []string `json:"portals,omitempty" protobuf:"bytes,7,opt,name=portals"` @@ -1267,10 +1268,56 @@ type ISCSIVolumeSource struct { // whether support iSCSI Session CHAP authentication // +optional SessionCHAPAuth bool `json:"chapAuthSession,omitempty" protobuf:"varint,11,opt,name=chapAuthSession"` - // CHAP secret for iSCSI target and initiator authentication + // CHAP Secret for iSCSI target and initiator authentication // +optional SecretRef *LocalObjectReference `json:"secretRef,omitempty" protobuf:"bytes,10,opt,name=secretRef"` - // Custom iSCSI initiator name. + // Custom iSCSI Initiator Name. + // If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface + // : will be created for the connection. + // +optional + InitiatorName *string `json:"initiatorName,omitempty" protobuf:"bytes,12,opt,name=initiatorName"` +} + +// ISCSIPersistentVolumeSource represents an ISCSI disk. +// ISCSI volumes can only be mounted as read/write once. +// ISCSI volumes support ownership management and SELinux relabeling. +type ISCSIPersistentVolumeSource struct { + // iSCSI Target Portal. The Portal is either an IP or ip_addr:port if the port + // is other than default (typically TCP ports 860 and 3260). + TargetPortal string `json:"targetPortal" protobuf:"bytes,1,opt,name=targetPortal"` + // Target iSCSI Qualified Name. + IQN string `json:"iqn" protobuf:"bytes,2,opt,name=iqn"` + // iSCSI Target Lun number. + Lun int32 `json:"lun" protobuf:"varint,3,opt,name=lun"` + // iSCSI Interface Name that uses an iSCSI transport. + // Defaults to 'default' (tcp). + // +optional + ISCSIInterface string `json:"iscsiInterface,omitempty" protobuf:"bytes,4,opt,name=iscsiInterface"` + // Filesystem type of the volume that you want to mount. + // Tip: Ensure that the filesystem type is supported by the host operating system. + // Examples: "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + // More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi + // TODO: how do we prevent errors in the filesystem from compromising the machine + // +optional + FSType string `json:"fsType,omitempty" protobuf:"bytes,5,opt,name=fsType"` + // ReadOnly here will force the ReadOnly setting in VolumeMounts. + // Defaults to false. + // +optional + ReadOnly bool `json:"readOnly,omitempty" protobuf:"varint,6,opt,name=readOnly"` + // iSCSI Target Portal List. The Portal is either an IP or ip_addr:port if the port + // is other than default (typically TCP ports 860 and 3260). + // +optional + Portals []string `json:"portals,omitempty" protobuf:"bytes,7,opt,name=portals"` + // whether support iSCSI Discovery CHAP authentication + // +optional + DiscoveryCHAPAuth bool `json:"chapAuthDiscovery,omitempty" protobuf:"varint,8,opt,name=chapAuthDiscovery"` + // whether support iSCSI Session CHAP authentication + // +optional + SessionCHAPAuth bool `json:"chapAuthSession,omitempty" protobuf:"varint,11,opt,name=chapAuthSession"` + // CHAP Secret for iSCSI target and initiator authentication + // +optional + SecretRef *SecretReference `json:"secretRef,omitempty" protobuf:"bytes,10,opt,name=secretRef"` + // Custom iSCSI Initiator Name. // If initiatorName is specified with iscsiInterface simultaneously, new iSCSI interface // : will be created for the connection. // +optional