feat: add immediate and deferred provisioning modes

This commit is contained in:
2026-06-13 17:58:50 +03:30
parent d36b309c9c
commit 082e80606f
7 changed files with 160 additions and 18 deletions
+27 -7
View File
@@ -40,7 +40,7 @@ Standard CSI sidecars are bundled with each workload:
## Features ## 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) - Controller publish/unpublish (attach/detach)
- Node stage/unstage and publish/unpublish (mount operations) - Node stage/unstage and publish/unpublish (mount operations)
- Block volumes are not supported - 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 | | `existingSecret` | `""` | Use an existing secret instead of creating one |
| `existingSecretAccessTokenKey` | `access-token` | Key in the secret holding the token | | `existingSecretAccessTokenKey` | `access-token` | Key in the secret holding the token |
| `driver.name` | `storage.csi.kloud.team` | CSI driver name | | `driver.name` | `storage.csi.kloud.team` | CSI driver name |
| `storageClass.enabled` | `true` | Create a `StorageClass` | | `storageClass.enabled` | `true` | Create StorageClasses |
| `storageClass.name` | `kloud-csi` | StorageClass name | | `storageClass.immediate.enabled` | `true` | Create immediate-binding StorageClass |
| `storageClass.isDefault` | `false` | Mark as the default 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.reclaimPolicy` | `Delete` | `Delete` or `Retain` |
| `storageClass.volumeBindingMode` | `WaitForFirstConsumer` | Volume binding mode |
| `controller.replicas` | `1` | Controller deployment replicas | | `controller.replicas` | `1` | Controller deployment replicas |
| `rbac.create` | `true` | Create RBAC for controller and node | | `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: The chart creates:
- `CSIDriver` — registers the driver with Kubernetes - `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 - `Deployment` — controller + sidecars
- `DaemonSet` — node plugin + registrar on every node - `DaemonSet` — node plugin + registrar on every node
- `ConfigMap` — minimal HCL stub (settings come from env vars) - `ConfigMap` — minimal HCL stub (settings come from env vars)
@@ -179,7 +183,7 @@ helm uninstall kloud-csi --namespace kube-system
## Using persistent volumes ## Using persistent volumes
With the default StorageClass installed, create a PVC: With the default immediate StorageClass installed, create a PVC:
```yaml ```yaml
apiVersion: v1 apiVersion: v1
@@ -197,6 +201,22 @@ spec:
Then mount it in a pod as usual. 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 ## 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. The driver accepts configuration from an HCL file, environment variables, or both. Environment variables take precedence and are what the Helm chart uses.
+36 -3
View File
@@ -1,9 +1,10 @@
{{- if .Values.storageClass.enabled }} {{- if .Values.storageClass.enabled }}
{{- if .Values.storageClass.immediate.enabled }}
apiVersion: storage.k8s.io/v1 apiVersion: storage.k8s.io/v1
kind: StorageClass kind: StorageClass
metadata: metadata:
name: {{ .Values.storageClass.name }} name: {{ .Values.storageClass.immediate.name }}
{{- if .Values.storageClass.isDefault }} {{- if .Values.storageClass.immediate.isDefault }}
annotations: annotations:
storageclass.kubernetes.io/is-default-class: "true" storageclass.kubernetes.io/is-default-class: "true"
{{- end }} {{- end }}
@@ -11,6 +12,38 @@ metadata:
{{- include "kloud-csi.labels" . | nindent 4 }} {{- include "kloud-csi.labels" . | nindent 4 }}
provisioner: {{ .Values.driver.name }} provisioner: {{ .Values.driver.name }}
reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }} reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }}
volumeBindingMode: {{ .Values.storageClass.volumeBindingMode }} volumeBindingMode: Immediate
allowVolumeExpansion: {{ .Values.storageClass.allowVolumeExpansion }} 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 }} {{- end }}
+12 -3
View File
@@ -44,11 +44,20 @@ sidecars:
storageClass: storageClass:
enabled: true enabled: true
name: kloud-csi
isDefault: false
reclaimPolicy: Delete reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: false 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: controller:
replicas: 1 replicas: 1
+15
View File
@@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@@ -40,6 +41,17 @@ func TestClientCreateVolume(t *testing.T) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/volumes" { if r.Method != http.MethodPost || r.URL.Path != "/v1/volumes" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) 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) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(provisioner.Volume{ _ = json.NewEncoder(w).Encode(provisioner.Volume{
VolumeID: "abc123/k8s-volumes/pvc-1", VolumeID: "abc123/k8s-volumes/pvc-1",
@@ -52,6 +64,9 @@ func TestClientCreateVolume(t *testing.T) {
vol, err := client.CreateVolume(context.Background(), provisioner.CreateVolumeRequest{ vol, err := client.CreateVolume(context.Background(), provisioner.CreateVolumeRequest{
Name: "pvc-1", Name: "pvc-1",
SizeBytes: 1024, SizeBytes: 1024,
Parameters: map[string]string{
"kks.kloud/provisioning-mode": "immediate",
},
}) })
if err != nil { if err != nil {
t.Fatalf("CreateVolume failed: %v", err) t.Fatalf("CreateVolume failed: %v", err)
+4 -3
View File
@@ -3,8 +3,8 @@ package driver
import ( import (
"context" "context"
"github.com/container-storage-interface/spec/lib/go/csi"
"github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner" "github.com/KubelanCloud/kks-csi-plugin/pkg/csi/provisioner"
"github.com/container-storage-interface/spec/lib/go/csi"
) )
type ControllerServer struct { 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{ vol, err := s.d.backend.CreateVolume(ctx, provisioner.CreateVolumeRequest{
Name: sanitizeVolumeName(req.GetName()), Name: sanitizeVolumeName(req.GetName()),
SizeBytes: capacity, SizeBytes: capacity,
Parameters: req.GetParameters(),
}) })
if err != nil { if err != nil {
return nil, internalError(err) return nil, internalError(err)
+63
View File
@@ -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)
}
}
+3 -2
View File
@@ -19,8 +19,9 @@ type Volume struct {
} }
type CreateVolumeRequest struct { type CreateVolumeRequest struct {
Name string `json:"name"` Name string `json:"name"`
SizeBytes int64 `json:"size_bytes"` SizeBytes int64 `json:"size_bytes"`
Parameters map[string]string `json:"parameters,omitempty"`
} }
type PublishVolumeRequest struct { type PublishVolumeRequest struct {