feat: add immediate and deferred provisioning modes
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user