feat: add support for ulimits in compose and container creation (#221) (#244)

* feat: add support for ulimits in compose and container creation ([#221](https://github.com/ipaddicting/uncloud/issues/221))
* refactor: move Ulimits to ContainerResources and use map, also removed GEMINI.md.
This commit is contained in:
Nick
2026-01-20 16:17:54 +10:00
committed by GitHub
parent 1caa5ccf0e
commit 3bd9400051
6 changed files with 146 additions and 0 deletions
+1
View File
@@ -39,3 +39,4 @@ node_modules/
# VS Code
.vscode
.devcontainer/
+19
View File
@@ -33,6 +33,7 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
"github.com/docker/go-units"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
@@ -617,6 +618,7 @@ func (s *Server) CreateServiceContainer(
Memory: spec.Container.Resources.Memory,
MemoryReservation: spec.Container.Resources.MemoryReservation,
DeviceRequests: spec.Container.Resources.DeviceReservations,
Ulimits: toDockerUlimits(spec.Container.Resources.Ulimits),
},
// Restart service containers if they exit or a machine restarts unless they are explicitly stopped.
// For one-off containers and batch jobs we plan to use a different service type/mode.
@@ -877,6 +879,23 @@ func toDockerBindOptions(opts *api.BindOptions) *mount.BindOptions {
return dockerOpts
}
func toDockerUlimits(ulimits map[string]api.Ulimit) []*units.Ulimit {
if len(ulimits) == 0 {
return nil
}
dockerUlimits := make([]*units.Ulimit, 0, len(ulimits))
for name, u := range ulimits {
dockerUlimits = append(dockerUlimits, &units.Ulimit{
Name: name,
Soft: u.Soft,
Hard: u.Hard,
})
}
return dockerUlimits
}
// verifyDockerVolumesExist checks if the Docker named volumes referenced in the mounts exist on the machine.
func (s *Server) verifyDockerVolumesExist(ctx context.Context, mounts []mount.Mount) error {
for _, m := range mounts {
+7
View File
@@ -19,4 +19,11 @@ type ContainerResources struct {
MemoryReservation int64
// Device reservations/requests for access to things like GPUs
DeviceReservations []container.DeviceRequest
// Ulimits defines the resource limits for the container.
Ulimits map[string]Ulimit
}
type Ulimit struct {
Soft int64
Hard int64
}
+3
View File
@@ -363,6 +363,9 @@ func (s *ContainerSpec) Clone() ContainerSpec {
spec.Sysctls[k] = v
}
}
if s.Resources.Ulimits != nil {
spec.Resources.Ulimits = maps.Clone(s.Resources.Ulimits)
}
return spec
}
+28
View File
@@ -125,6 +125,7 @@ func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
CPU: int64(service.CPUS * 1e9),
Memory: int64(service.MemLimit),
MemoryReservation: int64(service.MemReservation),
Ulimits: ulimitsFromCompose(service.Ulimits),
}
// Convert GPU device requests from compose format, appending "gpu" capability.
@@ -293,6 +294,33 @@ func tmpfsVolumeSpecFromCompose(serviceVolume types.ServiceVolumeConfig) api.Vol
return spec
}
func ulimitsFromCompose(ulimits map[string]*types.UlimitsConfig) map[string]api.Ulimit {
if len(ulimits) == 0 {
return nil
}
res := make(map[string]api.Ulimit, len(ulimits))
for name, u := range ulimits {
soft := u.Soft
hard := u.Hard
if u.Single != 0 {
if soft == 0 {
soft = u.Single
}
if hard == 0 {
hard = u.Single
}
}
res[name] = api.Ulimit{
Soft: int64(soft),
Hard: int64(hard),
}
}
return res
}
// validateServicesExtensions validates extension combinations across all services in the project.
func validateServicesExtensions(project *types.Project) error {
for _, service := range project.Services {
+88
View File
@@ -699,6 +699,94 @@ volumes:
}
}
func TestServiceSpecFromCompose_Ulimits(t *testing.T) {
tests := []struct {
name string
composeYAML string
expected map[string]api.Ulimit
}{
{
name: "single ulimit with soft and hard limits",
composeYAML: `
services:
db:
image: postgres
ulimits:
nofile:
soft: 20000
hard: 40000
`,
expected: map[string]api.Ulimit{
"nofile": {
Soft: 20000,
Hard: 40000,
},
},
},
{
name: "single ulimit with single value (soft=hard)",
composeYAML: `
services:
db:
image: postgres
ulimits:
nproc: 65535
`,
expected: map[string]api.Ulimit{
"nproc": {
Soft: 65535,
Hard: 65535,
},
},
},
{
name: "multiple ulimits",
composeYAML: `
services:
db:
image: postgres
ulimits:
nofile:
soft: 20000
hard: 40000
nproc: 65535
`,
expected: map[string]api.Ulimit{
"nofile": {
Soft: 20000,
Hard: 40000,
},
"nproc": {
Soft: 65535,
Hard: 65535,
},
},
},
{
name: "empty ulimits",
composeYAML: `
services:
db:
image: postgres
ulimits: {}
`,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
project, err := LoadProjectFromContent(context.Background(), tt.composeYAML)
require.NoError(t, err)
spec, err := ServiceSpecFromCompose(project, "db")
require.NoError(t, err)
assert.Equal(t, tt.expected, spec.Container.Resources.Ulimits)
})
}
}
func TestServiceSpecFromCompose_XMachinesPlacement(t *testing.T) {
tests := []struct {
name string