Commit d4087d0b authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #35022 from jsafrane/gluster-secrets2

Automatic merge from submit-queue Remove PV annotations for Gluster provisioner. Don't store Gluster SotrageClass parameters in annotations, it's insecure. Instead, expect that there is the StorageClass available at the time when it's needed by Gluster deleter. See #34822 for detais @kubernetes/sig-storage
parents 5133e1f0 2b2508ba
...@@ -403,21 +403,16 @@ func (d *glusterfsVolumeDeleter) Delete() error { ...@@ -403,21 +403,16 @@ func (d *glusterfsVolumeDeleter) Delete() error {
volumeName := d.glusterfsMounter.path volumeName := d.glusterfsMounter.path
volumeId := dstrings.TrimPrefix(volumeName, volprefix) volumeId := dstrings.TrimPrefix(volumeName, volprefix)
err = d.annotationsToParam(d.spec) class, err := volutil.GetClassForVolume(d.plugin.host.GetKubeClient(), d.spec)
if err != nil { if err != nil {
return err return err
} }
if len(d.secretName) > 0 {
d.secretValue, err = parseSecret(d.secretNamespace, d.secretName, d.plugin.host.GetKubeClient()) cfg, err := parseClassParameters(class.Parameters, d.plugin.host.GetKubeClient())
if err != nil { if err != nil {
glog.Errorf("glusterfs: failed to read secret: %v", err) return err
return err
}
} else if len(d.userKey) > 0 {
d.secretValue = d.userKey
} else {
d.secretValue = ""
} }
d.provisioningConfig = *cfg
glog.V(4).Infof("glusterfs: deleting volume %q with configuration %+v", volumeId, d.provisioningConfig) glog.V(4).Infof("glusterfs: deleting volume %q with configuration %+v", volumeId, d.provisioningConfig)
...@@ -444,56 +439,11 @@ func (r *glusterfsVolumeProvisioner) Provision() (*api.PersistentVolume, error) ...@@ -444,56 +439,11 @@ func (r *glusterfsVolumeProvisioner) Provision() (*api.PersistentVolume, error)
} }
glog.V(4).Infof("glusterfs: Provison VolumeOptions %v", r.options) glog.V(4).Infof("glusterfs: Provison VolumeOptions %v", r.options)
authEnabled := true cfg, err := parseClassParameters(r.options.Parameters, r.plugin.host.GetKubeClient())
for k, v := range r.options.Parameters { if err != nil {
switch dstrings.ToLower(k) { return nil, err
case "endpoint":
r.endpoint = v
case "resturl":
r.url = v
case "restuser":
r.user = v
case "restuserkey":
r.userKey = v
case "secretname":
r.secretName = v
case "secretnamespace":
r.secretNamespace = v
case "restauthenabled":
authEnabled = dstrings.ToLower(v) == "true"
default:
return nil, fmt.Errorf("glusterfs: invalid option %q for volume plugin %s", k, r.plugin.GetPluginName())
}
}
if len(r.url) == 0 {
return nil, fmt.Errorf("StorageClass for provisioner %q must contain 'resturl' parameter", r.plugin.GetPluginName())
}
if len(r.endpoint) == 0 {
return nil, fmt.Errorf("StorageClass for provisioner %q must contain 'endpoint' parameter", r.plugin.GetPluginName())
}
if !authEnabled {
r.user = ""
r.secretName = ""
r.secretNamespace = ""
r.userKey = ""
r.secretValue = ""
}
if len(r.secretName) != 0 || len(r.secretNamespace) != 0 {
// secretName + Namespace has precedence over userKey
if len(r.secretName) != 0 && len(r.secretNamespace) != 0 {
r.secretValue, err = parseSecret(r.secretNamespace, r.secretName, r.plugin.host.GetKubeClient())
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("StorageClass for provisioner %q must have secretNamespace and secretName either both set or both empty", r.plugin.GetPluginName())
}
} else {
r.secretValue = r.userKey
} }
r.provisioningConfig = *cfg
glog.V(4).Infof("glusterfs: creating volume with configuration %+v", r.provisioningConfig) glog.V(4).Infof("glusterfs: creating volume with configuration %+v", r.provisioningConfig)
glusterfs, sizeGB, err := r.CreateVolume() glusterfs, sizeGB, err := r.CreateVolume()
...@@ -511,7 +461,6 @@ func (r *glusterfsVolumeProvisioner) Provision() (*api.PersistentVolume, error) ...@@ -511,7 +461,6 @@ func (r *glusterfsVolumeProvisioner) Provision() (*api.PersistentVolume, error)
pv.Spec.Capacity = api.ResourceList{ pv.Spec.Capacity = api.ResourceList{
api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGB)), api.ResourceName(api.ResourceStorage): resource.MustParse(fmt.Sprintf("%dGi", sizeGB)),
} }
r.paramToAnnotations(pv)
return pv, nil return pv, nil
} }
...@@ -564,37 +513,60 @@ func parseSecret(namespace, secretName string, kubeClient clientset.Interface) ( ...@@ -564,37 +513,60 @@ func parseSecret(namespace, secretName string, kubeClient clientset.Interface) (
return secret, nil return secret, nil
} }
// paramToAnnotations stores parameters needed to delete the volume in the PV // parseClassParameters parses StorageClass.Parameters
// annotations. func parseClassParameters(params map[string]string, kubeClient clientset.Interface) (*provisioningConfig, error) {
func (p *glusterfsVolumeProvisioner) paramToAnnotations(pv *api.PersistentVolume) { var cfg provisioningConfig
ann := map[string]string{ var err error
annGlusterURL: p.url,
annGlusterUser: p.user, authEnabled := true
annGlusterSecretName: p.secretName, for k, v := range params {
annGlusterSecretNamespace: p.secretNamespace, switch dstrings.ToLower(k) {
annGlusterUserKey: p.userKey, case "endpoint":
cfg.endpoint = v
case "resturl":
cfg.url = v
case "restuser":
cfg.user = v
case "restuserkey":
cfg.userKey = v
case "secretname":
cfg.secretName = v
case "secretnamespace":
cfg.secretNamespace = v
case "restauthenabled":
authEnabled = dstrings.ToLower(v) == "true"
default:
return nil, fmt.Errorf("glusterfs: invalid option %q for volume plugin %s", k, glusterfsPluginName)
}
} }
volutil.AddVolumeAnnotations(pv, ann)
}
// annotationsToParam parses annotations stored by paramToAnnotations if len(cfg.url) == 0 {
func (d *glusterfsVolumeDeleter) annotationsToParam(pv *api.PersistentVolume) error { return nil, fmt.Errorf("StorageClass for provisioner %s must contain 'resturl' parameter", glusterfsPluginName)
annKeys := []string{
annGlusterSecretName,
annGlusterSecretNamespace,
annGlusterURL,
annGlusterUser,
annGlusterUserKey,
} }
params, err := volutil.ParseVolumeAnnotations(pv, annKeys) if len(cfg.endpoint) == 0 {
if err != nil { return nil, fmt.Errorf("StorageClass for provisioner %s must contain 'endpoint' parameter", glusterfsPluginName)
return err
} }
d.url = params[annGlusterURL] if !authEnabled {
d.user = params[annGlusterUser] cfg.user = ""
d.userKey = params[annGlusterUserKey] cfg.secretName = ""
d.secretName = params[annGlusterSecretName] cfg.secretNamespace = ""
d.secretNamespace = params[annGlusterSecretNamespace] cfg.userKey = ""
return nil cfg.secretValue = ""
}
if len(cfg.secretName) != 0 || len(cfg.secretNamespace) != 0 {
// secretName + Namespace has precedence over userKey
if len(cfg.secretName) != 0 && len(cfg.secretNamespace) != 0 {
cfg.secretValue, err = parseSecret(cfg.secretNamespace, cfg.secretName, kubeClient)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("StorageClass for provisioner %q must have secretNamespace and secretName either both set or both empty", glusterfsPluginName)
}
} else {
cfg.secretValue = cfg.userKey
}
return &cfg, nil
} }
...@@ -19,10 +19,13 @@ package glusterfs ...@@ -19,10 +19,13 @@ package glusterfs
import ( import (
"fmt" "fmt"
"os" "os"
"reflect"
"testing" "testing"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/kubernetes/pkg/client/testing/core"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/exec" "k8s.io/kubernetes/pkg/util/exec"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
...@@ -237,62 +240,153 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { ...@@ -237,62 +240,153 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) {
} }
} }
func TestAnnotations(t *testing.T) { func TestParseClassParameters(t *testing.T) {
// Pass a provisioningConfigs through paramToAnnotations and back through secret := api.Secret{
// annotationsToParam and check it did not change in the process. Data: map[string][]byte{
tests := []provisioningConfig{ "data": []byte("mypassword"),
},
}
tests := []struct {
name string
parameters map[string]string
secret *api.Secret
expectError bool
expectConfig *provisioningConfig
}{
{
"password",
map[string]string{
"endpoint": "endpointname",
"resturl": "https://localhost:8080",
"restuser": "admin",
"restuserkey": "password",
},
nil, // secret
false, // expect error
&provisioningConfig{
endpoint: "endpointname",
url: "https://localhost:8080",
user: "admin",
userKey: "password",
secretValue: "password",
},
},
{
"secret",
map[string]string{
"endpoint": "endpointname",
"resturl": "https://localhost:8080",
"restuser": "admin",
"secretname": "mysecret",
"secretnamespace": "default",
},
&secret,
false, // expect error
&provisioningConfig{
endpoint: "endpointname",
url: "https://localhost:8080",
user: "admin",
secretName: "mysecret",
secretNamespace: "default",
secretValue: "mypassword",
},
},
{ {
// Everything empty "no authentication",
map[string]string{
"endpoint": "endpointname",
"resturl": "https://localhost:8080",
"restauthenabled": "false",
},
&secret,
false, // expect error
&provisioningConfig{
endpoint: "endpointname",
url: "https://localhost:8080",
},
}, },
{ {
// Everything with a value "missing secret",
url: "http://localhost", map[string]string{
user: "admin", "endpoint": "endpointname",
secretNamespace: "default", "resturl": "https://localhost:8080",
secretName: "gluster-secret", "secretname": "mysecret",
userKey: "mykey", "secretnamespace": "default",
},
nil, // secret
true, // expect error
nil,
}, },
{ {
// No secret "secret with no namespace",
url: "http://localhost", map[string]string{
user: "admin", "endpoint": "endpointname",
secretNamespace: "", "resturl": "https://localhost:8080",
secretName: "", "secretname": "mysecret",
userKey: "", },
&secret,
true, // expect error
nil,
},
{
"missing url",
map[string]string{
"endpoint": "endpointname",
"restuser": "admin",
"restuserkey": "password",
},
nil, // secret
true, // expect error
nil,
},
{
"missing endpoint",
map[string]string{
"resturl": "https://localhost:8080",
"restuser": "admin",
"restuserkey": "password",
},
nil, // secret
true, // expect error
nil,
},
{
"unknown parameter",
map[string]string{
"unknown": "yes",
"resturl": "https://localhost:8080",
"restuser": "admin",
"restuserkey": "password",
},
nil, // secret
true, // expect error
nil,
}, },
} }
for i, test := range tests { for _, test := range tests {
provisioner := &glusterfsVolumeProvisioner{
provisioningConfig: test,
}
deleter := &glusterfsVolumeDeleter{}
pv := &api.PersistentVolume{ client := &fake.Clientset{}
ObjectMeta: api.ObjectMeta{ client.AddReactor("get", "secrets", func(action core.Action) (handled bool, ret runtime.Object, err error) {
Name: "pv", if test.secret != nil {
}, return true, test.secret, nil
} }
return true, nil, fmt.Errorf("Test %s did not set a secret", test.name)
})
provisioner.paramToAnnotations(pv) cfg, err := parseClassParameters(test.parameters, client)
err := deleter.annotationsToParam(pv)
if err != nil { if err != nil && !test.expectError {
t.Errorf("test %d failed: %v", i, err) t.Errorf("Test %s got unexpected error %v", test.name, err)
}
if test.url != deleter.url {
t.Errorf("test %d failed: expected url %q, got %q", i, test.url, deleter.url)
}
if test.user != deleter.user {
t.Errorf("test %d failed: expected user %q, got %q", i, test.user, deleter.user)
}
if test.userKey != deleter.userKey {
t.Errorf("test %d failed: expected userKey %q, got %q", i, test.userKey, deleter.userKey)
} }
if test.secretNamespace != deleter.secretNamespace { if err == nil && test.expectError {
t.Errorf("test %d failed: expected secretNamespace %q, got %q", i, test.secretNamespace, deleter.secretNamespace) t.Errorf("test %s expected error and got none", test.name)
} }
if test.secretName != deleter.secretName { if test.expectConfig != nil {
t.Errorf("test %d failed: expected secretName %q, got %q", i, test.secretName, deleter.secretName) if !reflect.DeepEqual(cfg, test.expectConfig) {
t.Errorf("Test %s returned unexpected data, expected: %+v, got: %+v", test.name, test.expectConfig, cfg)
}
} }
} }
} }
...@@ -23,6 +23,7 @@ import ( ...@@ -23,6 +23,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/storage"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
) )
...@@ -157,3 +158,17 @@ func ParseVolumeAnnotations(pv *api.PersistentVolume, parseAnnotations []string) ...@@ -157,3 +158,17 @@ func ParseVolumeAnnotations(pv *api.PersistentVolume, parseAnnotations []string)
return result, nil return result, nil
} }
func GetClassForVolume(kubeClient clientset.Interface, pv *api.PersistentVolume) (*storage.StorageClass, error) {
// TODO: replace with a real attribute after beta
className, found := pv.Annotations["volume.beta.kubernetes.io/storage-class"]
if !found {
return nil, fmt.Errorf("Volume has no class annotation")
}
class, err := kubeClient.Storage().StorageClasses().Get(className)
if err != nil {
return nil, err
}
return class, nil
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment