fix(libstack): pull images sequentially and respect COMPOSE_PARALLEL_LIMIT [BE-12930] (#2554)

This commit is contained in:
Devon Steenberg
2026-05-06 15:15:27 +12:00
committed by GitHub
parent d40462b756
commit 2029799c73
3 changed files with 269 additions and 5 deletions
+112 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/compose-spec/compose-go/v2/consts"
"github.com/compose-spec/compose-go/v2/types"
"github.com/docker/cli/cli/command"
configtypes "github.com/docker/cli/cli/config/types"
cmdcompose "github.com/docker/compose/v2/cmd/compose"
"github.com/docker/compose/v2/pkg/api"
"github.com/docker/compose/v2/pkg/compose"
@@ -136,7 +137,6 @@ configs:
require.NoError(t, err)
require.False(t, containerExists(containerName))
}
func TestRun(t *testing.T) {
@@ -1125,6 +1125,23 @@ func Test_createProject(t *testing.T) {
},
expectedProject: expectedSimpleComposeProject("", map[string]string{"PORTAINER_WEB_FOLDER": "html-1"}),
},
{
name: "OS COMPOSE_ vars are passed to project",
filesToCreate: map[string]string{
"docker-compose.yml": testSimpleComposeConfig,
},
configFilepaths: []string{dir + "/docker-compose.yml"},
options: libstack.Options{
ProjectName: projectName,
},
osEnv: map[string]string{
"COMPOSE_PARALLEL_LIMIT": "4",
"other_var": "something",
},
expectedProject: expectedSimpleComposeProject("", map[string]string{
"COMPOSE_PARALLEL_LIMIT": "4",
}),
},
{
name: "Env Vars in compose file, compose env file, env, os, and env_file",
filesToCreate: map[string]string{
@@ -1358,6 +1375,100 @@ func Test_createProject(t *testing.T) {
}
}
func TestGetImageNameOrDefault(t *testing.T) {
testCases := []struct {
name string
service types.ServiceConfig
projectName string
expectedName string
}{
{
name: "service with explicit image",
service: types.ServiceConfig{Name: "web", Image: "nginx:latest"},
projectName: "myproject",
expectedName: "nginx:latest",
},
{
name: "service without image uses default",
service: types.ServiceConfig{Name: "web", Image: ""},
projectName: "myproject",
expectedName: "myproject-web",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotName := getImageNameOrDefault(tc.service, tc.projectName)
require.Equal(t, tc.expectedName, gotName)
})
}
}
func TestEncodeRegistryAuth(t *testing.T) {
testCases := []struct {
name string
image string
registries []configtypes.AuthConfig
expectRegistryAuth string
expectError string
}{
{
name: "matching registry returns encoded auth",
image: "myregistry.example.com/myimage:latest",
registries: []configtypes.AuthConfig{
{ServerAddress: "myregistry.example.com", Username: "user", Password: "pass"},
},
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MiLCJzZXJ2ZXJhZGRyZXNzIjoibXlyZWdpc3RyeS5leGFtcGxlLmNvbSJ9",
},
{
name: "unknown registry returns empty string",
image: "myregistry.example.com/myimage:latest",
registries: []configtypes.AuthConfig{
{ServerAddress: "other.registry.com", Username: "user", Password: "pass"},
},
expectRegistryAuth: "",
},
{
name: "docker.io image matches index server",
image: "alpine:latest",
registries: []configtypes.AuthConfig{
{ServerAddress: "https://index.docker.io/v1/", Username: "user", Password: "pass"},
},
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MiLCJzZXJ2ZXJhZGRyZXNzIjoiaHR0cHM6Ly9pbmRleC5kb2NrZXIuaW8vdjEvIn0=",
},
{
name: "invalid image reference returns error",
image: "INVALID::IMAGE",
registries: []configtypes.AuthConfig{},
expectRegistryAuth: "",
expectError: `failed to parse image reference "INVALID::IMAGE": invalid reference format: repository name (library/INVALID) must be lowercase`,
},
{
name: "multiple registries only matching one used",
image: "registry-b.example.com/image:latest",
registries: []configtypes.AuthConfig{
{ServerAddress: "registry-a.example.com", Username: "user-a", Password: "pass-a"},
{ServerAddress: "registry-b.example.com", Username: "user-b", Password: "pass-b"},
},
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXItYiIsInBhc3N3b3JkIjoicGFzcy1iIiwic2VydmVyYWRkcmVzcyI6InJlZ2lzdHJ5LWIuZXhhbXBsZS5jb20ifQ==",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gotRegistryAuth, err := encodeRegistryAuth(tc.image, tc.registries)
if tc.expectError == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.expectError)
}
require.Equal(t, tc.expectRegistryAuth, gotRegistryAuth)
})
}
}
func createMockComposeService(command.Cli, ...compose.Option) api.Compose {
return &mockComposeService{}
}