mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
feat: add support for entrypoint, cpus, mem_limit, mem_reservation, priviliged, user to Compose deployment
This commit is contained in:
@@ -44,24 +44,41 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
spec := api.ServiceSpec{
|
spec := api.ServiceSpec{
|
||||||
Container: api.ContainerSpec{
|
Container: api.ContainerSpec{
|
||||||
Command: service.Command,
|
Command: service.Command,
|
||||||
|
Entrypoint: service.Entrypoint,
|
||||||
Env: env,
|
Env: env,
|
||||||
Image: service.Image,
|
Image: service.Image,
|
||||||
Init: service.Init,
|
Init: service.Init,
|
||||||
|
Privileged: service.Privileged,
|
||||||
PullPolicy: pullPolicy,
|
PullPolicy: pullPolicy,
|
||||||
|
Resources: resourcesFromCompose(service),
|
||||||
|
User: service.User,
|
||||||
},
|
},
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
// TODO: implement and map x-machines to Placement.
|
||||||
}
|
}
|
||||||
|
|
||||||
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
|
if ports, ok := service.Extensions[PortsExtensionKey].([]api.PortSpec); ok {
|
||||||
spec.Ports = ports
|
spec.Ports = ports
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map LogDriver if specified
|
||||||
|
if service.Logging != nil && service.Logging.Driver != "" {
|
||||||
|
spec.Container.LogDriver = &api.LogDriver{
|
||||||
|
Name: service.Logging.Driver,
|
||||||
|
Options: service.Logging.Options,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Scale != nil {
|
||||||
|
spec.Replicas = uint(*service.Scale)
|
||||||
|
}
|
||||||
|
|
||||||
if service.Deploy != nil {
|
if service.Deploy != nil {
|
||||||
switch service.Deploy.Mode {
|
switch service.Deploy.Mode {
|
||||||
case "global":
|
case "global":
|
||||||
spec.Mode = api.ServiceModeGlobal
|
spec.Mode = api.ServiceModeGlobal
|
||||||
case "", "replicated":
|
case "", "replicated":
|
||||||
spec.Mode = api.ServiceModeReplicated
|
|
||||||
if service.Deploy.Replicas != nil {
|
if service.Deploy.Replicas != nil {
|
||||||
spec.Replicas = uint(*service.Deploy.Replicas)
|
spec.Replicas = uint(*service.Deploy.Replicas)
|
||||||
}
|
}
|
||||||
@@ -82,6 +99,34 @@ func ServiceSpecFromCompose(project *types.Project, serviceName string) (api.Ser
|
|||||||
return spec, nil
|
return spec, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourcesFromCompose(service types.ServiceConfig) api.ContainerResources {
|
||||||
|
resources := api.ContainerResources{
|
||||||
|
CPU: int64(service.CPUS * 1e9),
|
||||||
|
Memory: int64(service.MemLimit),
|
||||||
|
MemoryReservation: int64(service.MemReservation),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map resources from deploy section if specified.
|
||||||
|
if service.Deploy != nil {
|
||||||
|
if service.Deploy.Resources.Limits != nil {
|
||||||
|
if service.Deploy.Resources.Limits.NanoCPUs > 0 {
|
||||||
|
// It seems Limits.NanoCPUs is actually not nano CPUs but a CPU fraction.
|
||||||
|
resources.CPU = int64(service.Deploy.Resources.Limits.NanoCPUs * 1e9)
|
||||||
|
}
|
||||||
|
if service.Deploy.Resources.Limits.MemoryBytes > 0 {
|
||||||
|
resources.Memory = int64(service.Deploy.Resources.Limits.MemoryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if service.Deploy.Resources.Reservations != nil {
|
||||||
|
if service.Deploy.Resources.Reservations.MemoryBytes > 0 {
|
||||||
|
resources.MemoryReservation = int64(service.Deploy.Resources.Reservations.MemoryBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
func volumeSpecsFromCompose(
|
func volumeSpecsFromCompose(
|
||||||
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
|
volumes types.Volumes, serviceVolumes []types.ServiceVolumeConfig,
|
||||||
) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
) ([]api.VolumeSpec, []api.VolumeMount, error) {
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/compose-spec/compose-go/v2/cli"
|
||||||
|
"github.com/compose-spec/compose-go/v2/types"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/go-units"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadProjectFromFile loads a compose project from a YAML file
|
||||||
|
func loadProjectFromFile(t *testing.T, filename string) *types.Project {
|
||||||
|
t.Helper()
|
||||||
|
ctx := context.Background()
|
||||||
|
path := filepath.Join("testdata", filename)
|
||||||
|
|
||||||
|
options, err := cli.NewProjectOptions(
|
||||||
|
[]string{path},
|
||||||
|
cli.WithName(FakeProjectName),
|
||||||
|
cli.WithOsEnv,
|
||||||
|
cli.WithDotEnv,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
project, err := options.LoadProject(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServiceSpecFromCompose(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
initTrue := true
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
want map[string]api.ServiceSpec
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "minimal",
|
||||||
|
filename: "compose-minimal.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"test": {
|
||||||
|
Name: "test",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deploy",
|
||||||
|
filename: "compose-deploy.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"no-deploy": {
|
||||||
|
Name: "no-deploy",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 1.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
Name: "deploy",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 1.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
"both": {
|
||||||
|
Name: "both",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Image: "nginx:latest",
|
||||||
|
PullPolicy: api.PullPolicyMissing,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 2 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "full-spec",
|
||||||
|
filename: "compose-full-spec.yaml",
|
||||||
|
want: map[string]api.ServiceSpec{
|
||||||
|
"test": {
|
||||||
|
Name: "test",
|
||||||
|
Mode: api.ServiceModeReplicated,
|
||||||
|
Container: api.ContainerSpec{
|
||||||
|
Command: []string{"nginx", "updated", "command"},
|
||||||
|
Entrypoint: []string{"/updated-docker-entrypoint.sh"},
|
||||||
|
Env: map[string]string{
|
||||||
|
"BOOL": "true",
|
||||||
|
"EMPTY": "",
|
||||||
|
"VAR": "value",
|
||||||
|
},
|
||||||
|
Image: "nginx:latest",
|
||||||
|
Init: &initTrue,
|
||||||
|
LogDriver: &api.LogDriver{
|
||||||
|
Name: "json-file",
|
||||||
|
Options: map[string]string{
|
||||||
|
"max-size": "10m",
|
||||||
|
"max-file": "3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Privileged: true,
|
||||||
|
PullPolicy: api.PullPolicyAlways,
|
||||||
|
Resources: api.ContainerResources{
|
||||||
|
CPU: 0.5 * api.Core,
|
||||||
|
Memory: 100 * units.MiB,
|
||||||
|
MemoryReservation: 50 * units.MiB,
|
||||||
|
},
|
||||||
|
User: "nginx:nginx",
|
||||||
|
VolumeMounts: []api.VolumeMount{
|
||||||
|
{
|
||||||
|
VolumeName: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
ContainerPath: "/host/etc/passwd",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data1",
|
||||||
|
ContainerPath: "/data1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data2-alias",
|
||||||
|
ContainerPath: "/data2/long/syntax",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "data-external",
|
||||||
|
ContainerPath: "/external",
|
||||||
|
ReadOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VolumeName: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
||||||
|
ContainerPath: "/tmpfs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Replicas: 3,
|
||||||
|
Volumes: []api.VolumeSpec{
|
||||||
|
{
|
||||||
|
Name: "bind-bb6aed1683cea1e0a1ae5cd227aacd0734f2f87f7a78fcf1baeff978ce300b90",
|
||||||
|
Type: api.VolumeTypeBind,
|
||||||
|
BindOptions: &api.BindOptions{
|
||||||
|
HostPath: "/etc/passwd",
|
||||||
|
CreateHostPath: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data-external",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data-external",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data1",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "data2-alias",
|
||||||
|
Type: api.VolumeTypeVolume,
|
||||||
|
VolumeOptions: &api.VolumeOptions{
|
||||||
|
Name: "data2",
|
||||||
|
Driver: &mount.Driver{
|
||||||
|
Name: "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "tmpfs-efa57ba8b6a1779674ac438de3af8729e2d55900b79eb929431cf9c5b0179542",
|
||||||
|
Type: api.VolumeTypeTmpfs,
|
||||||
|
TmpfsOptions: &mount.TmpfsOptions{
|
||||||
|
SizeBytes: 10 * units.MiB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, err := LoadProject(ctx, []string{filepath.Join("testdata", tt.filename)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for name, expectedSpec := range tt.want {
|
||||||
|
spec, err := ServiceSpecFromCompose(project, name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Due to the use of a map the order of volumes is non-deterministic.
|
||||||
|
slices.SortFunc(spec.Volumes, func(a, b api.VolumeSpec) int {
|
||||||
|
return strings.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.True(t, cmp.Equal(spec, expectedSpec, cmpopts.EquateEmpty()),
|
||||||
|
cmp.Diff(spec, expectedSpec, cmpopts.EquateEmpty()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
no-deploy:
|
||||||
|
image: nginx:latest
|
||||||
|
cpus: 1.5
|
||||||
|
mem_limit: 100M
|
||||||
|
mem_reservation: 50M
|
||||||
|
scale: 3
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
image: nginx:latest
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: 1.5
|
||||||
|
memory: 100M
|
||||||
|
reservations:
|
||||||
|
memory: 50M
|
||||||
|
|
||||||
|
both:
|
||||||
|
image: nginx:latest
|
||||||
|
cpus: 2
|
||||||
|
mem_limit: 102400K
|
||||||
|
mem_reservation: 52428800
|
||||||
|
scale: 3
|
||||||
|
deploy:
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: 2.0
|
||||||
|
memory: 100M
|
||||||
|
reservations:
|
||||||
|
memory: 50M
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
command: ["nginx", "updated", "command"]
|
||||||
|
cpus: 0.5
|
||||||
|
entrypoint: ["/updated-docker-entrypoint.sh"]
|
||||||
|
environment:
|
||||||
|
BOOL: "true"
|
||||||
|
EMPTY: ""
|
||||||
|
VAR: value
|
||||||
|
image: nginx:latest
|
||||||
|
init: true
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: 10m
|
||||||
|
max-file: 3
|
||||||
|
mem_limit: 100M
|
||||||
|
mem_reservation: 50M
|
||||||
|
privileged: true
|
||||||
|
pull_policy: always
|
||||||
|
scale: 3
|
||||||
|
user: nginx:nginx
|
||||||
|
volumes:
|
||||||
|
- /etc/passwd:/host/etc/passwd:ro
|
||||||
|
- data1:/data1
|
||||||
|
- type: volume
|
||||||
|
source: data2-alias
|
||||||
|
target: /data2/long/syntax
|
||||||
|
- data-external:/external:ro
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmpfs
|
||||||
|
tmpfs:
|
||||||
|
size: 10485760
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data1:
|
||||||
|
data2-alias:
|
||||||
|
name: data2
|
||||||
|
driver: local
|
||||||
|
data-external:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
services:
|
||||||
|
test:
|
||||||
|
image: nginx:latest
|
||||||
@@ -1311,7 +1311,6 @@ func TestEvalContainerSpecChange_Mixed(t *testing.T) {
|
|||||||
new api.ServiceSpec
|
new api.ServiceSpec
|
||||||
want ContainerSpecStatus
|
want ContainerSpecStatus
|
||||||
}{
|
}{
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "mutable changes",
|
name: "mutable changes",
|
||||||
current: api.ServiceSpec{
|
current: api.ServiceSpec{
|
||||||
|
|||||||
Reference in New Issue
Block a user