diff --git a/cmd/rbd/main.go b/cmd/rbd/main.go index cd78b6fc4..f5f727bc9 100644 --- a/cmd/rbd/main.go +++ b/cmd/rbd/main.go @@ -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() { diff --git a/deploy/rbd/kubernetes/csi-nodeplugin-rbac.yaml b/deploy/rbd/kubernetes/csi-nodeplugin-rbac.yaml index c960408e6..9a5ffed1b 100644 --- a/deploy/rbd/kubernetes/csi-nodeplugin-rbac.yaml +++ b/deploy/rbd/kubernetes/csi-nodeplugin-rbac.yaml @@ -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"] diff --git a/deploy/rbd/kubernetes/csi-rbdplugin-provisioner.yaml b/deploy/rbd/kubernetes/csi-rbdplugin-provisioner.yaml index aef25c04c..bd14fa363 100644 --- a/deploy/rbd/kubernetes/csi-rbdplugin-provisioner.yaml +++ b/deploy/rbd/kubernetes/csi-rbdplugin-provisioner.yaml @@ -67,6 +67,7 @@ spec: - "--drivername=rbd.csi.ceph.com" - "--containerized=true" - "--metadatastorage=k8s_configmap" + - "--configroot=k8s_objects" env: - name: HOST_ROOTFS value: "/rootfs" diff --git a/deploy/rbd/kubernetes/csi-rbdplugin.yaml b/deploy/rbd/kubernetes/csi-rbdplugin.yaml index 22fe06f3f..95bd27b7d 100644 --- a/deploy/rbd/kubernetes/csi-rbdplugin.yaml +++ b/deploy/rbd/kubernetes/csi-rbdplugin.yaml @@ -57,6 +57,7 @@ spec: - "--drivername=rbd.csi.ceph.com" - "--containerized=true" - "--metadatastorage=k8s_configmap" + - "--configroot=k8s_objects" env: - name: HOST_ROOTFS value: "/rootfs" diff --git a/docs/deploy-rbd.md b/docs/deploy-rbd.md index aef0ff32c..a0f9b2d34 100644 --- a/docs/deploy-rbd.md +++ b/docs/deploy-rbd.md @@ -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`: diff --git a/examples/README.md b/examples/README.md index f295a3cbf..aa7f710d9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 `` 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 `` 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 `` as the value for - the option `clusterID`, can now be created on the cluster. +Storage class and snapshot class, using `` 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. diff --git a/examples/rbd/storageclass.yaml b/examples/rbd/storageclass.yaml index d77a39ba9..3fa497919 100644 --- a/examples/rbd/storageclass.yaml +++ b/examples/rbd/storageclass.yaml @@ -11,6 +11,9 @@ parameters: # OR, # Ceph cluster fsid, of the cluster to provision storage from # clusterID: + # If using clusterID based configuration, CSI pods need to be passed in a + # secret named ceph-cluster- 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- 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- secret and skipped + # here adminid: admin userid: kubernetes # uncomment the following to use rbd-nbd as mounter on supported nodes diff --git a/examples/rbd/template-ceph-cluster-ID-config.yaml b/examples/rbd/template-ceph-cluster-ID-config.yaml deleted file mode 100644 index c859f22ee..000000000 --- a/examples/rbd/template-ceph-cluster-ID-config.yaml +++ /dev/null @@ -1,22 +0,0 @@ ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: ceph-cluster- - namespace: default -data: - cluster-config: | - { - "version": 1, - "cluster-config": { - "cluster-fsid": "", - "monitors": [ - "", - "" - ], - "pools": [ - "", - "" - ] - } - } diff --git a/examples/rbd/template-ceph-cluster-ID-provisioner-secret.yaml b/examples/rbd/template-ceph-cluster-ID-provisioner-secret.yaml deleted file mode 100644 index 707307596..000000000 --- a/examples/rbd/template-ceph-cluster-ID-provisioner-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - # The 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--provisioner-secret - namespace: default -data: - # Base64 encoded ID of the admin name - # - Typically output of: `echo -n "" | base64` - # Substitute the entire string including angle braces, with the base64 value - subjectid: - # 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: diff --git a/examples/rbd/template-ceph-cluster-ID-publish-secret.yaml b/examples/rbd/template-ceph-cluster-ID-publish-secret.yaml deleted file mode 100644 index ca31c0917..000000000 --- a/examples/rbd/template-ceph-cluster-ID-publish-secret.yaml +++ /dev/null @@ -1,19 +0,0 @@ ---- -apiVersion: v1 -kind: Secret -metadata: - # The 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--publish-secret - namespace: default -data: - # Base64 encoded ID of the admin name - # - Typically output of: `echo -n "" | base64` - # Substitute the entire string including angle braces, with the base64 value - subjectid: - # 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: diff --git a/examples/rbd/template-ceph-cluster-ID-secret.yaml b/examples/rbd/template-ceph-cluster-ID-secret.yaml new file mode 100644 index 000000000..d4c70c0fb --- /dev/null +++ b/examples/rbd/template-ceph-cluster-ID-secret.yaml @@ -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 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- + 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 and comma separated list of pool names from which volumes + # can be provisioned + pools: + # Base64 encoded admin ID to use for provisioning + # - Typically output of: `echo -n "" | base64` + # Substitute the entire string including angle braces, with the base64 value + adminid: + # 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 user ID to use for publishing + # - Typically output of: `echo -n "" | base64` + # Substitute the entire string including angle braces, with the base64 value + userid: + # 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: diff --git a/examples/rbd/template-csi-rbdplugin-patch.yaml b/examples/rbd/template-csi-rbdplugin-patch.yaml index 016f8c711..4a507b2f2 100644 --- a/examples/rbd/template-csi-rbdplugin-patch.yaml +++ b/examples/rbd/template-csi-rbdplugin-patch.yaml @@ -12,22 +12,10 @@ spec: containers: - name: csi-rbdplugin volumeMounts: - - name: provisioner-secret- - mountPath: "/etc/ceph-cluster--provisioner-secret" - readOnly: true - - name: publish-secret- - mountPath: "/etc/ceph-cluster--publish-secret" - readOnly: true - name: ceph-cluster- - mountPath: "/etc/ceph-cluster-/" + mountPath: "/etc/csi-config/ceph-cluster-" readOnly: true volumes: - - name: provisioner-secret- - secret: - secretName: ceph-cluster--provisioner-secret - - name: publish-secret- - secret: - secretName: ceph-cluster--publish-secret - name: ceph-cluster- - configMap: - name: ceph-cluster- + secret: + secretName: ceph-cluster- diff --git a/examples/rbd/template-csi-rbdplugin-provisioner-patch.yaml b/examples/rbd/template-csi-rbdplugin-provisioner-patch.yaml index 083f14d2c..1d12e634b 100644 --- a/examples/rbd/template-csi-rbdplugin-provisioner-patch.yaml +++ b/examples/rbd/template-csi-rbdplugin-provisioner-patch.yaml @@ -12,22 +12,10 @@ spec: containers: - name: csi-rbdplugin volumeMounts: - - name: provisioner-secret- - mountPath: "/etc/ceph-cluster--provisioner-secret" - readOnly: true - - name: publish-secret- - mountPath: "/etc/ceph-cluster--publish-secret" - readOnly: true - name: ceph-cluster- - mountPath: "/etc/ceph-cluster-/" + mountPath: "/etc/csi-config/ceph-cluster-" readOnly: true volumes: - - name: provisioner-secret- - secret: - secretName: ceph-cluster--provisioner-secret - - name: publish-secret- - secret: - secretName: ceph-cluster--publish-secret - name: ceph-cluster- - configMap: - name: ceph-cluster- + secret: + secretName: ceph-cluster- diff --git a/pkg/rbd/rbd.go b/pkg/rbd/rbd.go index 62983fdb2..aa8dd3930 100644 --- a/pkg/rbd/rbd.go +++ b/pkg/rbd/rbd.go @@ -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) diff --git a/pkg/rbd/rbd_util.go b/pkg/rbd/rbd_util.go index 5ff7c5104..f19535e03 100644 --- a/pkg/rbd/rbd_util.go +++ b/pkg/rbd/rbd_util.go @@ -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) { diff --git a/pkg/util/configstore.go b/pkg/util/configstore.go new file mode 100644 index 000000000..5064699ef --- /dev/null +++ b/pkg/util/configstore.go @@ -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 +} diff --git a/pkg/util/configstore_test.go b/pkg/util/configstore_test.go new file mode 100644 index 000000000..d85cefffa --- /dev/null +++ b/pkg/util/configstore_test.go @@ -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") + } +} diff --git a/pkg/util/fileconfig.go b/pkg/util/fileconfig.go index fb58dcbb8..3bac9a3d0 100644 --- a/pkg/util/fileconfig.go +++ b/pkg/util/fileconfig.go @@ -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-config - - BasePath/ceph-cluster--provisioner-secret/credentials - - BasePath/ceph-cluster--provisioner-secret/subjectid - - BasePath/ceph-cluster--publish-secret/credentials - - BasePath/ceph-cluster--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": "", - "monitors": [ - "IP/DNS:port", - "IP/DNS:port" - ], - "pools": [ - "", - "" - ] - } - } - - 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-, where 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 } diff --git a/pkg/util/fileconfig_test.go b/pkg/util/fileconfig_test.go deleted file mode 100644 index ae86e2de0..000000000 --- a/pkg/util/fileconfig_test.go +++ /dev/null @@ -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!") - } -} diff --git a/pkg/util/k8sconfig.go b/pkg/util/k8sconfig.go new file mode 100644 index 000000000..08e69dcca --- /dev/null +++ b/pkg/util/k8sconfig.go @@ -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-, where 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 +}