From 082e80606fe94df0c4868730a4a8721f2ffd358a Mon Sep 17 00:00:00 2001 From: Josh Date: Sat, 13 Jun 2026 17:58:50 +0330 Subject: [PATCH] feat: add immediate and deferred provisioning modes --- README.md | 34 ++++++++--- charts/kloud-csi/templates/storageclass.yaml | 39 +++++++++++- charts/kloud-csi/values.yaml | 15 ++++- pkg/csi/api/client_test.go | 15 +++++ pkg/csi/driver/controller.go | 7 ++- pkg/csi/driver/controller_test.go | 63 ++++++++++++++++++++ pkg/csi/provisioner/types.go | 5 +- 7 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 pkg/csi/driver/controller_test.go diff --git a/README.md b/README.md index 301fa69..46800b8 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Standard CSI sidecars are bundled with each workload: ## Features -- Dynamic provisioning via a `StorageClass` (optional, enabled by default) +- Dynamic provisioning via two `StorageClass` types (optional, enabled by default) - Controller publish/unpublish (attach/detach) - Node stage/unstage and publish/unpublish (mount operations) - Block volumes are not supported @@ -144,11 +144,14 @@ Helm fails at render time if `serverURL` or credentials are missing. | `existingSecret` | `""` | Use an existing secret instead of creating one | | `existingSecretAccessTokenKey` | `access-token` | Key in the secret holding the token | | `driver.name` | `storage.csi.kloud.team` | CSI driver name | -| `storageClass.enabled` | `true` | Create a `StorageClass` | -| `storageClass.name` | `kloud-csi` | StorageClass name | -| `storageClass.isDefault` | `false` | Mark as the default StorageClass | +| `storageClass.enabled` | `true` | Create StorageClasses | +| `storageClass.immediate.enabled` | `true` | Create immediate-binding StorageClass | +| `storageClass.immediate.name` | `kloud-csi` | Immediate-binding StorageClass name | +| `storageClass.immediate.isDefault` | `true` | Mark immediate StorageClass as default | +| `storageClass.waitForFirstConsumer.enabled` | `true` | Create WaitForFirstConsumer StorageClass | +| `storageClass.waitForFirstConsumer.name` | `kloud-csi-wait-for-first-consumer` | WaitForFirstConsumer StorageClass name | +| `storageClass.waitForFirstConsumer.isDefault` | `false` | Mark WaitForFirstConsumer StorageClass as default | | `storageClass.reclaimPolicy` | `Delete` | `Delete` or `Retain` | -| `storageClass.volumeBindingMode` | `WaitForFirstConsumer` | Volume binding mode | | `controller.replicas` | `1` | Controller deployment replicas | | `rbac.create` | `true` | Create RBAC for controller and node | @@ -159,7 +162,8 @@ See [`charts/kloud-csi/values.yaml`](charts/kloud-csi/values.yaml) for the full The chart creates: - `CSIDriver` — registers the driver with Kubernetes -- `StorageClass` — optional, for dynamic provisioning +- `StorageClass` (Immediate) — optional, default class for immediate provisioning +- `StorageClass` (WaitForFirstConsumer) — optional, delayed provisioning until pod scheduling - `Deployment` — controller + sidecars - `DaemonSet` — node plugin + registrar on every node - `ConfigMap` — minimal HCL stub (settings come from env vars) @@ -179,7 +183,7 @@ helm uninstall kloud-csi --namespace kube-system ## Using persistent volumes -With the default StorageClass installed, create a PVC: +With the default immediate StorageClass installed, create a PVC: ```yaml apiVersion: v1 @@ -197,6 +201,22 @@ spec: Then mount it in a pod as usual. +To delay provisioning until the first pod is scheduled, use the second class: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: my-data-wait +spec: + accessModes: + - ReadWriteOnce + storageClassName: kloud-csi-wait-for-first-consumer + resources: + requests: + storage: 10Gi +``` + ## Configuration reference The driver accepts configuration from an HCL file, environment variables, or both. Environment variables take precedence and are what the Helm chart uses. diff --git a/charts/kloud-csi/templates/storageclass.yaml b/charts/kloud-csi/templates/storageclass.yaml index 896297a..1c3c56c 100644 --- a/charts/kloud-csi/templates/storageclass.yaml +++ b/charts/kloud-csi/templates/storageclass.yaml @@ -1,9 +1,10 @@ {{- if .Values.storageClass.enabled }} +{{- if .Values.storageClass.immediate.enabled }} apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: - name: {{ .Values.storageClass.name }} - {{- if .Values.storageClass.isDefault }} + name: {{ .Values.storageClass.immediate.name }} + {{- if .Values.storageClass.immediate.isDefault }} annotations: storageclass.kubernetes.io/is-default-class: "true" {{- end }} @@ -11,6 +12,38 @@ metadata: {{- include "kloud-csi.labels" . | nindent 4 }} provisioner: {{ .Values.driver.name }} reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }} -volumeBindingMode: {{ .Values.storageClass.volumeBindingMode }} +volumeBindingMode: Immediate allowVolumeExpansion: {{ .Values.storageClass.allowVolumeExpansion }} +{{- with .Values.storageClass.immediate.parameters }} +parameters: + {{- range $key, $value := . }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} +{{- end }} +{{- end }} +{{- if and .Values.storageClass.immediate.enabled .Values.storageClass.waitForFirstConsumer.enabled }} +--- +{{- end }} +{{- if .Values.storageClass.waitForFirstConsumer.enabled }} +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ .Values.storageClass.waitForFirstConsumer.name }} + {{- if .Values.storageClass.waitForFirstConsumer.isDefault }} + annotations: + storageclass.kubernetes.io/is-default-class: "true" + {{- end }} + labels: + {{- include "kloud-csi.labels" . | nindent 4 }} +provisioner: {{ .Values.driver.name }} +reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }} +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: {{ .Values.storageClass.allowVolumeExpansion }} +{{- with .Values.storageClass.waitForFirstConsumer.parameters }} +parameters: + {{- range $key, $value := . }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} +{{- end }} +{{- end }} {{- end }} diff --git a/charts/kloud-csi/values.yaml b/charts/kloud-csi/values.yaml index 23a0e3b..4fd737f 100644 --- a/charts/kloud-csi/values.yaml +++ b/charts/kloud-csi/values.yaml @@ -44,11 +44,20 @@ sidecars: storageClass: enabled: true - name: kloud-csi - isDefault: false reclaimPolicy: Delete - volumeBindingMode: WaitForFirstConsumer allowVolumeExpansion: false + immediate: + enabled: true + name: kloud-csi + isDefault: true + parameters: + kks.kloud/provisioning-mode: immediate + waitForFirstConsumer: + enabled: true + name: kloud-csi-wait-for-first-consumer + isDefault: false + parameters: + kks.kloud/provisioning-mode: deferred controller: replicas: 1 diff --git a/pkg/csi/api/client_test.go b/pkg/csi/api/client_test.go index 5ce87e8..21eef93 100644 --- a/pkg/csi/api/client_test.go +++ b/pkg/csi/api/client_test.go @@ -3,6 +3,7 @@ package api import ( "context" "encoding/json" + "io" "net/http" "net/http/httptest" "testing" @@ -40,6 +41,17 @@ func TestClientCreateVolume(t *testing.T) { if r.Method != http.MethodPost || r.URL.Path != "/v1/volumes" { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } + body, err := io.ReadAll(r.Body) + if err != nil { + t.Fatalf("read request body: %v", err) + } + var got provisioner.CreateVolumeRequest + if err := json.Unmarshal(body, &got); err != nil { + t.Fatalf("decode request body: %v", err) + } + if got.Parameters["kks.kloud/provisioning-mode"] != "immediate" { + t.Fatalf("unexpected parameters: %#v", got.Parameters) + } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(provisioner.Volume{ VolumeID: "abc123/k8s-volumes/pvc-1", @@ -52,6 +64,9 @@ func TestClientCreateVolume(t *testing.T) { vol, err := client.CreateVolume(context.Background(), provisioner.CreateVolumeRequest{ Name: "pvc-1", SizeBytes: 1024, + Parameters: map[string]string{ + "kks.kloud/provisioning-mode": "immediate", + }, }) if err != nil { t.Fatalf("CreateVolume failed: %v", err) diff --git a/pkg/csi/driver/controller.go b/pkg/csi/driver/controller.go index 7fec546..69aac04 100644 --- a/pkg/csi/driver/controller.go +++ b/pkg/csi/driver/controller.go @@ -3,8 +3,8 @@ package driver import ( "context" - "github.com/container-storage-interface/spec/lib/go/csi" "github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner" + "github.com/container-storage-interface/spec/lib/go/csi" ) type ControllerServer struct { @@ -33,8 +33,9 @@ func (s *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolu } vol, err := s.d.backend.CreateVolume(ctx, provisioner.CreateVolumeRequest{ - Name: sanitizeVolumeName(req.GetName()), - SizeBytes: capacity, + Name: sanitizeVolumeName(req.GetName()), + SizeBytes: capacity, + Parameters: req.GetParameters(), }) if err != nil { return nil, internalError(err) diff --git a/pkg/csi/driver/controller_test.go b/pkg/csi/driver/controller_test.go new file mode 100644 index 0000000..31f47a2 --- /dev/null +++ b/pkg/csi/driver/controller_test.go @@ -0,0 +1,63 @@ +package driver + +import ( + "context" + "testing" + + "github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner" + "github.com/container-storage-interface/spec/lib/go/csi" +) + +type fakeBackend struct { + createReq provisioner.CreateVolumeRequest +} + +func (f *fakeBackend) ClusterInfo(context.Context) (provisioner.ClusterInfo, error) { + return provisioner.ClusterInfo{}, nil +} + +func (f *fakeBackend) CreateVolume(_ context.Context, req provisioner.CreateVolumeRequest) (provisioner.Volume, error) { + f.createReq = req + return provisioner.Volume{VolumeID: "vol-1", SizeBytes: req.SizeBytes}, nil +} + +func (f *fakeBackend) DeleteVolume(context.Context, string) error { return nil } + +func (f *fakeBackend) VolumeExists(context.Context, string) (bool, error) { return false, nil } + +func (f *fakeBackend) PublishVolume(context.Context, string, string) (provisioner.PublishVolumeResponse, error) { + return provisioner.PublishVolumeResponse{}, nil +} + +func (f *fakeBackend) UnpublishVolume(context.Context, string, string) error { return nil } + +func (f *fakeBackend) Close() error { return nil } + +func TestControllerCreateVolumeForwardsStorageClassParameters(t *testing.T) { + t.Parallel() + + backend := &fakeBackend{} + s := &ControllerServer{d: &Driver{backend: backend}} + + _, err := s.CreateVolume(context.Background(), &csi.CreateVolumeRequest{ + Name: "pvc-demo", + CapacityRange: &csi.CapacityRange{ + RequiredBytes: 10, + }, + VolumeCapabilities: []*csi.VolumeCapability{{ + AccessType: &csi.VolumeCapability_Mount{Mount: &csi.VolumeCapability_MountVolume{}}, + AccessMode: &csi.VolumeCapability_AccessMode{Mode: csi.VolumeCapability_AccessMode_SINGLE_NODE_WRITER}, + }}, + Parameters: map[string]string{ + "kks.kloud/provisioning-mode": "deferred", + "example": "value", + }, + }) + if err != nil { + t.Fatalf("CreateVolume returned error: %v", err) + } + + if backend.createReq.Parameters["kks.kloud/provisioning-mode"] != "deferred" { + t.Fatalf("unexpected forwarded parameters: %#v", backend.createReq.Parameters) + } +} diff --git a/pkg/csi/provisioner/types.go b/pkg/csi/provisioner/types.go index a8faf74..026f0df 100644 --- a/pkg/csi/provisioner/types.go +++ b/pkg/csi/provisioner/types.go @@ -19,8 +19,9 @@ type Volume struct { } type CreateVolumeRequest struct { - Name string `json:"name"` - SizeBytes int64 `json:"size_bytes"` + Name string `json:"name"` + SizeBytes int64 `json:"size_bytes"` + Parameters map[string]string `json:"parameters,omitempty"` } type PublishVolumeRequest struct {