mirror of
https://github.com/psviderski/uncloud.git
synced 2026-11-10 20:54:17 +00:00
feat(labels): support service and container labels, update labels and cpu/memory in-place
This commit is contained in:
+4
-1
@@ -39,7 +39,7 @@ func assertServiceMatchesSpec(t *testing.T, svc api.Service, spec api.ServiceSpe
|
||||
|
||||
func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api.ServiceSpec) {
|
||||
desiredSpec := spec.SetDefaults()
|
||||
status := deploy.EvalContainerSpecChange(ctr.ServiceSpec, desiredSpec)
|
||||
status := deploy.EvalContainerChange(ctr, desiredSpec)
|
||||
assert.Equal(t, deploy.ContainerUpToDate, status)
|
||||
spec = renderRuntimeTemplatesForContainer(t, desiredSpec, ctr.Name)
|
||||
|
||||
@@ -48,6 +48,9 @@ func assertContainerMatchesSpec(t *testing.T, ctr api.ServiceContainer, spec api
|
||||
assert.Equal(t, spec.Name, ctr.Config.Labels[api.LabelServiceName])
|
||||
assert.Equal(t, spec.Mode, ctr.Config.Labels[api.LabelServiceMode])
|
||||
assert.Contains(t, ctr.Config.Labels, api.LabelManaged)
|
||||
for k, v := range spec.Container.Labels {
|
||||
assert.Equal(t, v, ctr.Config.Labels[k], "Expected container label '%s'", k)
|
||||
}
|
||||
|
||||
// Command and Entrypoint can only be compared if they are set in the spec.
|
||||
// Otherwise, the container takes them from the image.
|
||||
|
||||
@@ -2,7 +2,9 @@ package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
"github.com/psviderski/uncloud/pkg/api"
|
||||
"github.com/psviderski/uncloud/pkg/client/compose"
|
||||
"github.com/psviderski/uncloud/pkg/client/deploy"
|
||||
"github.com/psviderski/uncloud/pkg/client/operation"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -548,6 +551,85 @@ func TestComposeDeployment(t *testing.T) {
|
||||
"Service containers should be on machines 1 and 3 from comma-separated list")
|
||||
})
|
||||
|
||||
t.Run("labels and in-place update of service labels and resources", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
name := "test-compose-labels"
|
||||
t.Cleanup(func() {
|
||||
removeServices(t, cli, name)
|
||||
})
|
||||
|
||||
composeYAML := func(tier, memLimit string) string {
|
||||
return fmt.Sprintf(`
|
||||
services:
|
||||
%s:
|
||||
image: portainer/pause:3.9
|
||||
mem_limit: %s
|
||||
labels:
|
||||
com.example.team: web
|
||||
deploy:
|
||||
replicas: 2
|
||||
labels:
|
||||
tier: %s
|
||||
`, name, memLimit, tier)
|
||||
}
|
||||
|
||||
project, err := compose.LoadProjectFromContent(ctx, composeYAML("frontend", "64m"))
|
||||
require.NoError(t, err)
|
||||
deployment, err := compose.NewDeployment(ctx, cli, project)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, deployment.Run(ctx))
|
||||
|
||||
svc, err := cli.InspectService(ctx, name)
|
||||
require.NoError(t, err)
|
||||
expectedSpec := api.ServiceSpec{
|
||||
Name: name,
|
||||
Mode: api.ServiceModeReplicated,
|
||||
Container: api.ContainerSpec{
|
||||
Image: "portainer/pause:3.9",
|
||||
Labels: map[string]string{"com.example.team": "web"},
|
||||
Resources: api.ContainerResources{Memory: 64 * 1024 * 1024},
|
||||
},
|
||||
Labels: map[string]string{"tier": "frontend"},
|
||||
Replicas: 2,
|
||||
}
|
||||
assertServiceMatchesSpec(t, svc, expectedSpec)
|
||||
for _, mc := range svc.Containers {
|
||||
assert.NotContains(t, mc.Container.Config.Labels, "tier",
|
||||
"Service labels must not be applied to containers")
|
||||
}
|
||||
containers := serviceContainerIDs(svc)
|
||||
|
||||
// Changing the service labels and memory limit should update the containers in place.
|
||||
project, err = compose.LoadProjectFromContent(ctx, composeYAML("backend", "128m"))
|
||||
require.NoError(t, err)
|
||||
deployment, err = compose.NewDeployment(ctx, cli, project)
|
||||
require.NoError(t, err)
|
||||
|
||||
plan, err := deployment.Plan(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, plan.Services, 1)
|
||||
require.Len(t, plan.Services[0].Operations, 2)
|
||||
for _, op := range plan.Services[0].Operations {
|
||||
assert.IsType(t, &operation.UpdateContainerOperation{}, op)
|
||||
}
|
||||
require.NoError(t, deployment.Run(ctx))
|
||||
|
||||
// The updated spec propagates to the cluster store asynchronously.
|
||||
expectedSpec.Labels = map[string]string{"tier": "backend"}
|
||||
expectedSpec.Container.Resources.Memory = 128 * 1024 * 1024
|
||||
assert.EventuallyWithT(t, func(c *assert.CollectT) {
|
||||
svc, err = cli.InspectService(ctx, name)
|
||||
require.NoError(c, err)
|
||||
for _, mc := range svc.Containers {
|
||||
assert.Equal(c, expectedSpec.Labels, mc.Container.ServiceSpec.Labels)
|
||||
assert.Equal(c, expectedSpec.Container.Resources.Memory, mc.Container.HostConfig.Memory)
|
||||
}
|
||||
}, 10*time.Second, 200*time.Millisecond)
|
||||
assertServiceMatchesSpec(t, svc, expectedSpec)
|
||||
assert.Equal(t, containers, serviceContainerIDs(svc), "Expected containers to be updated in place")
|
||||
})
|
||||
|
||||
// Catches regression: https://github.com/psviderski/uncloud/issues/176
|
||||
t.Run("plan new deployment with volumes and recreate strategy", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1761,6 +1761,90 @@ func TestServiceLifecycle(t *testing.T) {
|
||||
require.ErrorIs(t, err, api.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("update container in place", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
serviceID := newServiceID()
|
||||
spec := api.ServiceSpec{
|
||||
Name: "container-update",
|
||||
Container: api.ContainerSpec{
|
||||
Image: "portainer/pause:latest",
|
||||
Resources: api.ContainerResources{
|
||||
CPU: 100 * api.MilliCore,
|
||||
Memory: 20 * units.MiB,
|
||||
},
|
||||
},
|
||||
Labels: map[string]string{"tier": "frontend"},
|
||||
}
|
||||
|
||||
ctr, err := cli.CreateContainer(ctx, serviceID, spec, c.Machines[0].Name)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{Force: true})
|
||||
if !errors.Is(err, api.ErrNotFound) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
require.NoError(t, cli.StartContainer(ctx, serviceID, ctr.ID))
|
||||
|
||||
// Update the CPU and memory limits and replace the service labels.
|
||||
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
|
||||
CPU: new(int64(200 * api.MilliCore)),
|
||||
Memory: new(int64(40 * units.MiB)),
|
||||
MemoryReservation: new(int64(10 * units.MiB)),
|
||||
ServiceLabels: &map[string]string{"tier": "backend"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
spec.Container.Resources.CPU = 200 * api.MilliCore
|
||||
spec.Container.Resources.Memory = 40 * units.MiB
|
||||
spec.Container.Resources.MemoryReservation = 10 * units.MiB
|
||||
spec.Labels = map[string]string{"tier": "backend"}
|
||||
|
||||
mc, err := cli.InspectContainer(ctx, serviceID, ctr.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ctr.ID, mc.Container.ID, "Expected container to be updated in place")
|
||||
assert.True(t, mc.Container.State.Running, "Expected container to keep running")
|
||||
assertContainerMatchesSpec(t, mc.Container, spec)
|
||||
assert.Equal(t, spec.Labels, mc.Container.ServiceSpec.Labels)
|
||||
assert.EqualValues(t, 80*units.MiB, mc.Container.HostConfig.MemorySwap,
|
||||
"Expected swap limit to be twice the memory limit")
|
||||
|
||||
// Remove all service labels and leave the limits unchanged.
|
||||
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
|
||||
ServiceLabels: &map[string]string{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
spec.Labels = nil
|
||||
|
||||
mc, err = cli.InspectContainer(ctx, serviceID, ctr.ID)
|
||||
require.NoError(t, err)
|
||||
assertContainerMatchesSpec(t, mc.Container, spec)
|
||||
assert.Nil(t, mc.Container.ServiceSpec.Labels)
|
||||
|
||||
// A limit can't be removed without recreating the container.
|
||||
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{Memory: new(int64(0))})
|
||||
require.ErrorContains(t, err, "memory limit can't be removed")
|
||||
|
||||
mc, err = cli.InspectContainer(ctx, serviceID, ctr.ID)
|
||||
require.NoError(t, err)
|
||||
assertContainerMatchesSpec(t, mc.Container, spec)
|
||||
|
||||
// Reserved service labels are rejected.
|
||||
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
|
||||
ServiceLabels: &map[string]string{api.LabelManaged: ""},
|
||||
})
|
||||
require.ErrorContains(t, err, "reserved prefix")
|
||||
|
||||
require.NoError(t, cli.RemoveContainer(ctx, serviceID, ctr.ID, container.RemoveOptions{Force: true}))
|
||||
err = cli.UpdateContainer(ctx, serviceID, ctr.ID, api.UpdateContainerOptions{
|
||||
ServiceLabels: &map[string]string{"tier": "frontend"},
|
||||
})
|
||||
require.ErrorIs(t, err, api.ErrNotFound)
|
||||
})
|
||||
|
||||
t.Run("1 replica", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user