Files
ma/nextflow/plugins/nf-seqera/src/main/io/seqera/util/SchemaMapperUtil.groovy
2026-04-29 23:01:54 +02:00

289 lines
11 KiB
Groovy

/*
* Copyright 2013-2026, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.seqera.util
import groovy.transform.CompileStatic
import io.seqera.config.MachineRequirementOpts
import io.seqera.sched.api.schema.v1a1.DiskAllocation
import io.seqera.sched.api.schema.v1a1.DiskRequirement
import io.seqera.sched.api.schema.v1a1.EcsCapacityMode
import io.seqera.sched.api.schema.v1a1.MachineRequirement
import io.seqera.sched.api.schema.v1a1.PriceModel as SchedPriceModel
import io.seqera.sched.api.schema.v1a1.ProvisioningModel
import nextflow.cloud.types.PriceModel
import nextflow.fusion.FusionConfig
import nextflow.util.MemoryUnit
/**
* Utility class to map Nextflow config objects to Sched API schema objects.
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@CompileStatic
class SchemaMapperUtil {
/** Default EBS volume type - gp3 provides good balance of price and performance */
static final String DEFAULT_DISK_TYPE = 'ebs/gp3'
/** Default throughput in MiB/s - Fusion recommended setting for optimal I/O */
static final int DEFAULT_DISK_THROUGHPUT_MIBPS = 325
/** Supported EBS volume types */
static final Set<String> SUPPORTED_DISK_TYPES = Set.of(
'ebs/gp3', // General purpose SSD (default)
'ebs/gp2', // General purpose SSD (legacy)
'ebs/io1', // Provisioned IOPS SSD
'ebs/io2', // Provisioned IOPS SSD (higher durability)
'ebs/st1', // Throughput optimized HDD
'ebs/sc1' // Cold HDD
)
/**
* Maps MachineRequirementOpts to MachineRequirement API object.
*
* @param opts the config options (can be null)
* @return the MachineRequirement API object, or null if opts is null or has no settings
*/
static MachineRequirement toMachineRequirement(MachineRequirementOpts opts) {
if (!opts)
return null
final diskReq = toDiskRequirement(opts.diskSize, opts)
final capacityMode = toEcsCapacityMode(opts.capacityMode)
if (!opts.provisioning && !opts.maxSpotAttempts && !opts.machineTypes && !diskReq && !capacityMode)
return null
new MachineRequirement()
.provisioning(toProvisioningModel(opts.provisioning))
.maxSpotAttempts(opts.maxSpotAttempts)
.machineTypes(opts.machineTypes)
.disk(diskReq)
.capacityMode(capacityMode)
}
/**
* Maps MachineRequirementOpts to MachineRequirement API object, merging with task arch.
*
* @param opts the config options (can be null)
* @param taskArch the task container platform/arch (can be null)
* @return the MachineRequirement API object, or null if no settings
*/
static MachineRequirement toMachineRequirement(MachineRequirementOpts opts, String taskArch) {
return toMachineRequirement(opts, taskArch, null, false)
}
/**
* Maps MachineRequirementOpts to MachineRequirement API object, merging with task arch, disk, and snapshots.
*
* @param opts the config options (can be null)
* @param taskArch the task container platform/arch (can be null)
* @param diskSize the disk size from task config (can be null)
* @param snapshotEnabled whether Fusion snapshots are enabled
* @return the MachineRequirement API object, or null if no settings
*/
static MachineRequirement toMachineRequirement(MachineRequirementOpts opts, String taskArch, MemoryUnit diskSize, boolean snapshotEnabled) {
final arch = taskArch
final provisioning = opts?.provisioning
final maxSpotAttempts = opts?.maxSpotAttempts
?: (snapshotEnabled ? FusionConfig.DEFAULT_SNAPSHOT_MAX_SPOT_ATTEMPTS : null)
final machineTypes = opts?.machineTypes
// task disk overrides config disk
final effectiveDiskSize = diskSize ?: opts?.diskSize
final diskReq = toDiskRequirement(effectiveDiskSize, opts)
final capacityMode = toEcsCapacityMode(opts?.capacityMode)
// return null if no settings
if (!arch && !provisioning && !maxSpotAttempts && !machineTypes && !diskReq && !snapshotEnabled && !capacityMode)
return null
new MachineRequirement()
.arch(arch)
.provisioning(toProvisioningModel(provisioning))
.maxSpotAttempts(maxSpotAttempts)
.machineTypes(machineTypes)
.disk(diskReq)
.snapshotEnabled(snapshotEnabled ? Boolean.TRUE : null)
.capacityMode(capacityMode)
}
/**
* Maps a disk size to DiskRequirement API object.
* Uses config options if provided, otherwise defaults to Fusion recommended settings:
* EBS gp3 volume with 325 MiB/s throughput.
*
* For 'node' allocation (default), only sizeGiB and mountPath are applicable.
* For 'task' allocation, all EBS options can be specified.
*
* @param diskSize the disk size (can be null)
* @param opts the machine requirement options with disk settings (can be null)
* @return the DiskRequirement API object, or null if diskSize is null or zero
*/
static DiskRequirement toDiskRequirement(MemoryUnit diskSize, MachineRequirementOpts opts=null) {
final allocation = toDiskAllocation(opts?.diskAllocation)
// For NVMe allocation, disk size is instance-determined — no size required
if (allocation == DiskAllocation.NVME) {
validateNvmeAllocationOpts(opts)
final DiskRequirement req = new DiskRequirement()
req.sizeGiB(diskSize ? diskSize.toGiga() as Integer : 0)
req.allocation(allocation)
req.mountPath(opts?.diskMountPath)
return req
}
if (!diskSize || diskSize.toGiga() <= 0)
return null
final effectiveAllocation = allocation ?: DiskAllocation.NODE
// For 'node' allocation (default), only size and mountPath are valid
if (effectiveAllocation == DiskAllocation.NODE) {
validateNodeAllocationOpts(opts)
final DiskRequirement req = new DiskRequirement()
req.sizeGiB(diskSize.toGiga() as Integer)
req.allocation(effectiveAllocation)
req.mountPath(opts?.diskMountPath)
return req
}
// For 'task' allocation, apply EBS-specific options
final type = opts?.diskType ?: DEFAULT_DISK_TYPE
// Validate disk type is supported
if (!SUPPORTED_DISK_TYPES.contains(type)) {
throw new IllegalArgumentException("Invalid disk type: ${type}. Supported types: ${SUPPORTED_DISK_TYPES.join(', ')}")
}
final throughput = opts?.diskThroughputMiBps ?: DEFAULT_DISK_THROUGHPUT_MIBPS
final iops = opts?.diskIops
final encrypted = opts?.diskEncrypted ?: false
final DiskRequirement req = new DiskRequirement()
req.sizeGiB(diskSize.toGiga() as Integer)
req.volumeType(type)
req.encrypted(encrypted)
req.allocation(allocation)
// Only set throughput for gp3 volumes
if (type == DEFAULT_DISK_TYPE) {
req.throughputMiBps(throughput)
}
// Set IOPS if provided
if (iops) {
req.iops(iops)
}
req.mountPath(opts?.diskMountPath)
return req
}
/**
* Validates that no EBS-specific options are set when using 'node' allocation.
* Node allocation uses instance storage, not EBS volumes.
*
* @param opts the machine requirement options
* @throws IllegalArgumentException if EBS-specific options are set with node allocation
*/
private static void validateNodeAllocationOpts(MachineRequirementOpts opts) {
if (!opts)
return
final List<String> invalidOpts = []
if (opts.diskType)
invalidOpts.add('diskType')
if (opts.diskThroughputMiBps)
invalidOpts.add('diskThroughputMiBps')
if (opts.diskIops)
invalidOpts.add('diskIops')
if (opts.diskEncrypted)
invalidOpts.add('diskEncrypted')
if (invalidOpts) {
throw new IllegalArgumentException(
"The following options are not valid with 'node' disk allocation: ${invalidOpts.join(', ')}. " +
"Node allocation uses instance storage; only disk size is applicable."
)
}
}
/**
* Validates that no EBS-specific options are set when using 'nvme' allocation.
* NVMe uses instance store disks, not EBS volumes.
*/
private static void validateNvmeAllocationOpts(MachineRequirementOpts opts) {
if (!opts)
return
final List<String> invalidOpts = []
if (opts.diskType)
invalidOpts.add('diskType')
if (opts.diskThroughputMiBps)
invalidOpts.add('diskThroughputMiBps')
if (opts.diskIops)
invalidOpts.add('diskIops')
if (opts.diskEncrypted)
invalidOpts.add('diskEncrypted')
if (invalidOpts) {
throw new IllegalArgumentException(
"The following options are not valid with 'nvme' disk allocation: ${invalidOpts.join(', ')}. " +
"NVMe uses instance store disks; EBS options are not applicable."
)
}
}
/**
* Maps a disk allocation string to DiskAllocation enum.
*
* @param value the disk allocation string (task, node)
* @return the DiskAllocation enum value, or null if value is null
*/
static DiskAllocation toDiskAllocation(String value) {
value ? DiskAllocation.fromValue(value) : null
}
/**
* Maps a capacity mode string to EcsCapacityMode enum.
*
* @param value the capacity mode string (managed, asg)
* @return the EcsCapacityMode enum value, or null if value is null
*/
static EcsCapacityMode toEcsCapacityMode(String value) {
value ? EcsCapacityMode.fromValue(value) : null
}
/**
* Maps a provisioning string to ProvisioningModel enum.
*
* @param value the provisioning string (spot, ondemand, spotFirst)
* @return the ProvisioningModel enum value, or null if value is null
*/
static ProvisioningModel toProvisioningModel(String value) {
value ? ProvisioningModel.fromValue(value) : null
}
/**
* Maps Sched API PriceModel to Nextflow PriceModel.
*
* @param schedPriceModel the Sched API price model
* @return the Nextflow PriceModel, or null if input is null or unknown
*/
static PriceModel toPriceModel(SchedPriceModel schedPriceModel) {
if (schedPriceModel == null)
return null
switch (schedPriceModel) {
case SchedPriceModel.SPOT:
return PriceModel.spot
case SchedPriceModel.STANDARD:
return PriceModel.standard
default:
return null
}
}
}