Addressed using k8s client APIs to fetch secrets

Based on the review comments addressed the following,
- Moved away from having to update the pod with volumes
when a new Ceph cluster is added for provisioning via the
CSI driver

- The above now used k8s APIs to fetch secrets
  - TBD: Need to add a watch mechanisim such that these
secrets can be cached and updated when changed

- Folded the Cephc configuration and ID/key config map
and secrets into a single secret

- Provided the ability to read the same config via mapped
or created files within the pod

Tests:
- Ran PV creation/deletion/attach/use using new scheme
StorageClass
- Ran PV creation/deletion/attach/use using older scheme
to ensure nothing is broken
- Did not execute snapshot related tests

Signed-off-by: ShyamsundarR <srangana@redhat.com>
This commit is contained in:
ShyamsundarR 2019-03-07 16:03:33 -05:00 committed by mergify[bot]
parent 97f8c4b677
commit 2064e674a4
20 changed files with 506 additions and 709 deletions

View File

@ -31,7 +31,7 @@ var (
nodeID = flag.String("nodeid", "", "node id")
containerized = flag.Bool("containerized", true, "whether run as containerized")
metadataStorage = flag.String("metadatastorage", "", "metadata persistence method [node|k8s_configmap]")
configRoot = flag.String("configroot", "/etc", "Directory under which Ceph CSI configuration files will be present")
configRoot = flag.String("configroot", "/etc/csi-config", "directory in which CSI specific Ceph cluster configurations are present, OR the value \"k8s_objects\" if present as kubernetes secrets")
)
func init() {

View File

@ -10,6 +10,9 @@ apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: rbd-csi-nodeplugin
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "update"]

View File

@ -67,6 +67,7 @@ spec:
- "--drivername=rbd.csi.ceph.com"
- "--containerized=true"
- "--metadatastorage=k8s_configmap"
- "--configroot=k8s_objects"
env:
- name: HOST_ROOTFS
value: "/rootfs"

View File

@ -57,6 +57,7 @@ spec:
- "--drivername=rbd.csi.ceph.com"
- "--containerized=true"
- "--metadatastorage=k8s_configmap"
- "--configroot=k8s_objects"
env:
- name: HOST_ROOTFS
value: "/rootfs"

View File

@ -33,6 +33,7 @@ Option | Default value | Description
`--nodeid` | _empty_ | This node's ID
`--containerized` | true | Whether running in containerized mode
`--metadatastorage` | _empty_ | Whether should metadata be kept on node as file or in a k8s configmap (`node` or `k8s_configmap`)
`--configroot` | `/etc/csi-config` | Directory in which CSI specific Ceph cluster configurations are present, OR the value `k8s_objects` if present as kubernetes secrets"
**Available environmental variables:**
@ -52,7 +53,7 @@ Parameter | Required | Description
--------- | -------- | -----------
`monitors` | one of `monitors`, `clusterID` or `monValueFromSecret` must be set | Comma separated list of Ceph monitors (e.g. `192.168.100.1:6789,192.168.100.2:6789,192.168.100.3:6789`)
`monValueFromSecret` | one of `monitors`, `clusterID` or and `monValueFromSecret` must be set | a string pointing the key in the credential secret, whose value is the mon. This is used for the case when the monitors' IP or hostnames are changed, the secret can be updated to pick up the new monitors.
`clusterID` | one of `monitors`, `clusterID` or `monValueFromSecret` must be set | Value of Ceph cluster fsid, into which RBD images shall be created (e.g. `4ae5ae3d-ebfb-4150-bfc8-798970f4e3d9`)
`clusterID` | one of `monitors`, `clusterID` or `monValueFromSecret` must be set | Value of `ceph fsid`, into which RBD images shall be created (e.g. `4ae5ae3d-ebfb-4150-bfc8-798970f4e3d9`)
`pool` | yes | Ceph pool into which the RBD image shall be created
`imageFormat` | no | RBD image format. Defaults to `2`. See [man pages](http://docs.ceph.com/docs/mimic/man/8/rbd/#cmdoption-rbd-image-format)
`imageFeatures` | no | RBD image features. Available for `imageFormat=2`. CSI RBD currently supports only `layering` feature. See [man pages](http://docs.ceph.com/docs/mimic/man/8/rbd/#cmdoption-rbd-image-feature)
@ -60,6 +61,11 @@ Parameter | Required | Description
`csi.storage.k8s.io/provisioner-secret-namespace`, `csi.storage.k8s.io/node-publish-secret-namespace` | for Kubernetes | namespaces of the above Secret objects
`mounter`| no | if set to `rbd-nbd`, use `rbd-nbd` on nodes that have `rbd-nbd` and `nbd` kernel modules to map rbd images
NOTE: If `clusterID` parameter is used, then an accompanying Ceph cluster
configuration secret or config files needs to be provided to the running pods.
Refer to `examples/README.md` section titled "Cluster ID based configuration"
for more information.
**Required secrets:**
Admin credentials are required for provisioning new RBD images `ADMIN_NAME`:

View File

@ -14,7 +14,7 @@ Please consult the documentation for info about available parameters.
**NOTE:** See section
[Cluster ID based configuration](#cluster-id-based-configuration) if using
the `clusterID` instead of `monitors` or `monValueFromSecret` options in the
the `clusterID` instead of `monitors` or `monValueFromSecret` option in the
storage class for RBD based provisioning before proceeding.
After configuring the secrets, monitors, etc. you can deploy a
@ -222,30 +222,29 @@ I/O size (minimum/optimal): 4194304 bytes / 4194304 bytes
## Cluster ID based configuration
Before creating a storage class that uses the option `clusterID` to refer to a
Ceph cluster,
Ceph cluster, the following actions need to be completed.
**NOTE**: Substitute the output of `ceph fsid` instead of `<cluster-fsid>` in
the mentioned template YAML files, and also the Ceph admin ID and
credentials in their respective options. Further, update options like
`monitors` and `pools` in the respective YAML files to contain the
appropriate information.
Get the following information from the Ceph cluster,
Create the following config maps and secrets
* Ceph Cluster fsid
* Output of `ceph fsid`
* Used to substitute `<cluster-fsid>` references in the files below
* Admin ID and key, that has privileges to perform CRUD operations on the Ceph
cluster and pools of choice
* Key is typically the output of, `ceph auth get-key client.admin` where
`admin` is the Admin ID
* Used to substitute admin/user id and key values in the files below
* Ceph monitor list
* Typically in the output of `ceph mon dump`
* Used to prepare comma separated MON list where required in the files below
* `kubectl create -f ./rbd/template-ceph-cluster-ID-provisioner-secret.yaml`
* `kubectl create -f ./rbd/template-ceph-cluster-ID-publish-secret.yaml`
* `kubectl create -f ./rbd/template-ceph-cluster-ID-config.yaml`
Update the template `rbd/template-ceph-cluster-ID-secret.yaml` with values from
a Ceph cluster and create the following secret,
Modify the deployed CSI pods to additionally pass in the config maps and
secrets as volumes,
* `kubectl create -f rbd/template-ceph-cluster-ID-secret.yaml`
* `kubectl patch daemonset csi-rbdplugin --patch "$(cat ./rbd/template-csi-rbdplugin-patch.yaml)"`
* `kubectl patch statefulset csi-rbdplugin-provisioner --patch "$(cat ./rbd/template-csi-rbdplugin-provisioner-patch.yaml)"`
Restart the provisioner and node plugin daemonset.
Storage class and snapshot class, using the `<cluster-fsid>` as the value for
the option `clusterID`, can now be created on the cluster.
Storage class and snapshot class, using `<cluster-fsid>` as the value for the
option `clusterID`, can now be created on the cluster.
Remaining steps to test functionality remains the same as mentioned in the
sections above.

View File

@ -11,6 +11,9 @@ parameters:
# OR,
# Ceph cluster fsid, of the cluster to provision storage from
# clusterID: <ceph-fsid>
# If using clusterID based configuration, CSI pods need to be passed in a
# secret named ceph-cluster-<cluster-fsid> that contains the cluster
# information. (as in the provided template-ceph-cluster-ID-secret.yaml)
# OR,
# if "monitors" parameter is not set, driver to get monitors from same
# secret as admin/user credentials. "monValueFromSecret" provides the
@ -28,12 +31,18 @@ parameters:
imageFeatures: layering
# The secrets have to contain Ceph admin credentials.
# NOTE: If using "clusterID" instead of "monitors" above, the following
# secrets MAY be added to the ceph-cluster-<cluster-fsid> secret and skipped
# here
csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret
csi.storage.k8s.io/provisioner-secret-namespace: default
csi.storage.k8s.io/node-publish-secret-name: csi-rbd-secret
csi.storage.k8s.io/node-publish-secret-namespace: default
# Ceph users for operating RBD
# NOTE: If using "clusterID" instead of "monitors" above, the following
# IDs MAY be added to the ceph-cluster-<cluster-fsid> secret and skipped
# here
adminid: admin
userid: kubernetes
# uncomment the following to use rbd-nbd as mounter on supported nodes

View File

@ -1,22 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: ceph-cluster-<cluster-fsid>
namespace: default
data:
cluster-config: |
{
"version": 1,
"cluster-config": {
"cluster-fsid": "<ceph-fsid>",
"monitors": [
"<IP/DNS:port>",
"<IP/DNS:port>"
],
"pools": [
"<pool-name>",
"<pool-name>"
]
}
}

View File

@ -1,19 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
# The <cluster-fsid> is used by the CSI plugin to uniquely identify and use a
# Ceph cluster, hence the value MUST match the output of the following
# command.
# - Output of: `ceph fsid`
name: ceph-cluster-<cluster-fsid>-provisioner-secret
namespace: default
data:
# Base64 encoded ID of the admin name
# - Typically output of: `echo -n "<admin-id>" | base64`
# Substitute the entire string including angle braces, with the base64 value
subjectid: <BASE64-ENCODED-ID>
# Credentials of the above admin/user
# - Output of: `ceph auth get-key client.admin | base64`
# Substitute the entire string including angle braces, with the base64 value
credentials: <BASE64-ENCODED-PASSWORD>

View File

@ -1,19 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
# The <cluster-fsid> is used by the CSI plugin to uniquely identify and use a
# Ceph cluster, hence the value MUST match the output of the following
# command.
# - Output of: `ceph fsid`
name: ceph-cluster-<cluster-fsid>-publish-secret
namespace: default
data:
# Base64 encoded ID of the admin name
# - Typically output of: `echo -n "<admin-id>" | base64`
# Substitute the entire string including angle braces, with the base64 value
subjectid: <BASE64-ENCODED-ID>
# Credentials of the above admin/user
# - Output of: `ceph auth get-key client.admin | base64`
# Substitute the entire string including angle braces, with the base64 value
credentials: <BASE64-ENCODED-PASSWORD>

View File

@ -0,0 +1,37 @@
---
# This is a template secret that helps define a Ceph cluster configuration
# as required by the CSI driver. This is used when a StorageClass has the
# "clusterID" defined as one of the parameters, to provide the CSI instance
# Ceph cluster configuration information.
apiVersion: v1
kind: Secret
metadata:
# The <cluster-fsid> is used by the CSI plugin to uniquely identify and use a
# Ceph cluster, hence the value MUST match the output of the following
# command.
# - Output of: `ceph fsid`
name: ceph-cluster-<cluster-fsid>
namespace: default
data:
# Base64 encoded and comma separated Ceph cluster monitor list
# - Typically output of: `echo -n "mon1:port,mon2:port,..." | base64`
monitors: <BASE64-ENCODED-MONLIST>
# Base64 encoded and comma separated list of pool names from which volumes
# can be provisioned
pools: <BASE64-ENCODED-POOLIST>
# Base64 encoded admin ID to use for provisioning
# - Typically output of: `echo -n "<admin-id>" | base64`
# Substitute the entire string including angle braces, with the base64 value
adminid: <BASE64-ENCODED-ID>
# Base64 encoded key of the provisioner admin ID
# - Output of: `ceph auth get-key client.admin | base64`
# Substitute the entire string including angle braces, with the base64 value
adminkey: <BASE64-ENCODED-PASSWORD>
# Base64 encoded user ID to use for publishing
# - Typically output of: `echo -n "<admin-id>" | base64`
# Substitute the entire string including angle braces, with the base64 value
userid: <BASE64-ENCODED-ID>
# Base64 encoded key of the publisher user ID
# - Output of: `ceph auth get-key client.admin | base64`
# Substitute the entire string including angle braces, with the base64 value
userkey: <BASE64-ENCODED-PASSWORD>

View File

@ -12,22 +12,10 @@ spec:
containers:
- name: csi-rbdplugin
volumeMounts:
- name: provisioner-secret-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>-provisioner-secret"
readOnly: true
- name: publish-secret-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>-publish-secret"
readOnly: true
- name: ceph-cluster-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>/"
mountPath: "/etc/csi-config/ceph-cluster-<cluster-fsid>"
readOnly: true
volumes:
- name: provisioner-secret-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>-provisioner-secret
- name: publish-secret-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>-publish-secret
- name: ceph-cluster-<cluster-fsid>
configMap:
name: ceph-cluster-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>

View File

@ -12,22 +12,10 @@ spec:
containers:
- name: csi-rbdplugin
volumeMounts:
- name: provisioner-secret-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>-provisioner-secret"
readOnly: true
- name: publish-secret-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>-publish-secret"
readOnly: true
- name: ceph-cluster-<cluster-fsid>
mountPath: "/etc/ceph-cluster-<cluster-fsid>/"
mountPath: "/etc/csi-config/ceph-cluster-<cluster-fsid>"
readOnly: true
volumes:
- name: provisioner-secret-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>-provisioner-secret
- name: publish-secret-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>-publish-secret
- name: ceph-cluster-<cluster-fsid>
configMap:
name: ceph-cluster-<cluster-fsid>
secret:
secretName: ceph-cluster-<cluster-fsid>

View File

@ -47,9 +47,8 @@ type Driver struct {
var (
version = "1.0.0"
// Fc is the global file config type, and stores the top level directory
// under which rest of the Ceph config files can be found
Fc util.FileConfig
// ConfStore is the global config store
ConfStore *util.ConfigStore
)
// NewDriver returns new rbd driver
@ -94,8 +93,11 @@ func (r *Driver) Run(driverName, nodeID, endpoint string, containerized bool, co
var err error
klog.Infof("Driver: %v version: %v", driverName, version)
// Initialize fileconfig base path
Fc.BasePath = configroot
// Initialize config store
ConfStore, err = util.NewConfigStore(configroot)
if err != nil {
klog.Fatalln("Failed to initialize config store.")
}
// Initialize default library driver
r.cd = csicommon.NewCSIDriver(driverName, version, nodeID)

View File

@ -91,9 +91,10 @@ func getRBDKey(fsid string, id string, credentials map[string]string) (string, e
var ok bool
var err error
var key string
if key, ok = credentials[id]; !ok {
if fsid != "" {
key, err = Fc.GetCredentialForSubject(fsid, id)
key, err = ConfStore.CredentialForUser(fsid, id)
if err != nil {
klog.Errorf("failed getting credentials (%s)", err)
return "", fmt.Errorf("RBD key for ID: %s not found in config store", id)
@ -240,8 +241,7 @@ func execCommand(command string, args []string) ([]byte, error) {
return cmd.CombinedOutput()
}
func getMonsAndFsID(options map[string]string) (monitors, fsID, monInSecret string, noerr error) {
var err error
func getMonsAndFsID(options map[string]string) (monitors, fsID, monInSecret string, err error) {
var ok bool
monitors, ok = options["monitors"]
@ -250,11 +250,14 @@ func getMonsAndFsID(options map[string]string) (monitors, fsID, monInSecret stri
if monInSecret, ok = options["monValueFromSecret"]; !ok {
// if mons are not in secret, check if we have a cluster-fsid
if fsID, ok = options["clusterID"]; !ok {
return "", "", "", fmt.Errorf("either monitors or monValueFromSecret or clusterID must be set")
err = errors.New("either monitors or monValueFromSecret or clusterID must be set")
return
}
if monitors, err = Fc.GetMons(fsID); err != nil {
if monitors, err = ConfStore.Mons(fsID); err != nil {
klog.Errorf("failed getting mons (%s)", err)
return "", "", "", fmt.Errorf("failed to fetch monitor list using clusterID (%s)", fsID)
err = fmt.Errorf("failed to fetch monitor list using clusterID (%s)", fsID)
return
}
}
}
@ -262,35 +265,34 @@ func getMonsAndFsID(options map[string]string) (monitors, fsID, monInSecret stri
return
}
func getIDs(options map[string]string, fsID string) (adminID, userID string, noerr error) {
var err error
func getIDs(options map[string]string, fsID string) (adminID, userID string, err error) {
var ok bool
adminID, ok = options["adminid"]
if !ok {
if fsID != "" {
if adminID, err = Fc.GetProvisionerSubjectID(fsID); err != nil {
klog.Errorf("failed getting subject (%s)", err)
return "", "", fmt.Errorf("failed to fetch provisioner ID using clusterID (%s)", fsID)
}
} else {
adminID = rbdDefaultAdminID
switch {
case ok:
case fsID != "":
if adminID, err = ConfStore.AdminID(fsID); err != nil {
klog.Errorf("failed getting subject (%s)", err)
return "", "", fmt.Errorf("failed to fetch provisioner ID using clusterID (%s)", fsID)
}
default:
adminID = rbdDefaultAdminID
}
userID, ok = options["userid"]
if !ok {
if fsID != "" {
if userID, err = Fc.GetPublishSubjectID(fsID); err != nil {
klog.Errorf("failed getting subject (%s)", err)
return "", "", fmt.Errorf("failed to fetch publisher ID using clusterID (%s)", fsID)
}
} else {
userID = rbdDefaultUserID
switch {
case ok:
case fsID != "":
if userID, err = ConfStore.UserID(fsID); err != nil {
klog.Errorf("failed getting subject (%s)", err)
return "", "", fmt.Errorf("failed to fetch publisher ID using clusterID (%s)", fsID)
}
default:
userID = rbdDefaultUserID
}
return
return adminID, userID, err
}
func getRBDVolumeOptions(volOptions map[string]string, disableInUseChecks bool) (*rbdVolume, error) {

138
pkg/util/configstore.go Normal file
View File

@ -0,0 +1,138 @@
/*
Copyright 2018 The Ceph-CSI 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 util
import (
"errors"
"fmt"
"k8s.io/klog"
"path"
"strings"
)
// StoreReader interface enables plugging different stores, that contain the
// keys and data. (e.g k8s secrets or local files)
type StoreReader interface {
DataForKey(fsid string, key string) (string, error)
}
/* ConfigKeys contents and format,
- csMonitors: MON list, comma separated
- csAdminID: adminID, used for provisioning
- csUserID: userID, used for publishing
- csAdminKey: key, for userID in csProvisionerUser
- csUserKey: key, for userID in csPublisherUser
- csPools: Pool list, comma separated
*/
// Constants for various ConfigKeys
const (
csMonitors = "monitors"
csAdminID = "adminid"
csUserID = "userid"
csAdminKey = "adminkey"
csUserKey = "userkey"
csPools = "pools"
)
// ConfigStore provides various gettors for ConfigKeys
type ConfigStore struct {
StoreReader
}
// dataForKey returns data from the config store for the provided key
func (dc *ConfigStore) dataForKey(fsid string, key string) (string, error) {
if dc.StoreReader != nil {
return dc.StoreReader.DataForKey(fsid, key)
}
err := errors.New("config store location uninitialized")
return "", err
}
// Mons returns a comma separated MON list from the cluster config represented by fsid
func (dc *ConfigStore) Mons(fsid string) (string, error) {
return dc.dataForKey(fsid, csMonitors)
}
// Pools returns a list of pool names from the cluster config represented by fsid
func (dc *ConfigStore) Pools(fsid string) ([]string, error) {
content, err := dc.dataForKey(fsid, csPools)
if err != nil {
return nil, err
}
return strings.Split(content, ","), nil
}
// AdminID returns the admin ID from the cluster config represented by fsid
func (dc *ConfigStore) AdminID(fsid string) (string, error) {
return dc.dataForKey(fsid, csAdminID)
}
// UserID returns the user ID from the cluster config represented by fsid
func (dc *ConfigStore) UserID(fsid string) (string, error) {
return dc.dataForKey(fsid, csUserID)
}
// CredentialForUser returns the credentials for the requested user ID
// from the cluster config represented by fsid
func (dc *ConfigStore) CredentialForUser(fsid, userID string) (data string, err error) {
var credkey string
user, err := dc.AdminID(fsid)
if err != nil {
return
}
if user == userID {
credkey = csAdminKey
} else {
user, err = dc.UserID(fsid)
if err != nil {
return
}
if user != userID {
err = fmt.Errorf("requested user (%s) not found in cluster configuration of (%s)", userID, fsid)
return
}
credkey = csUserKey
}
return dc.dataForKey(fsid, credkey)
}
// NewConfigStore returns a config store based on value of configRoot. If
// configRoot is not "k8s_objects" then it is assumed to be a path to a
// directory, under which the configuration files can be found
func NewConfigStore(configRoot string) (*ConfigStore, error) {
if configRoot != "k8s_objects" {
klog.Infof("cache-store: using files in path (%s) as config store", configRoot)
fc := &FileConfig{}
fc.BasePath = path.Clean(configRoot)
dc := &ConfigStore{fc}
return dc, nil
}
klog.Infof("cache-store: using k8s objects as config store")
kc := &K8sConfig{}
kc.Client = NewK8sClient()
kc.Namespace = GetK8sNamespace()
dc := &ConfigStore{kc}
return dc, nil
}

View File

@ -0,0 +1,161 @@
/*
Copyright 2019 ceph-csi 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.
*/
// nolint: gocyclo
package util
import (
"io/ioutil"
"os"
"strings"
"testing"
)
var basePath = "./test_artifacts"
var cs *ConfigStore
func cleanupTestData() {
os.RemoveAll(basePath)
}
// nolint: gocyclo
func TestConfigStore(t *testing.T) {
var err error
var data string
var content string
var testDir string
defer cleanupTestData()
cs, err = NewConfigStore(basePath)
if err != nil {
t.Errorf("Fatal, failed to get a new config store")
}
err = os.MkdirAll(basePath, 0700)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Should fail as fsid directory is missing
_, err = cs.Mons("testfsid")
if err == nil {
t.Errorf("Failed: expected error due to missing parent directory")
}
testDir = basePath + "/" + "ceph-cluster-testfsid"
err = os.MkdirAll(testDir, 0700)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Should fail as mons file is missing
_, err = cs.Mons("testfsid")
if err == nil {
t.Errorf("Failed: expected error due to missing mons file")
}
data = ""
err = ioutil.WriteFile(testDir+"/"+csMonitors, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Should fail as MONs is an empty string
content, err = cs.Mons("testfsid")
if err == nil {
t.Errorf("Failed: want (%s), got (%s)", data, content)
}
data = "mon1,mon2,mon3"
err = ioutil.WriteFile(testDir+"/"+csMonitors, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching MONs should succeed
content, err = cs.Mons("testfsid")
if err != nil || content != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
data = "pool1,pool2"
err = ioutil.WriteFile(testDir+"/"+csPools, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching MONs should succeed
listContent, err := cs.Pools("testfsid")
if err != nil || strings.Join(listContent, ",") != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
data = "provuser"
err = ioutil.WriteFile(testDir+"/"+csAdminID, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching provuser should succeed
content, err = cs.AdminID("testfsid")
if err != nil || content != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
data = "pubuser"
err = ioutil.WriteFile(testDir+"/"+csUserID, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching pubuser should succeed
content, err = cs.UserID("testfsid")
if err != nil || content != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
data = "provkey"
err = ioutil.WriteFile(testDir+"/"+csAdminKey, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching provkey should succeed
content, err = cs.CredentialForUser("testfsid", "provuser")
if err != nil || content != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
data = "pubkey"
err = ioutil.WriteFile(testDir+"/"+csUserKey, []byte(data), 0644)
if err != nil {
t.Errorf("Test setup error %s", err)
}
// TEST: Fetching pubkey should succeed
content, err = cs.CredentialForUser("testfsid", "pubuser")
if err != nil || content != data {
t.Errorf("Failed: want (%s), got (%s), err (%s)", data, content, err)
}
// TEST: Fetching random user key should fail
_, err = cs.CredentialForUser("testfsid", "random")
if err == nil {
t.Errorf("Failed: Expected to fail fetching random user key")
}
}

View File

@ -17,241 +17,42 @@ limitations under the License.
package util
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"fmt"
"io/ioutil"
"k8s.io/klog"
"path"
)
/* FileConfig processes config information stored in files, mostly mapped into
the runtime container.
/*
FileConfig is a ConfigStore interface implementation that reads configuration
information from files.
The calls explicitly do not cache any information, to ensure that updated
configuration is always read from the files (for example when these are
mapped in as k8s config maps or secrets).
BasePath defines the directory under which FileConfig will attempt to open and
read contents of various Ceph cluster configurations.
The BasePath is the path where config files are found, and config files are
expected to be named in the following manner,
- BasePath/ceph-cluster-<cluster-fsid>/cluster-config
- BasePath/ceph-cluster-<cluster-fsid>-provisioner-secret/credentials
- BasePath/ceph-cluster-<cluster-fsid>-provisioner-secret/subjectid
- BasePath/ceph-cluster-<cluster-fsid>-publish-secret/credentials
- BasePath/ceph-cluster-<cluster-fsid>-publish-secret/subjectid
Where,
- cluster-fsid is the Ceph cluster fsid in UUID ascii notation
- The cluster-fsid corresponds to the cluster for which the
configuration information is present in the mentioned files
- cluster-config is expected to be a JSON blob with the following
structure,
{
"version": 1,
"cluster-config": {
"cluster-fsid": "<ceph-fsid>",
"monitors": [
"IP/DNS:port",
"IP/DNS:port"
],
"pools": [
"<pool-name>",
"<pool-name>"
]
}
}
- credentials is expected to contain Base64 encoded credentials for the
user encoded in subjectid
- subjectid is the username/subject to use with calls to Ceph, and is
also Base64 encoded
- Provisioner secret contains secrets to use by the provisioning system
- Publish secret contains secrets to use by the publishing/staging
system
Each Ceph cluster configuration is stored under a directory named,
BasePath/ceph-cluster-<fsid>, where <fsid> is the Ceph cluster fsid.
Under each Ceph cluster configuration directory, individual files named as per
the ConfigKeys constants in the ConfigStore interface, store the required
configuration information.
*/
// FileConfig type with basepath that points to source of all config files
type FileConfig struct {
BasePath string
BasePath string
}
// ClusterConfigv1 strongly typed JSON spec for cluster-config above
type ClusterConfigv1 struct {
ClusterFsID string `json:"cluster-fsid"`
Monitors []string `json:"monitors"`
Pools []string `json:"pools"`
}
// ClusterConfigJSONv1 strongly typed JSON spec for cluster-config above
type ClusterConfigJSONv1 struct {
Version int `json:"version"`
ClusterConf *ClusterConfigv1 `json:"cluster-config"`
}
// Constants and enum for constructPath operation
type pathType int
const (
clusterConfig pathType = 0
pubSubject pathType = 1
pubCreds pathType = 2
provSubject pathType = 3
provCreds pathType = 4
)
const (
fNamePrefix = "ceph-cluster"
fNameSep = "-"
fNamePubPrefix = "publish-secret"
fNameProvPrefix = "provisioner-secret"
fNameCephConfig = "cluster-config"
fNamePubSubject = "subjectid"
fNameProvSubject = "subjectid"
fNamePubCred = "credentials"
fNameProvCred = "credentials"
)
// constructPath constructs well defined paths based on the type of config
// file that needs to be accessed.
func (pType pathType) constructPath(basepath string, fsid string) (filePath string, noerr error) {
if fsid == "" || basepath == "" {
return "", fmt.Errorf("missing/empty fsid (%s) or basepath (%s) for config files", fsid, basepath)
}
switch pType {
case clusterConfig:
filePath = basepath + "/" + fNamePrefix + fNameSep + fsid +
"/" + fNameCephConfig
case pubSubject:
filePath = basepath + "/" + fNamePrefix + fNameSep + fsid +
fNameSep + fNamePubPrefix + "/" + fNamePubSubject
case pubCreds:
filePath = basepath + "/" + fNamePrefix + fNameSep + fsid +
fNameSep + fNamePubPrefix + "/" + fNamePubCred
case provSubject:
filePath = basepath + "/" + fNamePrefix + fNameSep + fsid +
fNameSep + fNameProvPrefix + "/" + fNameProvSubject
case provCreds:
filePath = basepath + "/" + fNamePrefix + fNameSep + fsid +
fNameSep + fNameProvPrefix + "/" + fNameProvCred
default:
return "", fmt.Errorf("invalid path type (%d) specified", pType)
}
return
}
// GetMons returns a comma separated MON list, that is read in from the config
// files, based on the passed in fsid
func (fc *FileConfig) GetMons(fsid string) (string, error) {
fPath, err := clusterConfig.constructPath(fc.BasePath, fsid)
if err != nil {
return "", err
}
// #nosec
contentRaw, err := ioutil.ReadFile(fPath)
if err != nil {
return "", err
}
var cephConfig ClusterConfigJSONv1
err = json.Unmarshal(contentRaw, &cephConfig)
if err != nil {
return "", err
}
if cephConfig.ClusterConf.ClusterFsID != fsid {
return "", fmt.Errorf("mismatching Ceph cluster fsid (%s) in file, passed in (%s)", cephConfig.ClusterConf.ClusterFsID, fsid)
}
if len(cephConfig.ClusterConf.Monitors) == 0 {
return "", fmt.Errorf("monitor list empty in configuration file")
}
return strings.Join(cephConfig.ClusterConf.Monitors, ","), nil
}
// GetProvisionerSubjectID returns the provisioner subject ID from the on-disk
// configuration file, based on the passed in fsid
func (fc *FileConfig) GetProvisionerSubjectID(fsid string) (string, error) {
fPath, err := provSubject.constructPath(fc.BasePath, fsid)
if err != nil {
return "", err
}
// #nosec
contentRaw, err := ioutil.ReadFile(fPath)
if err != nil {
return "", err
}
if string(contentRaw) == "" {
return "", fmt.Errorf("missing/empty provisioner subject ID from file (%s)", fPath)
}
return string(contentRaw), nil
}
// GetPublishSubjectID returns the publish subject ID from the on-disk
// configuration file, based on the passed in fsid
func (fc *FileConfig) GetPublishSubjectID(fsid string) (string, error) {
fPath, err := pubSubject.constructPath(fc.BasePath, fsid)
if err != nil {
return "", err
}
// #nosec
contentRaw, err := ioutil.ReadFile(fPath)
if err != nil {
return "", err
}
if string(contentRaw) == "" {
return "", fmt.Errorf("missing/empty publish subject ID from file (%s)", fPath)
}
return string(contentRaw), nil
}
// GetCredentialForSubject returns the credentials for the requested subject
// from the cluster config for the passed in fsid
func (fc *FileConfig) GetCredentialForSubject(fsid, subject string) (string, error) {
var fPath string
var err error
tmpSubject, err := fc.GetPublishSubjectID(fsid)
if err != nil {
return "", err
}
if tmpSubject != subject {
tmpSubject, err = fc.GetProvisionerSubjectID(fsid)
if err != nil {
return "", err
}
if tmpSubject != subject {
return "", fmt.Errorf("requested subject did not match stored publish/provisioner subjectID")
}
fPath, err = provCreds.constructPath(fc.BasePath, fsid)
if err != nil {
return "", err
}
} else {
fPath, err = pubCreds.constructPath(fc.BasePath, fsid)
if err != nil {
return "", err
}
}
// #nosec
contentRaw, err := ioutil.ReadFile(fPath)
if err != nil {
return "", err
}
if string(contentRaw) == "" {
return "", fmt.Errorf("missing/empty credentials in file (%s)", fPath)
}
return string(contentRaw), nil
// DataForKey reads the appropriate config file, named using key, and returns
// the contents of the file to the caller
func (fc *FileConfig) DataForKey(fsid string, key string) (data string, err error) {
pathToKey := path.Join(fc.BasePath, "ceph-cluster-"+fsid, key)
// #nosec
content, err := ioutil.ReadFile(pathToKey)
if err != nil || string(content) == "" {
err = fmt.Errorf("error fetching configuration for cluster ID (%s). (%s)", fsid, err)
return
}
data = string(content)
klog.V(3).Infof("returning data (%s) for key (%s) against cluster (%s)", data, key, fsid)
return
}

View File

@ -1,338 +0,0 @@
/*
Copyright 2019 ceph-csi 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.
*/
// nolint: gocyclo
package util
import (
"fmt"
"io/ioutil"
"os"
"testing"
)
var testFsid = "dummy-fs-id"
var basePath = "./test_artifacts"
// nolint: gocyclo
func TestGetMons(t *testing.T) {
var fc FileConfig
var err error
configFileDir := basePath + "/" + fNamePrefix + fNameSep + testFsid
defer os.RemoveAll(basePath)
fc.BasePath = basePath
// TEST: Empty fsid should error out
_, err = fc.GetMons("")
if err == nil {
t.Errorf("Call passed, expected to fail due to fsid missing!")
}
// TEST: Missing file should error out
_, err = fc.GetMons(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Empty file should error out
err = os.MkdirAll(configFileDir, 0700)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data := []byte{}
err = ioutil.WriteFile(configFileDir+"/"+fNameCephConfig, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetMons(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
/* Tests with bad JSON content should get caught due to strongly typed JSON
struct in implementation and are not tested here */
// TEST: Send JSON with incorrect fsid
data = []byte(`
{
"version": 1,
"cluster-config": {
"cluster-fsid": "bad_fsid",
"monitors": ["IP1:port1","IP2:port2"],
"pools": ["pool1","pool2"]
}
}`)
err = ioutil.WriteFile(configFileDir+"/"+fNameCephConfig, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetMons(testFsid)
if err == nil {
t.Errorf("Expected to fail on bad fsid in JSON")
}
// TEST: Send JSON with empty mon list
data = []byte(`
{
"version": 1,
"cluster-config": {
"cluster-fsid": "` + testFsid + `",
"monitors": [],
"pools": ["pool1","pool2"]
}
}`)
err = ioutil.WriteFile(configFileDir+"/"+fNameCephConfig, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetMons(testFsid)
if err == nil {
t.Errorf("Expected to fail in empty MON list in JSON")
}
// TEST: Check valid return from successful call
data = []byte(`
{
"version": 1,
"cluster-config": {
"cluster-fsid": "` + testFsid + `",
"monitors": ["IP1:port1","IP2:port2"],
"pools": ["pool1","pool2"]
}
}`)
err = ioutil.WriteFile(configFileDir+"/"+fNameCephConfig, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
output, err := fc.GetMons(testFsid)
if err != nil {
t.Errorf("Call failed %s", err)
}
if output != "IP1:port1,IP2:port2" {
t.Errorf("Failed to generate correct output: expected %s, got %s",
"IP1:port1,IP2:port2", output)
}
}
func TestGetProvisionerSubjectID(t *testing.T) {
var fc FileConfig
var err error
configFileDir := basePath + "/" + fNamePrefix + fNameSep + testFsid + fNameSep + fNameProvPrefix
defer os.RemoveAll(basePath)
fc.BasePath = basePath
// TEST: Empty fsid should error out
_, err = fc.GetProvisionerSubjectID("")
if err == nil {
t.Errorf("Call passed, expected to fail due to fsid missing!")
}
// TEST: Missing file should error out
_, err = fc.GetProvisionerSubjectID(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Empty file should error out
err = os.MkdirAll(configFileDir, 0700)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data := []byte{}
err = ioutil.WriteFile(configFileDir+"/"+fNameProvSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetProvisionerSubjectID(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Check valid return from successful call
data = []byte("admin")
err = ioutil.WriteFile(configFileDir+"/"+fNameProvSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
output, err := fc.GetProvisionerSubjectID(testFsid)
if err != nil || output != "admin" {
t.Errorf("Failed to get valid subject ID: expected %s, got %s, err %s", "admin", output, err)
}
}
func TestGetPublishSubjectID(t *testing.T) {
var fc FileConfig
var err error
configFileDir := basePath + "/" + fNamePrefix + fNameSep + testFsid + fNameSep + fNamePubPrefix
defer os.RemoveAll(basePath)
fc.BasePath = basePath
// TEST: Empty fsid should error out
_, err = fc.GetPublishSubjectID("")
if err == nil {
t.Errorf("Call passed, expected to fail due to fsid missing!")
}
// TEST: Missing file should error out
_, err = fc.GetPublishSubjectID(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Empty file should error out
err = os.MkdirAll(configFileDir, 0700)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data := []byte{}
err = ioutil.WriteFile(configFileDir+"/"+fNamePubSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetPublishSubjectID(testFsid)
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Check valid return from successful call
data = []byte("admin")
err = ioutil.WriteFile(configFileDir+"/"+fNamePubSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
output, err := fc.GetPublishSubjectID(testFsid)
if err != nil || output != "admin" {
t.Errorf("Failed to get valid subject ID: expected %s, got %s, err %s", "admin", output, err)
}
}
// nolint: gocyclo
func TestGetCredentialForSubject(t *testing.T) {
var fc FileConfig
var err error
configFileDir := basePath + "/" + fNamePrefix + fNameSep + testFsid + fNameSep + fNamePubPrefix
defer os.RemoveAll(basePath)
fc.BasePath = basePath
// TEST: Empty fsid should error out
_, err = fc.GetCredentialForSubject("", "subject")
if err == nil {
t.Errorf("Call passed, expected to fail due to fsid missing!")
}
// TEST: Missing file should error out
_, err = fc.GetCredentialForSubject(testFsid, "")
if err == nil {
t.Errorf("Call passed, expected to fail due to missing config file!")
}
// TEST: Empty subject file should error out
err = os.MkdirAll(configFileDir, 0700)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data := []byte{}
err = ioutil.WriteFile(configFileDir+"/"+fNamePubSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetCredentialForSubject(testFsid, "adminpub")
if err == nil {
t.Errorf("Call passed, expected to fail due to empty subject file!")
}
// TEST: Empty subject cred file should error out
data = []byte("adminpub")
err = ioutil.WriteFile(configFileDir+"/"+fNamePubSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data = []byte{}
err = ioutil.WriteFile(configFileDir+"/"+fNamePubCred, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
_, err = fc.GetCredentialForSubject(testFsid, "adminpub")
if err == nil {
t.Errorf("Call passed, expected to fail due to missing cred content!")
}
// TEST: Success fetching pub creds
data = []byte("testpwd")
err = ioutil.WriteFile(configFileDir+"/"+fNamePubCred, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
output, err := fc.GetCredentialForSubject(testFsid, "adminpub")
if err != nil || output != "testpwd" {
t.Errorf("Failed to get valid Publish credentials: expected %s, got %s, err %s", "testpwd", output, err)
}
// TEST: Fetch missing prov creds
configFileDir = basePath + "/" + fNamePrefix + fNameSep + testFsid + fNameSep + fNameProvPrefix
err = os.MkdirAll(configFileDir, 0700)
if err != nil {
t.Errorf("Test utility error %s", err)
}
data = []byte("adminprov")
err = ioutil.WriteFile(configFileDir+"/"+fNameProvSubject, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
fmt.Printf("Starting test")
_, err = fc.GetCredentialForSubject(testFsid, "adminprov")
if err == nil {
t.Errorf("Call passed, expected to fail due to missing cred content!")
}
// TEST: Fetch prov creds successfully
data = []byte("testpwd")
err = ioutil.WriteFile(configFileDir+"/"+fNameProvCred, data, 0644)
if err != nil {
t.Errorf("Test utility error %s", err)
}
output, err = fc.GetCredentialForSubject(testFsid, "adminprov")
if err != nil || output != "testpwd" {
t.Errorf("Call passed, expected to fail due to missing cred content!")
}
}

59
pkg/util/k8sconfig.go Normal file
View File

@ -0,0 +1,59 @@
/*
Copyright 2019 ceph-csi 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 util
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s "k8s.io/client-go/kubernetes"
"k8s.io/klog"
)
/*
K8sConfig is a ConfigStore interface implementation that reads configuration
information from k8s secrets.
Each Ceph cluster configuration secret is expected to be named,
ceph-cluster-<fsid>, where <fsid> is the Ceph cluster fsid.
The secret is expected to contain keys, as defined by the ConfigKeys constants
in the ConfigStore interface.
*/
type K8sConfig struct {
Client *k8s.Clientset
Namespace string
}
// DataForKey reads the appropriate k8s secret, named using fsid, and returns
// the contents of key within the secret
func (kc *K8sConfig) DataForKey(fsid string, key string) (data string, err error) {
secret, err := kc.Client.CoreV1().Secrets(kc.Namespace).Get("ceph-cluster-"+fsid, metav1.GetOptions{})
if err != nil {
err = fmt.Errorf("error fetching configuration for cluster ID (%s). (%s)", fsid, err)
return
}
content, ok := secret.Data[key]
if !ok {
err = fmt.Errorf("missing data for key (%s) in cluster configuration of (%s)", key, fsid)
return
}
data = string(content)
klog.V(3).Infof("returning data (%s) for key (%s) against cluster (%s)", data, key, fsid)
return
}