2024-11-11 19:05:56 -03:00
|
|
|
package compose
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2025-03-25 08:57:23 +13:00
|
|
|
"errors"
|
2023-06-26 08:11:05 +07:00
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2025-03-25 08:57:23 +13:00
|
|
|
"strconv"
|
2023-06-26 08:11:05 +07:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2025-03-25 08:57:23 +13:00
|
|
|
"github.com/compose-spec/compose-go/v2/consts"
|
|
|
|
"github.com/compose-spec/compose-go/v2/types"
|
|
|
|
"github.com/docker/cli/cli/command"
|
|
|
|
cmdcompose "github.com/docker/compose/v2/cmd/compose"
|
|
|
|
"github.com/docker/compose/v2/pkg/api"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
2023-06-26 08:11:05 +07:00
|
|
|
"github.com/portainer/portainer/pkg/libstack"
|
2025-03-25 08:57:23 +13:00
|
|
|
zerolog "github.com/rs/zerolog/log"
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
func Test_UpAndDown(t *testing.T) {
|
2024-11-11 19:05:56 -03:00
|
|
|
const projectName = "composetest"
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
const composeFileContent = `version: "3.9"
|
|
|
|
services:
|
|
|
|
busybox:
|
|
|
|
image: "alpine:3.7"
|
2024-11-11 19:05:56 -03:00
|
|
|
container_name: "composetest_container_one"`
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
const overrideComposeFileContent = `version: "3.9"
|
|
|
|
services:
|
|
|
|
busybox:
|
|
|
|
image: "alpine:latest"
|
2024-11-11 19:05:56 -03:00
|
|
|
container_name: "composetest_container_two"`
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
composeContainerName := projectName + "_container_two"
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
w := NewComposeDeployer()
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
filePathOriginal := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
|
|
|
filePathOverride := createFile(t, dir, "docker-compose-override.yml", overrideComposeFileContent)
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
filePaths := []string{filePathOriginal, filePathOverride}
|
2024-01-02 10:59:49 +07:00
|
|
|
|
2023-06-26 08:11:05 +07:00
|
|
|
ctx := context.Background()
|
2024-11-11 19:05:56 -03:00
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
err := w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
2024-11-11 19:05:56 -03:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = w.Pull(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(composeContainerName))
|
|
|
|
|
|
|
|
err = w.Deploy(ctx, filePaths, libstack.DeployOptions{
|
2024-01-02 10:59:49 +07:00
|
|
|
Options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
})
|
2024-11-11 19:05:56 -03:00
|
|
|
require.NoError(t, err)
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
require.True(t, containerExists(composeContainerName))
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-12-17 16:25:49 -03:00
|
|
|
waitResult := w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
2023-06-26 08:11:05 +07:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
require.Empty(t, waitResult.ErrorMsg)
|
|
|
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
|
|
|
|
|
|
|
err = w.Remove(ctx, projectName, filePaths, libstack.RemoveOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(composeContainerName))
|
2023-06-26 08:11:05 +07:00
|
|
|
}
|
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
func TestRun(t *testing.T) {
|
|
|
|
w := NewComposeDeployer()
|
|
|
|
|
|
|
|
filePath := createFile(t, t.TempDir(), "docker-compose.yml", `
|
|
|
|
services:
|
|
|
|
updater:
|
|
|
|
image: alpine
|
|
|
|
`)
|
|
|
|
|
|
|
|
filePaths := []string{filePath}
|
|
|
|
serviceName := "updater"
|
|
|
|
|
|
|
|
err := w.Run(context.Background(), filePaths, serviceName, libstack.RunOptions{
|
2024-11-13 20:24:20 -03:00
|
|
|
Remove: true,
|
2024-11-13 14:38:53 -03:00
|
|
|
Options: libstack.Options{
|
|
|
|
ProjectName: "project_name",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func createFile(t *testing.T, dir, fileName, content string) string {
|
2023-06-26 08:11:05 +07:00
|
|
|
filePath := filepath.Join(dir, fileName)
|
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
err := os.WriteFile(filePath, []byte(content), 0o644)
|
|
|
|
require.NoError(t, err)
|
2023-07-13 23:55:52 +03:00
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
return filePath
|
2023-06-26 08:11:05 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
func containerExists(containerName string) bool {
|
2024-10-10 12:06:20 -03:00
|
|
|
cmd := exec.Command("docker", "ps", "-a", "-f", "name="+containerName)
|
2023-06-26 08:11:05 +07:00
|
|
|
|
|
|
|
out, err := cmd.Output()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("failed to list containers: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.Contains(string(out), containerName)
|
|
|
|
}
|
2024-09-06 08:43:12 +12:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
func Test_Validate(t *testing.T) {
|
|
|
|
invalidComposeFileContent := `invalid-file-content`
|
|
|
|
|
|
|
|
w := NewComposeDeployer()
|
2024-09-06 08:43:12 +12:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
dir := t.TempDir()
|
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
filePathOriginal := createFile(t, dir, "docker-compose.yml", invalidComposeFileContent)
|
2024-11-11 19:05:56 -03:00
|
|
|
|
|
|
|
filePaths := []string{filePathOriginal}
|
|
|
|
|
|
|
|
projectName := "plugintest"
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
2024-11-13 14:38:53 -03:00
|
|
|
err := w.Validate(ctx, filePaths, libstack.Options{ProjectName: projectName})
|
2024-11-11 19:05:56 -03:00
|
|
|
require.Error(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func Test_Config(t *testing.T) {
|
2024-09-06 08:43:12 +12:00
|
|
|
ctx := context.Background()
|
|
|
|
dir := t.TempDir()
|
|
|
|
projectName := "configtest"
|
|
|
|
|
|
|
|
defer os.RemoveAll(dir)
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
composeFileContent string
|
|
|
|
expectFileContent string
|
|
|
|
envFileContent string
|
2024-11-26 17:37:22 -03:00
|
|
|
env []string
|
2024-09-06 08:43:12 +12:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "compose file with relative path",
|
|
|
|
composeFileContent: `services:
|
|
|
|
app:
|
|
|
|
image: 'nginx:latest'
|
|
|
|
ports:
|
|
|
|
- '80:80'
|
|
|
|
volumes:
|
|
|
|
- ./nginx-data:/data`,
|
|
|
|
expectFileContent: `name: configtest
|
|
|
|
services:
|
|
|
|
app:
|
|
|
|
image: nginx:latest
|
|
|
|
networks:
|
|
|
|
default: null
|
|
|
|
ports:
|
|
|
|
- mode: ingress
|
|
|
|
target: 80
|
|
|
|
published: "80"
|
|
|
|
protocol: tcp
|
|
|
|
volumes:
|
|
|
|
- type: bind
|
|
|
|
source: ./nginx-data
|
|
|
|
target: /data
|
|
|
|
bind:
|
|
|
|
create_host_path: true
|
|
|
|
networks:
|
|
|
|
default:
|
|
|
|
name: configtest_default
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose file with absolute path",
|
|
|
|
composeFileContent: `services:
|
|
|
|
app:
|
|
|
|
image: 'nginx:latest'
|
|
|
|
ports:
|
|
|
|
- '80:80'
|
|
|
|
volumes:
|
|
|
|
- /nginx-data:/data`,
|
|
|
|
expectFileContent: `name: configtest
|
|
|
|
services:
|
|
|
|
app:
|
|
|
|
image: nginx:latest
|
|
|
|
networks:
|
|
|
|
default: null
|
|
|
|
ports:
|
|
|
|
- mode: ingress
|
|
|
|
target: 80
|
|
|
|
published: "80"
|
|
|
|
protocol: tcp
|
|
|
|
volumes:
|
|
|
|
- type: bind
|
|
|
|
source: /nginx-data
|
|
|
|
target: /data
|
|
|
|
bind:
|
|
|
|
create_host_path: true
|
|
|
|
networks:
|
|
|
|
default:
|
|
|
|
name: configtest_default
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose file with declared volume",
|
|
|
|
composeFileContent: `services:
|
|
|
|
app:
|
|
|
|
image: 'nginx:latest'
|
|
|
|
ports:
|
|
|
|
- '80:80'
|
|
|
|
volumes:
|
|
|
|
- nginx-data:/data
|
|
|
|
volumes:
|
|
|
|
nginx-data:
|
|
|
|
driver: local`,
|
|
|
|
expectFileContent: `name: configtest
|
|
|
|
services:
|
|
|
|
app:
|
|
|
|
image: nginx:latest
|
|
|
|
networks:
|
|
|
|
default: null
|
|
|
|
ports:
|
|
|
|
- mode: ingress
|
|
|
|
target: 80
|
|
|
|
published: "80"
|
|
|
|
protocol: tcp
|
|
|
|
volumes:
|
|
|
|
- type: volume
|
|
|
|
source: nginx-data
|
|
|
|
target: /data
|
|
|
|
volume: {}
|
|
|
|
networks:
|
|
|
|
default:
|
|
|
|
name: configtest_default
|
|
|
|
volumes:
|
|
|
|
nginx-data:
|
|
|
|
name: configtest_nginx-data
|
|
|
|
driver: local
|
|
|
|
`,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose file with relative path environment variable placeholder",
|
|
|
|
composeFileContent: `services:
|
|
|
|
nginx:
|
|
|
|
image: nginx:latest
|
|
|
|
ports:
|
|
|
|
- 8019:80
|
|
|
|
volumes:
|
|
|
|
- ${WEB_HOME}:/usr/share/nginx/html/
|
2024-11-26 17:37:22 -03:00
|
|
|
- ./config/${CONFIG_DIR}:/tmp/config
|
2024-09-06 08:43:12 +12:00
|
|
|
env_file:
|
|
|
|
- stack.env
|
|
|
|
`,
|
|
|
|
expectFileContent: `name: configtest
|
|
|
|
services:
|
|
|
|
nginx:
|
|
|
|
environment:
|
|
|
|
WEB_HOME: ./html
|
|
|
|
image: nginx:latest
|
|
|
|
networks:
|
|
|
|
default: null
|
|
|
|
ports:
|
|
|
|
- mode: ingress
|
|
|
|
target: 80
|
|
|
|
published: "8019"
|
|
|
|
protocol: tcp
|
|
|
|
volumes:
|
|
|
|
- type: bind
|
|
|
|
source: ./html
|
|
|
|
target: /usr/share/nginx/html
|
|
|
|
bind:
|
|
|
|
create_host_path: true
|
2024-11-26 17:37:22 -03:00
|
|
|
- type: bind
|
|
|
|
source: ./config/something
|
|
|
|
target: /tmp/config
|
|
|
|
bind:
|
|
|
|
create_host_path: true
|
2024-09-06 08:43:12 +12:00
|
|
|
networks:
|
|
|
|
default:
|
|
|
|
name: configtest_default
|
|
|
|
`,
|
|
|
|
envFileContent: `WEB_HOME=./html`,
|
2024-11-26 17:37:22 -03:00
|
|
|
env: []string{"CONFIG_DIR=something"},
|
2024-09-06 08:43:12 +12:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose file with absolute path environment variable placeholder",
|
|
|
|
composeFileContent: `services:
|
|
|
|
nginx:
|
|
|
|
image: nginx:latest
|
|
|
|
ports:
|
|
|
|
- 8019:80
|
|
|
|
volumes:
|
|
|
|
- ${WEB_HOME}:/usr/share/nginx/html/
|
|
|
|
env_file:
|
|
|
|
- stack.env
|
|
|
|
`,
|
|
|
|
expectFileContent: `name: configtest
|
|
|
|
services:
|
|
|
|
nginx:
|
|
|
|
environment:
|
|
|
|
WEB_HOME: /usr/share/nginx/html
|
|
|
|
image: nginx:latest
|
|
|
|
networks:
|
|
|
|
default: null
|
|
|
|
ports:
|
|
|
|
- mode: ingress
|
|
|
|
target: 80
|
|
|
|
published: "8019"
|
|
|
|
protocol: tcp
|
|
|
|
volumes:
|
|
|
|
- type: bind
|
|
|
|
source: /usr/share/nginx/html
|
|
|
|
target: /usr/share/nginx/html
|
|
|
|
bind:
|
|
|
|
create_host_path: true
|
|
|
|
networks:
|
|
|
|
default:
|
|
|
|
name: configtest_default
|
|
|
|
`,
|
|
|
|
envFileContent: `WEB_HOME=/usr/share/nginx/html`,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
2024-11-13 14:38:53 -03:00
|
|
|
composeFilePath := createFile(t, dir, "docker-compose.yml", tc.composeFileContent)
|
2024-09-06 08:43:12 +12:00
|
|
|
|
|
|
|
envFilePath := ""
|
|
|
|
if tc.envFileContent != "" {
|
2024-11-13 14:38:53 -03:00
|
|
|
envFilePath = createFile(t, dir, "stack.env", tc.envFileContent)
|
2024-09-06 08:43:12 +12:00
|
|
|
}
|
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
w := NewComposeDeployer()
|
2024-09-06 08:43:12 +12:00
|
|
|
actual, err := w.Config(ctx, []string{composeFilePath}, libstack.Options{
|
|
|
|
WorkingDir: dir,
|
|
|
|
ProjectName: projectName,
|
|
|
|
EnvFilePath: envFilePath,
|
2024-11-26 17:37:22 -03:00
|
|
|
Env: tc.env,
|
2024-09-06 08:43:12 +12:00
|
|
|
ConfigOptions: []string{"--no-path-resolution"},
|
|
|
|
})
|
2024-11-11 19:05:56 -03:00
|
|
|
require.NoError(t, err)
|
2024-09-06 08:43:12 +12:00
|
|
|
|
2024-11-11 19:05:56 -03:00
|
|
|
require.Equal(t, tc.expectFileContent, string(actual))
|
2024-09-06 08:43:12 +12:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2025-03-25 08:57:23 +13:00
|
|
|
|
|
|
|
func Test_DeployWithRemoveOrphans(t *testing.T) {
|
|
|
|
const projectName = "compose_remove_orphans_test"
|
|
|
|
|
|
|
|
const composeFileContent = `services:
|
|
|
|
service-1:
|
|
|
|
image: alpine:latest
|
|
|
|
service-2:
|
|
|
|
image: alpine:latest`
|
|
|
|
|
|
|
|
const modifiedFileContent = `services:
|
|
|
|
service-2:
|
|
|
|
image: alpine:latest`
|
|
|
|
|
|
|
|
service1ContainerName := projectName + "-service-1"
|
|
|
|
service2ContainerName := projectName + "-service-2"
|
|
|
|
|
|
|
|
w := NewComposeDeployer()
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
composeFilepath := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
|
|
|
modifiedComposeFilepath := createFile(t, dir, "docker-compose-modified.yml", modifiedFileContent)
|
|
|
|
|
|
|
|
filepaths := []string{composeFilepath}
|
|
|
|
modifiedFilepaths := []string{modifiedComposeFilepath}
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
testCases := []struct {
|
|
|
|
name string
|
|
|
|
options libstack.DeployOptions
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Remove Orphans in env",
|
|
|
|
options: libstack.DeployOptions{
|
|
|
|
Options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{cmdcompose.ComposeRemoveOrphans + "=true"},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Remove Orphans in options",
|
|
|
|
options: libstack.DeployOptions{
|
|
|
|
Options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
RemoveOrphans: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Remove Orphans in options and env",
|
|
|
|
options: libstack.DeployOptions{
|
|
|
|
Options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{cmdcompose.ComposeRemoveOrphans + "=true"},
|
|
|
|
},
|
|
|
|
RemoveOrphans: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testCases {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
options := tc.options.Options
|
|
|
|
|
|
|
|
err := w.Validate(ctx, filepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = w.Pull(ctx, filepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(service1ContainerName))
|
|
|
|
require.False(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
err = w.Deploy(ctx, filepaths, tc.options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
err = w.Remove(ctx, projectName, filepaths, libstack.RemoveOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(service1ContainerName))
|
|
|
|
require.False(t, containerExists(service2ContainerName))
|
|
|
|
}()
|
|
|
|
|
|
|
|
require.True(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
waitResult := w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
|
|
|
|
|
|
|
require.Empty(t, waitResult.ErrorMsg)
|
|
|
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
|
|
|
|
|
|
|
err = w.Validate(ctx, modifiedFilepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = w.Pull(ctx, modifiedFilepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.True(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
err = w.Deploy(ctx, modifiedFilepaths, tc.options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
waitResult = w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
|
|
|
|
|
|
|
require.Empty(t, waitResult.ErrorMsg)
|
|
|
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func Test_DeployWithIgnoreOrphans(t *testing.T) {
|
|
|
|
var logOutput strings.Builder
|
|
|
|
oldLogger := zerolog.Logger
|
|
|
|
zerolog.Logger = zerolog.Output(&logOutput)
|
|
|
|
defer func() {
|
|
|
|
zerolog.Logger = oldLogger
|
|
|
|
}()
|
|
|
|
|
|
|
|
const projectName = "compose_ignore_orphans_test"
|
|
|
|
|
|
|
|
const composeFileContent = `services:
|
|
|
|
service-1:
|
|
|
|
image: alpine:latest
|
|
|
|
service-2:
|
|
|
|
image: alpine:latest`
|
|
|
|
|
|
|
|
const modifiedFileContent = `services:
|
|
|
|
service-2:
|
|
|
|
image: alpine:latest`
|
|
|
|
|
|
|
|
service1ContainerName := projectName + "-service-1"
|
|
|
|
service2ContainerName := projectName + "-service-2"
|
|
|
|
|
|
|
|
w := NewComposeDeployer()
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
composeFilepath := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
|
|
|
modifiedComposeFilepath := createFile(t, dir, "docker-compose-modified.yml", modifiedFileContent)
|
|
|
|
|
|
|
|
filepaths := []string{composeFilepath}
|
|
|
|
modifiedFilepaths := []string{modifiedComposeFilepath}
|
|
|
|
options := libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{cmdcompose.ComposeIgnoreOrphans + "=true"},
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
err := w.Validate(ctx, filepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = w.Pull(ctx, filepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(service1ContainerName))
|
|
|
|
require.False(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
err = w.Deploy(ctx, filepaths, libstack.DeployOptions{Options: options})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
err = w.Remove(ctx, projectName, filepaths, libstack.RemoveOptions{})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.False(t, containerExists(service1ContainerName))
|
|
|
|
require.False(t, containerExists(service2ContainerName))
|
|
|
|
}()
|
|
|
|
|
|
|
|
require.True(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
waitResult := w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
|
|
|
|
|
|
|
require.Empty(t, waitResult.ErrorMsg)
|
|
|
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
|
|
|
|
|
|
|
err = w.Validate(ctx, modifiedFilepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = w.Pull(ctx, modifiedFilepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.True(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
err = w.Deploy(ctx, modifiedFilepaths, libstack.DeployOptions{Options: options})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.True(t, containerExists(service1ContainerName))
|
|
|
|
require.True(t, containerExists(service2ContainerName))
|
|
|
|
|
|
|
|
waitResult = w.WaitForStatus(ctx, projectName, libstack.StatusCompleted)
|
|
|
|
|
|
|
|
require.Empty(t, waitResult.ErrorMsg)
|
|
|
|
require.Equal(t, libstack.StatusCompleted, waitResult.Status)
|
|
|
|
|
|
|
|
logString := logOutput.String()
|
|
|
|
require.False(t, strings.Contains(logString, "Found orphan containers ([compose_ignore_orphans_test-service-1-1])"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func Test_MaxConcurrency(t *testing.T) {
|
|
|
|
const projectName = "compose_max_concurrency_test"
|
|
|
|
|
|
|
|
const composeFileContent = `services:
|
|
|
|
service-1:
|
|
|
|
image: alpine:latest`
|
|
|
|
|
|
|
|
w := ComposeDeployer{
|
|
|
|
createComposeServiceFn: createMockComposeService,
|
|
|
|
}
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
|
|
|
composeFilepath := createFile(t, dir, "docker-compose.yml", composeFileContent)
|
|
|
|
|
|
|
|
expectedMaxConcurrency := 4
|
|
|
|
|
|
|
|
filepaths := []string{composeFilepath}
|
|
|
|
options := libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{cmdcompose.ComposeParallelLimit + "=" + strconv.Itoa(expectedMaxConcurrency)},
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
err := w.Validate(ctx, filepaths, options)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
w.withComposeService(ctx, filepaths, options, func(service api.Service, _ *types.Project) error {
|
|
|
|
if mockS, ok := service.(*mockComposeService); ok {
|
|
|
|
require.Equal(t, mockS.maxConcurrency, expectedMaxConcurrency)
|
|
|
|
} else {
|
|
|
|
t.Fatalf("Expected mockComposeService but got %T", service)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func Test_createProject(t *testing.T) {
|
|
|
|
ctx := context.Background()
|
|
|
|
dir := t.TempDir()
|
|
|
|
projectName := "create-project-test"
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
err := os.RemoveAll(dir)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Failed to remove temp dir: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
createTestLibstackOptions := func(workingDir, projectName string, env []string, envFilepath string) libstack.Options {
|
|
|
|
return libstack.Options{
|
|
|
|
WorkingDir: workingDir,
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: env,
|
|
|
|
EnvFilePath: envFilepath,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
testSimpleComposeConfig := `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`
|
|
|
|
|
|
|
|
expectedSimpleComposeProject := func(workingDirectory string, envOverrides map[string]string) *types.Project {
|
|
|
|
env := types.Mapping{consts.ComposeProjectName: "create-project-test"}
|
|
|
|
env = env.Merge(envOverrides)
|
|
|
|
|
|
|
|
if workingDirectory == "" {
|
|
|
|
workingDirectory = dir
|
|
|
|
}
|
|
|
|
|
|
|
|
if !filepath.IsAbs(workingDirectory) {
|
|
|
|
absWorkingDir, err := filepath.Abs(workingDirectory)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Failed to get absolute path of working directory (%s): %v", workingDirectory, err)
|
|
|
|
}
|
|
|
|
workingDirectory = absWorkingDir
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: workingDirectory,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: env,
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
testComposeProfilesConfig := `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest
|
|
|
|
profiles: ['web1']
|
|
|
|
apache:
|
|
|
|
container_name: apache
|
|
|
|
image: httpd:latest
|
|
|
|
profiles: ['web2']`
|
|
|
|
|
|
|
|
expectedComposeProfilesProject := func(envOverrides map[string]string) *types.Project {
|
|
|
|
env := types.Mapping{consts.ComposeProfiles: "web1", consts.ComposeProjectName: "create-project-test"}
|
|
|
|
env = env.Merge(envOverrides)
|
|
|
|
|
|
|
|
return &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
Profiles: []string{"web1"},
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: env,
|
|
|
|
DisabledServices: types.Services{
|
|
|
|
"apache": {
|
|
|
|
Name: "apache",
|
|
|
|
Profiles: []string{"web2"},
|
|
|
|
ContainerName: "apache",
|
|
|
|
Environment: nil,
|
|
|
|
Image: "httpd:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Profiles: []string{"web1"},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
testcases := []struct {
|
|
|
|
name string
|
|
|
|
filesToCreate map[string]string
|
|
|
|
configFilepaths []string
|
|
|
|
options libstack.Options
|
2025-05-12 11:18:04 +12:00
|
|
|
osEnv map[string]string
|
2025-03-25 08:57:23 +13:00
|
|
|
expectedProject *types.Project
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "Compose profiles in env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testComposeProfilesConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeProfiles + "=web1"}, ""),
|
|
|
|
expectedProject: expectedComposeProfilesProject(nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose profiles in env file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testComposeProfilesConfig,
|
|
|
|
"stack.env": consts.ComposeProfiles + "=web1",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, nil, dir+"/stack.env"),
|
|
|
|
expectedProject: expectedComposeProfilesProject(nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose profiles in env file in COMPOSE_ENV_FILES",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testComposeProfilesConfig,
|
|
|
|
"stack.env": consts.ComposeProfiles + "=web1",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{cmdcompose.ComposeEnvFiles + "=" + dir + "/stack.env"}, ""),
|
|
|
|
expectedProject: expectedComposeProfilesProject(map[string]string{
|
|
|
|
cmdcompose.ComposeEnvFiles: dir + "/stack.env",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose profiles in both env and env file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testComposeProfilesConfig,
|
|
|
|
"stack.env": consts.ComposeProfiles + "=web2",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeProfiles + "=web1"}, dir+"/stack.env"),
|
|
|
|
expectedProject: expectedComposeProfilesProject(nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose project name in both options and env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeProjectName + "=totally_different_name"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose project name in only env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, "", []string{consts.ComposeProjectName + "=totally_different_name"}, ""),
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: "totally_different_name",
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "totally_different_name_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: types.Mapping{consts.ComposeProjectName: "totally_different_name"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose files in env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: nil,
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeFilePath + "=" + dir + "/docker-compose.yml"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
|
|
|
consts.ComposeFilePath: dir + "/docker-compose.yml",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Compose files in both options and env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
"profiles-docker-compose.yml": testComposeProfilesConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeFilePath + "=" + dir + "/profiles-docker-compose.yml"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
|
|
|
consts.ComposeFilePath: dir + "/profiles-docker-compose.yml",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Multiple Compose files in options",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose-0.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"docker-compose-1.yml": `services:
|
|
|
|
apache:
|
|
|
|
container_name: apache
|
|
|
|
image: httpd:latest`,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose-0.yml", dir + "/docker-compose-1.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{}, ""),
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
"apache": {
|
|
|
|
Name: "apache",
|
|
|
|
ContainerName: "apache",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "httpd:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose-0.yml",
|
|
|
|
dir + "/docker-compose-1.yml",
|
|
|
|
},
|
|
|
|
Environment: types.Mapping{consts.ComposeProjectName: "create-project-test"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Multiple Compose files in env",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose-0.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"docker-compose-1.yml": `services:
|
|
|
|
apache:
|
|
|
|
container_name: apache
|
|
|
|
image: httpd:latest`,
|
|
|
|
},
|
|
|
|
configFilepaths: nil,
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposeFilePath + "=" + dir + "/docker-compose-0.yml:" + dir + "/docker-compose-1.yml"}, ""),
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
"apache": {
|
|
|
|
Name: "apache",
|
|
|
|
ContainerName: "apache",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "httpd:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose-0.yml",
|
|
|
|
dir + "/docker-compose-1.yml",
|
|
|
|
},
|
|
|
|
Environment: types.Mapping{consts.ComposeProjectName: "create-project-test", consts.ComposeFilePath: dir + "/docker-compose-0.yml:" + dir + "/docker-compose-1.yml"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Multiple Compose files in env with COMPOSE_PATH_SEPARATOR",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose-0.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"docker-compose-1.yml": `services:
|
|
|
|
apache:
|
|
|
|
container_name: apache
|
|
|
|
image: httpd:latest`,
|
|
|
|
},
|
|
|
|
configFilepaths: nil,
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{consts.ComposePathSeparator + "=|", consts.ComposeFilePath + "=" + dir + "/docker-compose-0.yml|" + dir + "/docker-compose-1.yml"}, ""),
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
"apache": {
|
|
|
|
Name: "apache",
|
|
|
|
ContainerName: "apache",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "httpd:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose-0.yml",
|
|
|
|
dir + "/docker-compose-1.yml",
|
|
|
|
},
|
|
|
|
Environment: types.Mapping{consts.ComposeProjectName: "create-project-test", consts.ComposePathSeparator: "|", consts.ComposeFilePath: dir + "/docker-compose-0.yml|" + dir + "/docker-compose-1.yml"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose ignore orphans",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{cmdcompose.ComposeIgnoreOrphans + "=true"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
|
|
|
cmdcompose.ComposeIgnoreOrphans: "true",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose remove orphans",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{cmdcompose.ComposeRemoveOrphans + "=true"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
|
|
|
cmdcompose.ComposeRemoveOrphans: "true",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "compose parallel limit",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: createTestLibstackOptions(dir, projectName, []string{cmdcompose.ComposeParallelLimit + "=true"}, ""),
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
|
|
|
cmdcompose.ComposeParallelLimit: "true",
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Absolute Working Directory",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
2025-04-04 09:07:35 +13:00
|
|
|
// Note that this is the execution working directory not the compose project working directory
|
|
|
|
// and so it has no affect on the created projects working directory
|
2025-03-25 08:57:23 +13:00
|
|
|
WorkingDir: "/something-totally-different",
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
2025-04-04 09:07:35 +13:00
|
|
|
expectedProject: expectedSimpleComposeProject("", nil),
|
2025-03-25 08:57:23 +13:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Relative Working Directory",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
2025-04-04 09:07:35 +13:00
|
|
|
// Note that this is the execution working directory not the compose project working directory
|
|
|
|
// and so it has no affect on the created projects working directory
|
2025-03-25 08:57:23 +13:00
|
|
|
WorkingDir: "something-totally-different",
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
2025-04-04 09:07:35 +13:00
|
|
|
expectedProject: expectedSimpleComposeProject("", nil),
|
2025-03-25 08:57:23 +13:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Absolute Project Directory",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectDir: "/something-totally-different",
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
expectedProject: expectedSimpleComposeProject("/something-totally-different", nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Relative Project Directory",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectDir: "something-totally-different",
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
expectedProject: expectedSimpleComposeProject("something-totally-different", nil),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Absolute Project and Working Directory set",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
WorkingDir: "/working-dir",
|
|
|
|
ProjectDir: "/project-dir",
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
expectedProject: expectedSimpleComposeProject("/project-dir", nil),
|
|
|
|
},
|
2025-05-12 11:18:04 +12:00
|
|
|
{
|
|
|
|
name: "OS Env Vars",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": testSimpleComposeConfig,
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
},
|
|
|
|
osEnv: map[string]string{
|
|
|
|
"PORTAINER_WEB_FOLDER": "html-1",
|
|
|
|
"other_var": "something",
|
|
|
|
},
|
|
|
|
expectedProject: expectedSimpleComposeProject("", map[string]string{"PORTAINER_WEB_FOLDER": "html-1"}),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Env Vars in compose file, compose env file, env, os, and env_file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest
|
|
|
|
env_file: ` + dir + `/compose-stack.env
|
|
|
|
environment:
|
|
|
|
PORTAINER_VAR: compose_file_environment`,
|
|
|
|
"stack.env": "PORTAINER_VAR=env_file",
|
|
|
|
"compose-stack.env": "PORTAINER_VAR=compose_env_file",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{"PORTAINER_VAR=env"},
|
|
|
|
EnvFilePath: dir + "/stack.env",
|
|
|
|
},
|
|
|
|
osEnv: map[string]string{
|
|
|
|
"PORTAINER_VAR": "os",
|
|
|
|
},
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.NewMappingWithEquals([]string{"PORTAINER_VAR=compose_file_environment"}),
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: map[string]string{"COMPOSE_PROJECT_NAME": "create-project-test", "PORTAINER_VAR": "env"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Env Vars in compose env file, env, os, and env_file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest
|
|
|
|
env_file: ` + dir + `/compose-stack.env`,
|
|
|
|
"stack.env": "PORTAINER_VAR=env_file",
|
|
|
|
"compose-stack.env": "PORTAINER_VAR=compose_env_file",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{"PORTAINER_VAR=env"},
|
|
|
|
EnvFilePath: dir + "/stack.env",
|
|
|
|
},
|
|
|
|
osEnv: map[string]string{
|
|
|
|
"PORTAINER_VAR": "os",
|
|
|
|
},
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.NewMappingWithEquals([]string{"PORTAINER_VAR=compose_env_file"}),
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: map[string]string{"COMPOSE_PROJECT_NAME": "create-project-test", "PORTAINER_VAR": "env"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Env Vars in env, os, and env_file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"stack.env": "PORTAINER_VAR=env_file",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
Env: []string{"PORTAINER_VAR=env"},
|
|
|
|
EnvFilePath: dir + "/stack.env",
|
|
|
|
},
|
|
|
|
osEnv: map[string]string{
|
|
|
|
"PORTAINER_VAR": "os",
|
|
|
|
},
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: map[string]string{"COMPOSE_PROJECT_NAME": "create-project-test", "PORTAINER_VAR": "env"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Env Vars in os and env_file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"stack.env": "PORTAINER_VAR=env_file",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
EnvFilePath: dir + "/stack.env",
|
|
|
|
},
|
|
|
|
osEnv: map[string]string{
|
|
|
|
"PORTAINER_VAR": "os",
|
|
|
|
},
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: map[string]string{"COMPOSE_PROJECT_NAME": "create-project-test", "PORTAINER_VAR": "os"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "Env Vars in env_file",
|
|
|
|
filesToCreate: map[string]string{
|
|
|
|
"docker-compose.yml": `services:
|
|
|
|
nginx:
|
|
|
|
container_name: nginx
|
|
|
|
image: nginx:latest`,
|
|
|
|
"stack.env": "PORTAINER_VAR=env_file",
|
|
|
|
},
|
|
|
|
configFilepaths: []string{dir + "/docker-compose.yml"},
|
|
|
|
options: libstack.Options{
|
|
|
|
ProjectName: projectName,
|
|
|
|
EnvFilePath: dir + "/stack.env",
|
|
|
|
},
|
|
|
|
expectedProject: &types.Project{
|
|
|
|
Name: projectName,
|
|
|
|
WorkingDir: dir,
|
|
|
|
Services: types.Services{
|
|
|
|
"nginx": {
|
|
|
|
Name: "nginx",
|
|
|
|
ContainerName: "nginx",
|
|
|
|
Environment: types.MappingWithEquals{},
|
|
|
|
Image: "nginx:latest",
|
|
|
|
Networks: map[string]*types.ServiceNetworkConfig{"default": nil},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
|
|
|
ComposeFiles: []string{
|
|
|
|
dir + "/docker-compose.yml",
|
|
|
|
},
|
|
|
|
Environment: map[string]string{"COMPOSE_PROJECT_NAME": "create-project-test", "PORTAINER_VAR": "env_file"},
|
|
|
|
DisabledServices: types.Services{},
|
|
|
|
Profiles: []string{""},
|
|
|
|
},
|
|
|
|
},
|
2025-03-25 08:57:23 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, tc := range testcases {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
createdFiles := make([]string, 0, len(tc.filesToCreate))
|
|
|
|
for f, fc := range tc.filesToCreate {
|
|
|
|
createdFiles = append(createdFiles, createFile(t, dir, f, fc))
|
|
|
|
}
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
var errs []error
|
|
|
|
for _, f := range createdFiles {
|
|
|
|
errs = append(errs, os.Remove(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
err := errors.Join(errs...)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Failed to remove config files: %v", err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2025-05-12 11:18:04 +12:00
|
|
|
for k, v := range tc.osEnv {
|
|
|
|
t.Setenv(k, v)
|
|
|
|
}
|
|
|
|
|
2025-03-25 08:57:23 +13:00
|
|
|
gotProject, err := createProject(ctx, tc.configFilepaths, tc.options)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Failed to create new project: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if diff := cmp.Diff(gotProject, tc.expectedProject); diff != "" {
|
|
|
|
t.Fatalf("Projects are different:\n%s", diff)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func createMockComposeService(dockerCli command.Cli) api.Service {
|
|
|
|
return &mockComposeService{}
|
|
|
|
}
|
|
|
|
|
|
|
|
type mockComposeService struct {
|
|
|
|
api.Service
|
|
|
|
maxConcurrency int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *mockComposeService) MaxConcurrency(parallel int) {
|
|
|
|
s.maxConcurrency = parallel
|
|
|
|
}
|