diff --git a/README.md b/README.md index 65b4913..fe32c9b 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,17 @@ make image-build # Build container image using podman/docker | POST | /api/v1alpha1/providers | External SP registration | | GET | /api/v1alpha1/providers/{provider_id} | Get a single SP by ID | +Embedded SPs do **not** expose their service-type REST APIs (for example +`/api/v1alpha1/volumes`) on the agent. The agent forwards **create** and +**delete** in-process (or to external SP HTTP endpoints). **List and get** +of provisioned instances go through the control-plane SP Resource Manager: + +- `GET /api/v1alpha1/service-type-instances?service_type=` +- `GET /api/v1alpha1/service-type-instances/{instance_id}` + +Instance status is updated when embedded SPs publish CloudEvents to NATS and +the SPRM status consumer processes them. + ## License Apache 2.0 — see [LICENSE](LICENSE) for details. diff --git a/api/storage/v1alpha1/openapi.yaml b/api/storage/v1alpha1/openapi.yaml index d7071b7..23a5fc8 100644 --- a/api/storage/v1alpha1/openapi.yaml +++ b/api/storage/v1alpha1/openapi.yaml @@ -1,9 +1,8 @@ openapi: 3.0.4 info: - title: Kubernetes Storage Service Provider API + title: Storage Service Provider API description: | - REST API for managing persistent storage volumes (PersistentVolumeClaims) - on Kubernetes clusters on behalf of DCM. + REST API for managing persistent storage volumes on behalf of DCM. This API follows AEP standards for consistency across DCM services. version: 1.0.0 contact: @@ -14,7 +13,7 @@ info: servers: - url: / - description: K8s Storage Service Provider + description: Storage Service Provider tags: - name: Health @@ -69,7 +68,7 @@ paths: post: summary: Create Volume - description: Create a new storage volume instance (PersistentVolumeClaim) + description: Create a new storage volume instance operationId: createVolume tags: - Volumes @@ -107,13 +106,13 @@ paths: schema: $ref: '#/components/schemas/Error' '409': - description: Volume with the same instance ID (PVC name) already exists + description: Volume with the same instance ID already exists content: application/problem+json: schema: $ref: '#/components/schemas/Error' '422': - description: Requested StorageClass does not exist + description: Requested storage class does not exist content: application/problem+json: schema: @@ -171,7 +170,7 @@ paths: delete: summary: Delete Volume - description: Delete a specific volume instance (PVC) + description: Delete a specific volume instance operationId: deleteVolume tags: - Volumes @@ -213,7 +212,7 @@ components: singular: health plural: healths singleton: true - type: k8s-storage-service-provider.dcm.io/health + type: storage-service-provider.dcm.io/health patterns: - health required: @@ -228,7 +227,7 @@ components: format: uri-reference readOnly: true description: Resource type identifier - example: "k8s-storage-service-provider.dcm.io/health" + example: "storage-service-provider.dcm.io/health" status: type: string description: Health status @@ -259,24 +258,6 @@ components: - DELETED description: Current status of the storage instance (CloudEvents payload uses this enum) - VolumeAccessMode: - type: string - enum: - - ReadWriteOnce - - ReadOnlyMany - - ReadWriteMany - description: | - PVC access mode. Controls attachment scope and permissions at creation - time (Kubernetes-specific; see service type definitions). - example: ReadWriteOnce - - VolumeMode: - type: string - enum: - - Filesystem - - Block - description: PVC volume mode - VolumeMetadata: type: object description: Resource metadata for identification @@ -285,7 +266,7 @@ components: properties: name: type: string - description: DCM instance ID and Kubernetes PVC name (AEP-122) + description: DCM instance ID (AEP-122) pattern: '^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' minLength: 1 maxLength: 63 @@ -297,40 +278,24 @@ components: namespace: type: string readOnly: true - description: Kubernetes namespace where the PVC was created + description: Platform namespace where the volume was created storage_class: type: string readOnly: true - description: StorageClass name bound to the PVC + description: Storage class name bound to the volume volume_name: type: string readOnly: true - description: Bound PersistentVolume name when available - - KubernetesProviderHints: - type: object - description: Kubernetes-specific PVC settings - additionalProperties: false - properties: - storage_class: - type: string - description: StorageClass name (overrides SP default) - volume_mode: - $ref: '#/components/schemas/VolumeMode' - access_mode: - $ref: '#/components/schemas/VolumeAccessMode' + description: Bound backend volume name when available ProviderHints: type: object description: Provider-specific hints from the catalog additionalProperties: true - properties: - kubernetes: - $ref: '#/components/schemas/KubernetesProviderHints' StorageSpec: type: object - description: Storage specification for creating a PVC (implements the portable storage service type) + description: Storage specification for creating a volume (implements the portable storage service type) required: - service_type - metadata @@ -352,9 +317,9 @@ components: Volume: type: object - description: Volume resource representing a PVC instance + description: Volume resource representing a storage instance x-aep-resource: - type: k8s-storage-service-provider.dcm.io/volume + type: storage-service-provider.dcm.io/volume singular: volume plural: volumes patterns: diff --git a/api/storage/v1alpha1/spec.gen.go b/api/storage/v1alpha1/spec.gen.go index 7195311..c400c9f 100644 --- a/api/storage/v1alpha1/spec.gen.go +++ b/api/storage/v1alpha1/spec.gen.go @@ -20,50 +20,46 @@ import ( // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", + "A1YYnUK0iBi4aYP9YFYryN9ndb/6bwAAAP//", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/api/storage/v1alpha1/types.gen.go b/api/storage/v1alpha1/types.gen.go index e57246a..1660442 100644 --- a/api/storage/v1alpha1/types.gen.go +++ b/api/storage/v1alpha1/types.gen.go @@ -4,8 +4,6 @@ package v1alpha1 import ( - "encoding/json" - "fmt" "time" ) @@ -87,45 +85,6 @@ func (e StorageStatus) Valid() bool { } } -// Defines values for VolumeAccessMode. -const ( - ReadOnlyMany VolumeAccessMode = "ReadOnlyMany" - ReadWriteMany VolumeAccessMode = "ReadWriteMany" - ReadWriteOnce VolumeAccessMode = "ReadWriteOnce" -) - -// Valid indicates whether the value is a known member of the VolumeAccessMode enum. -func (e VolumeAccessMode) Valid() bool { - switch e { - case ReadOnlyMany: - return true - case ReadWriteMany: - return true - case ReadWriteOnce: - return true - default: - return false - } -} - -// Defines values for VolumeMode. -const ( - Block VolumeMode = "Block" - Filesystem VolumeMode = "Filesystem" -) - -// Valid indicates whether the value is a known member of the VolumeMode enum. -func (e VolumeMode) Valid() bool { - switch e { - case Block: - return true - case Filesystem: - return true - default: - return false - } -} - // Error RFC 9457 compliant error response type Error struct { // Detail Human-readable explanation specific to this occurrence @@ -161,7 +120,7 @@ type Health struct { // Type Resource type identifier // - // Example: k8s-storage-service-provider.dcm.io/health + // Example: storage-service-provider.dcm.io/health Type *string `json:"type,omitempty"` // Uptime Seconds since the service provider started @@ -175,30 +134,10 @@ type Health struct { Version *string `json:"version,omitempty"` } -// KubernetesProviderHints Kubernetes-specific PVC settings -type KubernetesProviderHints struct { - // AccessMode PVC access mode. Controls attachment scope and permissions at creation - // time (Kubernetes-specific; see service type definitions). - // - // - // Example: ReadWriteOnce - AccessMode *VolumeAccessMode `json:"access_mode,omitempty"` - - // StorageClass StorageClass name (overrides SP default) - StorageClass *string `json:"storage_class,omitempty"` - - // VolumeMode PVC volume mode - VolumeMode *VolumeMode `json:"volume_mode,omitempty"` -} - // ProviderHints Provider-specific hints from the catalog -type ProviderHints struct { - // Kubernetes Kubernetes-specific PVC settings - Kubernetes *KubernetesProviderHints `json:"kubernetes,omitempty"` - AdditionalProperties map[string]interface{} `json:"-"` -} +type ProviderHints map[string]interface{} -// StorageSpec Storage specification for creating a PVC (implements the portable storage service type) +// StorageSpec Storage specification for creating a volume (implements the portable storage service type) type StorageSpec struct { // Capacity Requested storage capacity (e.g., "100Gi") // @@ -221,7 +160,7 @@ type StorageSpecServiceType string // StorageStatus Current status of the storage instance (CloudEvents payload uses this enum) type StorageStatus string -// Volume Volume resource representing a PVC instance +// Volume Volume resource representing a storage instance type Volume struct { // CreateTime Timestamp when the volume was created CreateTime *time.Time `json:"create_time,omitempty"` @@ -236,7 +175,7 @@ type Volume struct { // Example: volumes/app-data-volume Path *string `json:"path,omitempty"` - // Spec Storage specification for creating a PVC (implements the portable storage service type) + // Spec Storage specification for creating a volume (implements the portable storage service type) Spec StorageSpec `json:"spec"` Status *StorageStatus `json:"status,omitempty"` @@ -244,12 +183,6 @@ type Volume struct { UpdateTime *time.Time `json:"update_time,omitempty"` } -// VolumeAccessMode PVC access mode. Controls attachment scope and permissions at creation -// time (Kubernetes-specific; see service type definitions). -// -// Example: ReadWriteOnce -type VolumeAccessMode string - // VolumeList Paginated list of volume instances type VolumeList struct { // NextPageToken Token for retrieving the next page of results @@ -262,22 +195,19 @@ type VolumeMetadata struct { // Labels Custom key-value pairs for tagging and filtering Labels *map[string]string `json:"labels,omitempty"` - // Name DCM instance ID and Kubernetes PVC name (AEP-122) + // Name DCM instance ID (AEP-122) Name string `json:"name"` - // Namespace Kubernetes namespace where the PVC was created + // Namespace Platform namespace where the volume was created Namespace *string `json:"namespace,omitempty"` - // StorageClass StorageClass name bound to the PVC + // StorageClass Storage class name bound to the volume StorageClass *string `json:"storage_class,omitempty"` - // VolumeName Bound PersistentVolume name when available + // VolumeName Bound backend volume name when available VolumeName *string `json:"volume_name,omitempty"` } -// VolumeMode PVC volume mode -type VolumeMode string - // VolumeIdPath defines model for VolumeIdPath. type VolumeIdPath = string @@ -300,71 +230,3 @@ type CreateVolumeParams struct { // CreateVolumeJSONRequestBody defines body for CreateVolume for application/json ContentType. type CreateVolumeJSONRequestBody = Volume - -// Getter for additional properties for ProviderHints. Returns the specified -// element and whether it was found -func (a ProviderHints) Get(fieldName string) (value interface{}, found bool) { - if a.AdditionalProperties != nil { - value, found = a.AdditionalProperties[fieldName] - } - return -} - -// Setter for additional properties for ProviderHints -func (a *ProviderHints) Set(fieldName string, value interface{}) { - if a.AdditionalProperties == nil { - a.AdditionalProperties = make(map[string]interface{}) - } - a.AdditionalProperties[fieldName] = value -} - -// Override default JSON handling for ProviderHints to handle AdditionalProperties -func (a *ProviderHints) UnmarshalJSON(b []byte) error { - object := make(map[string]json.RawMessage) - err := json.Unmarshal(b, &object) - if err != nil { - return err - } - - if raw, found := object["kubernetes"]; found { - err = json.Unmarshal(raw, &a.Kubernetes) - if err != nil { - return fmt.Errorf("error reading 'kubernetes': %w", err) - } - delete(object, "kubernetes") - } - - if len(object) != 0 { - a.AdditionalProperties = make(map[string]interface{}) - for fieldName, fieldBuf := range object { - var fieldVal interface{} - err := json.Unmarshal(fieldBuf, &fieldVal) - if err != nil { - return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err) - } - a.AdditionalProperties[fieldName] = fieldVal - } - } - return nil -} - -// Override default JSON handling for ProviderHints to handle AdditionalProperties -func (a ProviderHints) MarshalJSON() ([]byte, error) { - var err error - object := make(map[string]json.RawMessage) - - if a.Kubernetes != nil { - object["kubernetes"], err = json.Marshal(a.Kubernetes) - if err != nil { - return nil, fmt.Errorf("error marshaling 'kubernetes': %w", err) - } - } - - for fieldName, field := range a.AdditionalProperties { - object[fieldName], err = json.Marshal(field) - if err != nil { - return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err) - } - } - return json.Marshal(object) -} diff --git a/internal/embedded/storage/health.go b/internal/embedded/storage/health.go index 4cc1057..54ad9d5 100644 --- a/internal/embedded/storage/health.go +++ b/internal/embedded/storage/health.go @@ -6,7 +6,7 @@ import ( "github.com/dcm-project/environment-agent/internal/health/monitor" ) -// storageHealthChecker checks backing Kubernetes connectivity for agent SP health. +// storageHealthChecker checks backing platform connectivity for agent SP health. type storageHealthChecker interface { CheckHealth(ctx context.Context) error } diff --git a/internal/embedded/storage/setup.go b/internal/embedded/storage/setup.go index fa858bc..5e5b33b 100644 --- a/internal/embedded/storage/setup.go +++ b/internal/embedded/storage/setup.go @@ -1,4 +1,4 @@ -// Package storage embeds the k8s storage service provider in the agent. +// Package storage embeds the storage service provider in the agent. package storage import ( diff --git a/internal/openshift/storage/kubernetes/convert.go b/internal/openshift/storage/kubernetes/convert.go index 370c300..22b4c0e 100644 --- a/internal/openshift/storage/kubernetes/convert.go +++ b/internal/openshift/storage/kubernetes/convert.go @@ -103,15 +103,15 @@ func volumeFromPVC(pvc *corev1.PersistentVolumeClaim, instanceID string) v1alpha } } -func buildPVC(spec v1alpha1.StorageSpec, cfg K8sConfig, labels map[string]string) (*corev1.PersistentVolumeClaim, error) { +func buildPVC(spec v1alpha1.StorageSpec, cfg K8sConfig, labels map[string]string, hints *k8sProviderHints) (*corev1.PersistentVolumeClaim, error) { qty, err := resource.ParseQuantity(spec.Capacity) if err != nil { - return nil, &store.InvalidArgumentError{Message: fmt.Sprintf("invalid capacity %q: %v", spec.Capacity, err)} + return nil, &store.InvalidArgumentError{Message: fmt.Sprintf("invalid capacity %q", spec.Capacity), Err: err} } - accessMode, err := resolveAccessMode(spec, cfg.DefaultAccessMode) + accessMode, err := resolveAccessMode(hints, cfg.DefaultAccessMode) if err != nil { - return nil, err + return nil, fmt.Errorf("resolving access mode: %w", err) } pvc := &corev1.PersistentVolumeClaim{ @@ -130,25 +130,23 @@ func buildPVC(spec v1alpha1.StorageSpec, cfg K8sConfig, labels map[string]string }, } - storageClass := resolveStorageClass(spec, cfg.DefaultStorageClass) + storageClass := resolveStorageClass(hints, cfg.DefaultStorageClass) if storageClass != "" { pvc.Spec.StorageClassName = &storageClass } - if spec.ProviderHints != nil && spec.ProviderHints.Kubernetes != nil { - if vm := spec.ProviderHints.Kubernetes.VolumeMode; vm != nil { - mode, err := resolveVolumeMode(*vm) - if err != nil { - return nil, err - } - pvc.Spec.VolumeMode = &mode + if hints != nil && hints.VolumeMode != nil { + mode, err := resolveVolumeMode(*hints.VolumeMode) + if err != nil { + return nil, fmt.Errorf("resolving volume mode: %w", err) } + pvc.Spec.VolumeMode = &mode } return pvc, nil } -func resolveVolumeMode(vm v1alpha1.VolumeMode) (corev1.PersistentVolumeMode, error) { +func resolveVolumeMode(vm string) (corev1.PersistentVolumeMode, error) { mode := corev1.PersistentVolumeMode(vm) switch mode { case corev1.PersistentVolumeFilesystem, corev1.PersistentVolumeBlock: @@ -158,10 +156,9 @@ func resolveVolumeMode(vm v1alpha1.VolumeMode) (corev1.PersistentVolumeMode, err } } -func resolveAccessMode(spec v1alpha1.StorageSpec, defaultMode string) (corev1.PersistentVolumeAccessMode, error) { - if spec.ProviderHints != nil && spec.ProviderHints.Kubernetes != nil && - spec.ProviderHints.Kubernetes.AccessMode != nil { - mode := corev1.PersistentVolumeAccessMode(*spec.ProviderHints.Kubernetes.AccessMode) +func resolveAccessMode(hints *k8sProviderHints, defaultMode string) (corev1.PersistentVolumeAccessMode, error) { + if hints != nil && hints.AccessMode != nil { + mode := corev1.PersistentVolumeAccessMode(*hints.AccessMode) switch mode { case corev1.ReadWriteOnce, corev1.ReadOnlyMany, corev1.ReadWriteMany: return mode, nil @@ -181,10 +178,9 @@ func resolveAccessMode(spec v1alpha1.StorageSpec, defaultMode string) (corev1.Pe return corev1.ReadWriteOnce, nil } -func resolveStorageClass(spec v1alpha1.StorageSpec, defaultClass string) string { - if spec.ProviderHints != nil && spec.ProviderHints.Kubernetes != nil && - spec.ProviderHints.Kubernetes.StorageClass != nil { - return *spec.ProviderHints.Kubernetes.StorageClass +func resolveStorageClass(hints *k8sProviderHints, defaultClass string) string { + if hints != nil && hints.StorageClass != nil { + return *hints.StorageClass } return defaultClass } @@ -197,27 +193,27 @@ func storageClassFromPVC(pvc *corev1.PersistentVolumeClaim) string { } func providerHintsFromPVC(pvc *corev1.PersistentVolumeClaim) *v1alpha1.ProviderHints { - var k8sHints v1alpha1.KubernetesProviderHints + var hints k8sProviderHints hasHints := false if sc := storageClassFromPVC(pvc); sc != "" { - k8sHints.StorageClass = &sc + hints.StorageClass = &sc hasHints = true } if pvc.Spec.VolumeMode != nil { - vm := v1alpha1.VolumeMode(*pvc.Spec.VolumeMode) - k8sHints.VolumeMode = &vm + vm := string(*pvc.Spec.VolumeMode) + hints.VolumeMode = &vm hasHints = true } if len(pvc.Spec.AccessModes) > 0 { - am := v1alpha1.VolumeAccessMode(pvc.Spec.AccessModes[0]) - k8sHints.AccessMode = &am + am := string(pvc.Spec.AccessModes[0]) + hints.AccessMode = &am hasHints = true } if !hasHints { return nil } - return &v1alpha1.ProviderHints{Kubernetes: &k8sHints} + return providerHintsFromK8s(hints) } func userLabelsFromPVC(pvc *corev1.PersistentVolumeClaim) map[string]string { diff --git a/internal/openshift/storage/kubernetes/hints.go b/internal/openshift/storage/kubernetes/hints.go new file mode 100644 index 0000000..b7888ca --- /dev/null +++ b/internal/openshift/storage/kubernetes/hints.go @@ -0,0 +1,74 @@ +package kubernetes + +import ( + "encoding/json" + "fmt" + + v1alpha1 "github.com/dcm-project/environment-agent/api/storage/v1alpha1" + "github.com/dcm-project/environment-agent/internal/openshift/storage/store" +) + +var allowedK8sHintKeys = map[string]struct{}{ + "storage_class": {}, + "volume_mode": {}, + "access_mode": {}, +} + +type k8sProviderHints struct { + StorageClass *string `json:"storage_class,omitempty"` + VolumeMode *string `json:"volume_mode,omitempty"` + AccessMode *string `json:"access_mode,omitempty"` +} + +func (h k8sProviderHints) empty() bool { + return h.StorageClass == nil && h.VolumeMode == nil && h.AccessMode == nil +} + +func k8sHintsFromSpec(spec v1alpha1.StorageSpec) (*k8sProviderHints, error) { + if spec.ProviderHints == nil { + return nil, nil + } + raw, found := (*spec.ProviderHints)["kubernetes"] + if !found { + return nil, nil + } + data, err := json.Marshal(raw) + if err != nil { + return nil, fmt.Errorf("marshaling kubernetes provider hints: %w", err) + } + + var rawMap map[string]json.RawMessage + if err := json.Unmarshal(data, &rawMap); err != nil { + return nil, &store.InvalidArgumentError{ + Message: "invalid kubernetes provider hints", + Err: err, + } + } + for key := range rawMap { + if _, ok := allowedK8sHintKeys[key]; !ok { + return nil, &store.InvalidArgumentError{ + Message: fmt.Sprintf("unknown kubernetes provider hint %q", key), + } + } + } + + var hints k8sProviderHints + if err := json.Unmarshal(data, &hints); err != nil { + return nil, &store.InvalidArgumentError{ + Message: "invalid kubernetes provider hints", + Err: err, + } + } + if hints.empty() { + return nil, nil + } + return &hints, nil +} + +func providerHintsFromK8s(hints k8sProviderHints) *v1alpha1.ProviderHints { + if hints.empty() { + return nil + } + ph := v1alpha1.ProviderHints{"kubernetes": hints} + return &ph +} diff --git a/internal/openshift/storage/kubernetes/store_create.go b/internal/openshift/storage/kubernetes/store_create.go index b44cd84..81b3c71 100644 --- a/internal/openshift/storage/kubernetes/store_create.go +++ b/internal/openshift/storage/kubernetes/store_create.go @@ -19,14 +19,19 @@ func (s *K8sVolumeStore) Create(ctx context.Context, spec v1alpha1.StorageSpec, labels = mergeLabels(labels, *spec.Metadata.Labels) } - storageClass := resolveStorageClass(spec, s.cfg.DefaultStorageClass) + hints, err := k8sHintsFromSpec(spec) + if err != nil { + return nil, fmt.Errorf("parsing kubernetes provider hints for volume %q: %w", name, err) + } + + storageClass := resolveStorageClass(hints, s.cfg.DefaultStorageClass) if storageClass != "" { if err := s.validateStorageClass(ctx, storageClass); err != nil { return nil, fmt.Errorf("validating StorageClass %q for volume %q: %w", storageClass, name, err) } } - pvc, err := buildPVC(spec, s.cfg, labels) + pvc, err := buildPVC(spec, s.cfg, labels, hints) if err != nil { return nil, fmt.Errorf("building PVC for volume %q: %w", name, err) } diff --git a/internal/openshift/storage/kubernetes/store_crud_test.go b/internal/openshift/storage/kubernetes/store_crud_test.go index 4fba015..55ecb59 100644 --- a/internal/openshift/storage/kubernetes/store_crud_test.go +++ b/internal/openshift/storage/kubernetes/store_crud_test.go @@ -119,6 +119,18 @@ func minimalVolumeSpec(name string) v1alpha1.StorageSpec { } } +func withK8sProviderHints(spec v1alpha1.StorageSpec, hints map[string]string) v1alpha1.StorageSpec { + ph := v1alpha1.ProviderHints{"kubernetes": hints} + spec.ProviderHints = &ph + return spec +} + +func withK8sProviderHintsRaw(spec v1alpha1.StorageSpec, hints map[string]interface{}) v1alpha1.StorageSpec { + ph := v1alpha1.ProviderHints{"kubernetes": hints} + spec.ProviderHints = &ph + return spec +} + func createStorageClass(client *fake.Clientset, name string) { sc := &storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{Name: name}, @@ -154,17 +166,11 @@ var _ = Describe("K8s Volume Store CRUD", func() { It("applies provider hints for storage class, access mode, and volume mode", func() { s, client := newTestStore(defaultConfig()) createStorageClass(client, "fast-ssd") - accessMode := v1alpha1.ReadWriteMany - volumeMode := v1alpha1.Block - sc := "fast-ssd" - spec := minimalVolumeSpec("hinted-vol") - spec.ProviderHints = &v1alpha1.ProviderHints{ - Kubernetes: &v1alpha1.KubernetesProviderHints{ - StorageClass: &sc, - AccessMode: &accessMode, - VolumeMode: &volumeMode, - }, - } + spec := withK8sProviderHints(minimalVolumeSpec("hinted-vol"), map[string]string{ + "storage_class": "fast-ssd", + "access_mode": "ReadWriteMany", + "volume_mode": "Block", + }) _, err := s.Create(context.Background(), spec, "hinted-vol") Expect(err).NotTo(HaveOccurred()) @@ -241,11 +247,9 @@ var _ = Describe("K8s Volume Store CRUD", func() { It("returns failed precondition when StorageClass does not exist", func() { s, _ := newTestStore(defaultConfig()) - sc := "missing-sc" - spec := minimalVolumeSpec("app-data") - spec.ProviderHints = &v1alpha1.ProviderHints{ - Kubernetes: &v1alpha1.KubernetesProviderHints{StorageClass: &sc}, - } + spec := withK8sProviderHints(minimalVolumeSpec("app-data"), map[string]string{ + "storage_class": "missing-sc", + }) _, err := s.Create(context.Background(), spec, "app-data") Expect(err).To(HaveOccurred()) @@ -266,11 +270,9 @@ var _ = Describe("K8s Volume Store CRUD", func() { It("returns invalid argument for unsupported volume mode", func() { s, _ := newTestStore(defaultConfig()) - badMode := v1alpha1.VolumeMode("Raw") - spec := minimalVolumeSpec("app-data") - spec.ProviderHints = &v1alpha1.ProviderHints{ - Kubernetes: &v1alpha1.KubernetesProviderHints{VolumeMode: &badMode}, - } + spec := withK8sProviderHints(minimalVolumeSpec("app-data"), map[string]string{ + "volume_mode": "Raw", + }) _, err := s.Create(context.Background(), spec, "app-data") Expect(err).To(HaveOccurred()) @@ -278,6 +280,36 @@ var _ = Describe("K8s Volume Store CRUD", func() { Expect(errors.As(err, &invalid)).To(BeTrue()) Expect(invalid.Message).To(ContainSubstring("volume mode")) }) + + It("returns invalid argument for misspelled kubernetes provider hint keys", func() { + s, _ := newTestStore(defaultConfig()) + spec := withK8sProviderHintsRaw(minimalVolumeSpec("app-data"), map[string]interface{}{ + "storageclass": "fast-ssd", + }) + + _, err := s.Create(context.Background(), spec, "app-data") + Expect(err).To(HaveOccurred()) + var invalid *store.InvalidArgumentError + Expect(errors.As(err, &invalid)).To(BeTrue()) + Expect(invalid.Message).To(ContainSubstring("unknown kubernetes provider hint")) + }) + + It("returns invalid argument for malformed hints before validating default StorageClass", func() { + cfg := defaultConfig() + cfg.DefaultStorageClass = "missing-default-sc" + s, _ := newTestStore(cfg) + spec := withK8sProviderHintsRaw(minimalVolumeSpec("app-data"), map[string]interface{}{ + "storage_class": []string{"not-a-string"}, + }) + + _, err := s.Create(context.Background(), spec, "app-data") + Expect(err).To(HaveOccurred()) + var invalid *store.InvalidArgumentError + Expect(errors.As(err, &invalid)).To(BeTrue()) + var failed *store.FailedPreconditionError + Expect(errors.As(err, &failed)).To(BeFalse(), + "malformed hints should not be masked by missing default StorageClass validation") + }) }) Describe("Get", func() { diff --git a/internal/openshift/storage/store/errors.go b/internal/openshift/storage/store/errors.go index e739782..3b00d5f 100644 --- a/internal/openshift/storage/store/errors.go +++ b/internal/openshift/storage/store/errors.go @@ -23,12 +23,20 @@ func (e *ConflictError) Error() string { // InvalidArgumentError indicates a validation failure in the request. type InvalidArgumentError struct { Message string + Err error } func (e *InvalidArgumentError) Error() string { + if e.Err != nil { + return fmt.Sprintf("%s: %v", e.Message, e.Err) + } return e.Message } +func (e *InvalidArgumentError) Unwrap() error { + return e.Err +} + // FailedPreconditionError indicates a request that cannot be fulfilled due to // policy or cluster state (HTTP 422). type FailedPreconditionError struct {