Warning
Experimental — for demo purposes only. DO NOT USE IN PRODUCTION. This project is an experiment and is provided as-is, without any guarantees of stability, support, or maintenance.
A Kubernetes Container Storage Interface (CSI) driver for Open Telekom Cloud (OTC),
providing dynamic provisioning of persistent volumes backed by OTC's Elastic Volume
Service (EVS). This is a fresh, OTC-native driver authenticated via AK/SK credentials,
not based on upstream cinder-csi-plugin.
Status: v1 (dynamic provisioning), live-verified against OTC eu-de — full
PVC → EVS create → attach → mount → write → detach → delete lifecycle with zero
orphaned resources.
Note
This project was developed with substantial AI assistance (Claude Code). All changes are human-reviewed, covered by unit tests, and the driver has been live-verified against real OTC infrastructure.
- Storage backend: OTC EVS (Cinder-compatible block storage)
- Authentication: AK/SK (same credential pattern as
cloud-provider-otc) - Deployment: Helm chart (Controller Deployment + Node DaemonSet)
- Kubernetes compatibility: Works with clusters provisioned by
cluster-api-provider-otcwithcloud-provider-otcalready running (Node providerID + topology labels available). Any cluster whose Nodes carry anotc:///<uuid>providerID and thetopology.kubernetes.io/zonelabel works.
| Feature | Status |
|---|---|
Dynamic provisioning (CreateVolume/DeleteVolume) |
✅ |
Attach/detach (ControllerPublish/Unpublish, OTC-native cloudservers API) |
✅ |
| Format + mount (ext4 default, fsType/mountFlags from the StorageClass) | ✅ |
AZ-correct placement (WaitForFirstConsumer topology) |
✅ |
EVS volume types via StorageClass type parameter (default SSD) |
✅ |
| Access mode | ReadWriteOnce only |
| Snapshots / restore | ❌ planned |
| Volume expansion (resize) | ❌ planned |
| Raw block volumes | ❌ planned |
| Volume cloning, multi-attach, encryption parameters | ❌ not yet scoped |
Each deferred feature can be added without breaking the v1 API surface.
Upstream cinder-csi-plugin authenticates only via Keystone (OpenStack
username/password or application-credentials), not AK/SK. Adopting it on OTC would
require provisioning a second credential class per cluster, breaking the unified
AK/SK model already established by the CCM and CAPI provider. OTC's gophertelekomcloud
SDK provides full EVS support via AK/SK (openstack/evs/* APIs), so building a fresh
driver is the right fit — following the same reasoning that led to sub-project #1
(CCM skeleton).
Released images are published to GHCR on every tag:
ghcr.io/d-velop/csi-driver-otc:<version>
To build and push your own:
make docker-build IMG=docker.io/myorg/csi-driver-otc:v0.1.3
make docker-push IMG=docker.io/myorg/csi-driver-otc:v0.1.3make helm-templateThis outputs the rendered chart YAML to stdout. Customize values in charts/csi-driver-otc/values.yaml
as needed (e.g., image repository/tag, node selectors, tolerations).
The driver reads cluster credentials from a cloud.conf file embedded in a Kubernetes
Secret with key cloud.conf. This Secret must be mounted to the Controller and Node
Pods.
The cloud.conf file uses an INI format with a [Global] section, identical to the
cloud-provider-otc CCM:
[Global]
access-key = <AK from otc-credentials Secret>
secret-key = <SK from otc-credentials Secret>
domain-name = <OTC domain, e.g., OTC-EU-DE-000000000010000XXXXX>
region = <OTC region, e.g., eu-de>
project-name = <OTC project, defaults to region if omitted>Secrets (access-key, secret-key) are never logged or included in error messages.
The otc-credentials principal behind these values needs specific IAM grants
beyond the basics (EVS volume access, and — non-obvious, cost two rounds of live
debugging to find — ecs:serverVolumeAttachments:* for attach/detach, since the
legacy Nova volume-attach endpoint is blocked on OTC regardless of policy). See
cloud-provider-otc/docs/context/otc-iam-permissions.md
for the full picture.
Example Secret:
apiVersion: v1
kind: Secret
metadata:
name: csi-otc-cloud-config
namespace: kube-system
type: Opaque
data:
cloud.conf: <base64-encoded cloud.conf file>Follow the same manual delivery pattern that established the CCM (sub-project #2):
-
Render the chart on your build machine:
make helm-template > csi-driver-rendered.yaml -
Package the chart output into a ConfigMap resource for GitOps delivery:
apiVersion: v1 kind: ConfigMap metadata: name: csi-driver-otc-chart namespace: capi-system # or your GitOps namespace data: csi-driver.yaml: | <paste rendered csi-driver-rendered.yaml here>
-
Create the cloud.conf Secret from the cluster's existing
otc-credentialsSecret (created bycluster-api-provider-otcduring cluster provisioning):kubectl get secret otc-credentials -n kube-system -o jsonpath='{.data.cloud\.conf}' | base64 -d > /tmp/cloud.conf kubectl create secret generic csi-otc-cloud-config \ --from-file=cloud.conf=/tmp/cloud.conf \ -n kube-system
Alternatively, use a
csi-cloud-configwrite analogous to the CCM'sreconcileCCMConfig(a follow-up sub-project, not part of this release). -
Create a ClusterResourceSet in the sibling
cluster-api-provider-otcrepo to deliver both resources:In
cluster-api-provider-otc/config/samples/otc-csi-driver-crs.yaml:apiVersion: addons.cluster.x-k8s.io/v1beta1 kind: ClusterResourceSet metadata: name: otc-csi-driver spec: clusterSelector: matchLabels: csi: "true" # or your selector resources: - name: csi-driver-chart kind: ConfigMap resourceRef: apiVersion: v1 name: csi-driver-otc-chart - name: csi-cloud-config kind: Secret resourceRef: apiVersion: v1 name: csi-otc-cloud-config
Then apply the CRS to the management cluster to deliver both the chart and Secret to target workload clusters.
Note: ClusterResourceSet authoring and any later automation of csi-cloud-config
delivery (analogous to the CCM's per-cluster config automation) are out of scope for
this release. They are natural follow-up sub-projects after live verification, exactly
as the CCM's own per-cluster config delivery (reconcileCCMConfig) was added post-release.
This release focuses on getting the driver itself working, tested, and deliverable as a
Helm chart.
Unit tests use in-memory fakes for the EVS and ECS-attach clients:
go test ./...All tests pass without requiring a live OTC environment.
make build # go build ./...
go vet ./...
gofmt -l . # must be empty
make test # go test ./...
make lint # golangci-lint run
make helm-lint # helm lint on the chartExpected result: build clean, gofmt empty, all tests PASS, lint "0 issues", helm lint 0 failed.
Live-verified end-to-end on a real OTC test cluster (2026-07-08): PVC create → EVS volume create → bind → attach → mount → write → detach → delete, full cycle, zero orphaned resources. To repeat:
- Deploy the driver via the Helm chart (or manually)
- Create a
PersistentVolumeClaimusing theotc-evsStorageClass - Create a Pod mounting the PVC and write a file
- Delete the Pod and PVC
- Verify in the OTC console that the EVS volume and its attachment are cleaned up
Two real SDK/API mismatches were found only by this live run, not by unit tests or review, and are now fixed:
- EVS: the vendored v1 SDK package's
Createhardcoded OkCodes that didn't match OTC's actual (async, 202) response, and itsNamefield used the wrong JSON tag — together they brokeCreateVolume's idempotency and orphaned volumes on every retry. Fixed by movingCreateVolume/GetVolumeto EVS v3 (job-based) andFindVolumeByNametov2/cloudvolumes.List. - Attach/detach: the legacy OpenStack-compatible Nova endpoint
(
/servers/{id}/os-volume_attachments) is blocked by OTC's policy for regular tenant IAM credentials regardless of grants. Fixed by switching to OTC's nativecloudserversattach/detach/list API (pkg/otc/attach.go).
The three correctness gaps noted after the original design review are also now
fixed: CreateVolume rejects a same-named volume with a different size/type/AZ as
ALREADY_EXISTS; DeleteVolume maps an in-use volume to FAILED_PRECONDITION
instead of generic Internal; NodeStageVolume polls for the device path to
appear (up to 10s) before calling FormatAndMount.
See CONTRIBUTING.md. For security issues, see SECURITY.md.