/* * 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 */ @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 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 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 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 } } }