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

This commit is contained in:
Devon Steenberg
2026-05-06 15:16:41 +12:00
committed by GitHub
parent ff9c10f641
commit e7ec69708e
3 changed files with 259 additions and 5 deletions
+111 -1
View File
@@ -137,7 +137,6 @@ configs:
require.NoError(t, err)
require.False(t, containerExists(containerName))
}
func TestRun(t *testing.T) {
@@ -1132,6 +1131,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{
@@ -1436,6 +1452,100 @@ func Test_CredentialsStore_Behavior(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{}
}