feat: support service.gpus and reservations.devices (#156)

* feat: support compose.yaml `service.gpus` and `service.deploy.resources.reservations.devices` DeviceRequests to Docker container
* Fix lint error
* Adjust test comments to work with linter
* Rename DeviceRequests to DeviceReservations to be more consistent with compose nomenclature
* Device reservation changes are immutable and should trigger a container recreate rather than update
This commit is contained in:
Justin Bradford
2025-10-27 16:49:38 +10:00
committed by GitHub
parent 0153e9f9e2
commit 3b4bcb7932
7 changed files with 653 additions and 1 deletions
+31
View File
@@ -8,6 +8,7 @@ import (
"strings"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/opencontainers/go-digest"
"github.com/psviderski/uncloud/pkg/api"
@@ -124,6 +125,9 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
MemoryReservation: int64(service.MemReservation),
}
// Convert GPU device requests from compose format, appending "gpu" capability.
resources.DeviceReservations = append(resources.DeviceReservations, deviceReservationsFromCompose(service.Gpus, "gpu")...)
// Map resources from deploy section if specified.
if service.Deploy != nil {
if service.Deploy.Resources.Limits != nil {
@@ -139,12 +143,39 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
if service.Deploy.Resources.Reservations.MemoryBytes > 0 {
resources.MemoryReservation = int64(service.Deploy.Resources.Reservations.MemoryBytes)
}
// Handle arbitrary device reservations (same structure as Gpus above).
resources.DeviceReservations = append(resources.DeviceReservations, deviceReservationsFromCompose(service.Deploy.Resources.Reservations.Devices)...)
}
}
return resources
}
// Converts compose-go DeviceRequest format to Docker API DeviceRequest format.
// Additional capabilities can be appended via extraCapabilities (e.g., "gpu" for service.Gpus).
func deviceReservationsFromCompose(devices []types.DeviceRequest, extraCapabilities ...string) []container.DeviceRequest {
if devices == nil {
return nil
}
requests := make([]container.DeviceRequest, 0, len(devices))
for _, deviceRequest := range devices {
// Docker expects an OR'd list of AND'd capabilities (e.g. [][]string),
// but compose-go provides a single AND'd list (e.g. []string).
capabilities := [][]string{append(deviceRequest.Capabilities, extraCapabilities...)}
spec := container.DeviceRequest{
Driver: deviceRequest.Driver,
Count: int(deviceRequest.Count),
DeviceIDs: deviceRequest.IDs,
Capabilities: capabilities,
Options: deviceRequest.Options,
}
requests = append(requests, spec)
}
return requests
}
func volumeSpecsFromCompose(
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
) ([]api.VolumeSpec, []api.VolumeMount, error) {