// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package structs import ( "errors" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/plugins/csi" ) // CSIVolumeMountOptions contains the mount options that should be provided when // attaching and mounting a volume with the CSIVolumeAttachmentModeFilesystem // attachment mode. type CSIVolumeMountOptions struct { // Filesystem is the desired filesystem type that should be used by the volume // (e.g ext4, aufs, zfs). This field is optional. Filesystem string // MountFlags contain the mount options that should be used for the volume. // These may contain _sensitive_ data and should not be leaked to logs or // returned in debugging data. // The total size of this field must be under 4KiB. MountFlags []string } func (c *CSIVolumeMountOptions) ToCSIMountOptions() *structs.CSIMountOptions { if c == nil { return nil } return &structs.CSIMountOptions{ FSType: c.Filesystem, MountFlags: c.MountFlags, } } // CSIControllerRequest interface lets us set embedded CSIControllerQuery // fields in the server type CSIControllerRequest interface { SetControllerNodeID(string) } // CSIControllerQuery is used to specify various flags for queries against CSI // Controllers type CSIControllerQuery struct { // ControllerNodeID is the node that should be targeted by the request ControllerNodeID string // PluginID is the plugin that should be targeted on the given node. PluginID string } func (c *CSIControllerQuery) SetControllerNodeID(nodeID string) { c.ControllerNodeID = nodeID } type ClientCSIControllerValidateVolumeRequest struct { VolumeID string // note: this is the external ID VolumeCapabilities []*structs.CSIVolumeCapability MountOptions *structs.CSIMountOptions Secrets structs.CSISecrets // COMPAT(1.1.1): the AttachmentMode and AccessMode fields are deprecated // and replaced by the VolumeCapabilities field above AttachmentMode structs.CSIVolumeAttachmentMode AccessMode structs.CSIVolumeAccessMode // Parameters as returned by storage provider in CreateVolumeResponse. // This field is optional. Parameters map[string]string // Volume context as returned by storage provider in CreateVolumeResponse. // This field is optional. Context map[string]string CSIControllerQuery } func (c *ClientCSIControllerValidateVolumeRequest) ToCSIRequest() (*csi.ControllerValidateVolumeRequest, error) { if c == nil { return &csi.ControllerValidateVolumeRequest{}, nil } creq := &csi.ControllerValidateVolumeRequest{ ExternalID: c.VolumeID, Secrets: c.Secrets, Capabilities: []*csi.VolumeCapability{}, Parameters: c.Parameters, Context: c.Context, } for _, cap := range c.VolumeCapabilities { ccap, err := csi.VolumeCapabilityFromStructs( cap.AttachmentMode, cap.AccessMode, c.MountOptions) if err != nil { return nil, err } creq.Capabilities = append(creq.Capabilities, ccap) } return creq, nil } type ClientCSIControllerValidateVolumeResponse struct { } type ClientCSIControllerAttachVolumeRequest struct { // The external ID of the volume to be used on a node. // This field is REQUIRED. VolumeID string // The ID of the node. This field is REQUIRED. This must match the NodeID that // is fingerprinted by the target node for this plugin name. ClientCSINodeID string // AttachmentMode indicates how the volume should be attached and mounted into // a task. AttachmentMode structs.CSIVolumeAttachmentMode // AccessMode indicates the desired concurrent access model for the volume AccessMode structs.CSIVolumeAccessMode // MountOptions is an optional field that contains additional configuration // when providing an AttachmentMode of CSIVolumeAttachmentModeFilesystem MountOptions *CSIVolumeMountOptions // ReadOnly indicates that the volume will be used in a readonly fashion. This // only works when the Controller has the PublishReadonly capability. ReadOnly bool // Secrets required by plugin to complete the controller publish // volume request. This field is OPTIONAL. Secrets structs.CSISecrets // Volume context as returned by storage provider in CreateVolumeResponse. // This field is optional. VolumeContext map[string]string CSIControllerQuery } func (c *ClientCSIControllerAttachVolumeRequest) ToCSIRequest() (*csi.ControllerPublishVolumeRequest, error) { if c == nil { return &csi.ControllerPublishVolumeRequest{}, nil } var opts = c.MountOptions.ToCSIMountOptions() caps, err := csi.VolumeCapabilityFromStructs(c.AttachmentMode, c.AccessMode, opts) if err != nil { return nil, err } return &csi.ControllerPublishVolumeRequest{ ExternalID: c.VolumeID, NodeID: c.ClientCSINodeID, VolumeCapability: caps, ReadOnly: c.ReadOnly, Secrets: c.Secrets, VolumeContext: c.VolumeContext, }, nil } // ClientCSIControllerDetachVolumeRequest is the RPC made from the server to // a Nomad client to tell a CSI controller plugin on that client to perform // ControllerUnpublish for a volume on a specific client. type ClientCSIControllerAttachVolumeResponse struct { // Opaque static publish properties of the volume. SP MAY use this // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` // calls calls have contextual information. // The contents of this field SHALL be opaque to nomad. // The contents of this field SHALL NOT be mutable. // The contents of this field SHALL be safe for the nomad to cache. // The contents of this field SHOULD NOT contain sensitive // information. // The contents of this field SHOULD NOT be used for uniquely // identifying a volume. The `volume_id` alone SHOULD be sufficient to // identify the volume. // This field is OPTIONAL and when present MUST be passed to // subsequent `NodeStageVolume` or `NodePublishVolume` calls PublishContext map[string]string } type ClientCSIControllerDetachVolumeRequest struct { // The external ID of the volume to be unpublished for the node // This field is REQUIRED. VolumeID string // The CSI Node ID for the Node that the volume should be detached from. // This field is REQUIRED. This must match the NodeID that is fingerprinted // by the target node for this plugin name. ClientCSINodeID string // Secrets required by plugin to complete the controller unpublish // volume request. This field is OPTIONAL. Secrets structs.CSISecrets CSIControllerQuery } func (c *ClientCSIControllerDetachVolumeRequest) ToCSIRequest() *csi.ControllerUnpublishVolumeRequest { if c == nil { return &csi.ControllerUnpublishVolumeRequest{} } return &csi.ControllerUnpublishVolumeRequest{ ExternalID: c.VolumeID, NodeID: c.ClientCSINodeID, } } type ClientCSIControllerDetachVolumeResponse struct{} // ClientCSIControllerCreateVolumeRequest the RPC made from the server to a // Nomad client to tell a CSI controller plugin on that client to perform // CreateVolume type ClientCSIControllerCreateVolumeRequest struct { Name string VolumeCapabilities []*structs.CSIVolumeCapability MountOptions *structs.CSIMountOptions Parameters map[string]string Secrets structs.CSISecrets CapacityMin int64 CapacityMax int64 SnapshotID string CloneID string RequestedTopologies *structs.CSITopologyRequest CSIControllerQuery } func (req *ClientCSIControllerCreateVolumeRequest) ToCSIRequest() (*csi.ControllerCreateVolumeRequest, error) { creq := &csi.ControllerCreateVolumeRequest{ Name: req.Name, VolumeCapabilities: []*csi.VolumeCapability{}, Parameters: req.Parameters, Secrets: req.Secrets, ContentSource: &csi.VolumeContentSource{ CloneID: req.CloneID, SnapshotID: req.SnapshotID, }, AccessibilityRequirements: &csi.TopologyRequirement{ Requisite: []*csi.Topology{}, Preferred: []*csi.Topology{}, }, } // The CSI spec requires that at least one of the fields in CapacityRange // must be defined. Fields set to 0 are considered unspecified and the // CreateVolumeRequest should not send an invalid value. if req.CapacityMin != 0 || req.CapacityMax != 0 { creq.CapacityRange = &csi.CapacityRange{ RequiredBytes: req.CapacityMin, LimitBytes: req.CapacityMax, } } for _, cap := range req.VolumeCapabilities { ccap, err := csi.VolumeCapabilityFromStructs(cap.AttachmentMode, cap.AccessMode, req.MountOptions) if err != nil { return nil, err } creq.VolumeCapabilities = append(creq.VolumeCapabilities, ccap) } if req.RequestedTopologies != nil { for _, topo := range req.RequestedTopologies.Required { creq.AccessibilityRequirements.Requisite = append( creq.AccessibilityRequirements.Requisite, &csi.Topology{ Segments: topo.Segments, }) } for _, topo := range req.RequestedTopologies.Preferred { creq.AccessibilityRequirements.Preferred = append( creq.AccessibilityRequirements.Preferred, &csi.Topology{ Segments: topo.Segments, }) } } return creq, nil } type ClientCSIControllerCreateVolumeResponse struct { ExternalVolumeID string CapacityBytes int64 VolumeContext map[string]string Topologies []*structs.CSITopology } // ClientCSIControllerExpandVolumeRequest is the RPC made from the server to a // Nomad client to tell a CSI controller plugin on that client to perform // ControllerExpandVolume type ClientCSIControllerExpandVolumeRequest struct { ExternalVolumeID string CapacityRange *csi.CapacityRange Secrets structs.CSISecrets VolumeCapability *csi.VolumeCapability CSIControllerQuery } func (req *ClientCSIControllerExpandVolumeRequest) ToCSIRequest() *csi.ControllerExpandVolumeRequest { csiReq := &csi.ControllerExpandVolumeRequest{ ExternalVolumeID: req.ExternalVolumeID, Capability: req.VolumeCapability, Secrets: req.Secrets, } if req.CapacityRange != nil { csiReq.RequiredBytes = req.CapacityRange.RequiredBytes csiReq.LimitBytes = req.CapacityRange.LimitBytes } return csiReq } type ClientCSIControllerExpandVolumeResponse struct { CapacityBytes int64 NodeExpansionRequired bool } // ClientCSIControllerDeleteVolumeRequest the RPC made from the server to a // Nomad client to tell a CSI controller plugin on that client to perform // DeleteVolume type ClientCSIControllerDeleteVolumeRequest struct { ExternalVolumeID string Secrets structs.CSISecrets CSIControllerQuery } func (req *ClientCSIControllerDeleteVolumeRequest) ToCSIRequest() *csi.ControllerDeleteVolumeRequest { return &csi.ControllerDeleteVolumeRequest{ ExternalVolumeID: req.ExternalVolumeID, Secrets: req.Secrets, } } type ClientCSIControllerDeleteVolumeResponse struct{} // ClientCSIControllerListVolumesVolumeRequest the RPC made from the server to // a Nomad client to tell a CSI controller plugin on that client to perform // ListVolumes type ClientCSIControllerListVolumesRequest struct { // these pagination fields match the pagination fields of the plugins and // not Nomad's own fields, for clarity when mapping between the two RPCs MaxEntries int32 StartingToken string CSIControllerQuery } func (req *ClientCSIControllerListVolumesRequest) ToCSIRequest() *csi.ControllerListVolumesRequest { return &csi.ControllerListVolumesRequest{ MaxEntries: req.MaxEntries, StartingToken: req.StartingToken, } } type ClientCSIControllerListVolumesResponse struct { Entries []*structs.CSIVolumeExternalStub NextToken string } // ClientCSIControllerCreateSnapshotRequest the RPC made from the server to a // Nomad client to tell a CSI controller plugin on that client to perform // CreateSnapshot type ClientCSIControllerCreateSnapshotRequest struct { ExternalSourceVolumeID string Name string Secrets structs.CSISecrets Parameters map[string]string CSIControllerQuery } func (req *ClientCSIControllerCreateSnapshotRequest) ToCSIRequest() (*csi.ControllerCreateSnapshotRequest, error) { return &csi.ControllerCreateSnapshotRequest{ VolumeID: req.ExternalSourceVolumeID, Name: req.Name, Secrets: req.Secrets, Parameters: req.Parameters, }, nil } type ClientCSIControllerCreateSnapshotResponse struct { ID string ExternalSourceVolumeID string SizeBytes int64 CreateTime int64 IsReady bool } // ClientCSIControllerDeleteSnapshotRequest the RPC made from the server to a // Nomad client to tell a CSI controller plugin on that client to perform // DeleteSnapshot type ClientCSIControllerDeleteSnapshotRequest struct { ID string Secrets structs.CSISecrets CSIControllerQuery } func (req *ClientCSIControllerDeleteSnapshotRequest) ToCSIRequest() *csi.ControllerDeleteSnapshotRequest { return &csi.ControllerDeleteSnapshotRequest{ SnapshotID: req.ID, Secrets: req.Secrets, } } type ClientCSIControllerDeleteSnapshotResponse struct{} // ClientCSIControllerListSnapshotsRequest is the RPC made from the server to // a Nomad client to tell a CSI controller plugin on that client to perform // ListSnapshots type ClientCSIControllerListSnapshotsRequest struct { // these pagination fields match the pagination fields of the plugins and // not Nomad's own fields, for clarity when mapping between the two RPCs MaxEntries int32 StartingToken string Secrets structs.CSISecrets CSIControllerQuery } func (req *ClientCSIControllerListSnapshotsRequest) ToCSIRequest() *csi.ControllerListSnapshotsRequest { return &csi.ControllerListSnapshotsRequest{ MaxEntries: req.MaxEntries, StartingToken: req.StartingToken, Secrets: req.Secrets, } } type ClientCSIControllerListSnapshotsResponse struct { Entries []*structs.CSISnapshot NextToken string } // ClientCSINodeDetachVolumeRequest is the RPC made from the server to // a Nomad client to tell a CSI node plugin on that client to perform // NodeUnpublish and NodeUnstage. type ClientCSINodeDetachVolumeRequest struct { PluginID string // ID of the plugin that manages the volume (required) VolumeID string // ID of the volume to be unpublished (required) AllocID string // ID of the allocation we're unpublishing for (required) NodeID string // ID of the Nomad client targeted ExternalID string // External ID of the volume to be unpublished (required) // These fields should match the original volume request so that // we can find the mount points on the client AttachmentMode structs.CSIVolumeAttachmentMode AccessMode structs.CSIVolumeAccessMode ReadOnly bool } type ClientCSINodeDetachVolumeResponse struct{} // ClientCSINodeExpandVolumeRequest is the RPC made from the server to // a Nomad client to tell a CSI node plugin on that client to perform // NodeExpandVolume. type ClientCSINodeExpandVolumeRequest struct { PluginID string // ID of the plugin that manages the volume (required) VolumeID string // ID of the volume to be expanded (required) ExternalID string // External ID of the volume to be expanded (required) // Capacity range (required) to be sent to the node plugin Capacity *csi.CapacityRange // Claim currently held for the allocation (required) // used to determine capabilities and the mount point on the client Claim *structs.CSIVolumeClaim } func (req *ClientCSINodeExpandVolumeRequest) Validate() error { var err error // These should not occur during normal operations; they're here // mainly to catch potential programmer error. if req.PluginID == "" { err = errors.Join(err, errors.New("PluginID is required")) } if req.VolumeID == "" { err = errors.Join(err, errors.New("VolumeID is required")) } if req.ExternalID == "" { err = errors.Join(err, errors.New("ExternalID is required")) } if req.Claim == nil { err = errors.Join(err, errors.New("Claim is required")) } else if req.Claim.AllocationID == "" { err = errors.Join(err, errors.New("Claim.AllocationID is required")) } return err } type ClientCSINodeExpandVolumeResponse struct { CapacityBytes int64 }