mirror of
https://github.com/portainer/portainer.git
synced 2025-07-18 21:09:40 +02:00
fix(libstack): correctly load COMPOSE_* env vars [BE-11474] (#536)
This commit is contained in:
parent
5d1cd670e9
commit
34235199dd
7 changed files with 952 additions and 87 deletions
1
go.mod
1
go.mod
|
@ -192,6 +192,7 @@ require (
|
|||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -481,6 +481,8 @@ github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
|
|||
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
package compose
|
||||
|
||||
type ComposeDeployer struct{}
|
||||
import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
)
|
||||
|
||||
type ComposeDeployer struct {
|
||||
createComposeServiceFn func(command.Cli) api.Service
|
||||
}
|
||||
|
||||
// NewComposeDeployer creates a new compose deployer
|
||||
func NewComposeDeployer() *ComposeDeployer {
|
||||
return &ComposeDeployer{}
|
||||
return &ComposeDeployer{
|
||||
createComposeServiceFn: compose.NewComposeService,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
@ -15,13 +14,14 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/dotenv"
|
||||
"github.com/compose-spec/compose-go/v2/loader"
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
cmdcompose "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -75,66 +75,34 @@ func withCli(
|
|||
return cliFn(ctx, cli)
|
||||
}
|
||||
|
||||
func withComposeService(
|
||||
func (c *ComposeDeployer) withComposeService(
|
||||
ctx context.Context,
|
||||
filePaths []string,
|
||||
options libstack.Options,
|
||||
composeFn func(api.Service, *types.Project) error,
|
||||
) error {
|
||||
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
|
||||
composeService := compose.NewComposeService(cli)
|
||||
composeService := c.createComposeServiceFn(cli)
|
||||
|
||||
if len(filePaths) == 0 {
|
||||
return composeFn(composeService, nil)
|
||||
}
|
||||
|
||||
env, err := parseEnvironment(options, filePaths)
|
||||
project, err := createProject(ctx, filePaths, options)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create compose project: %w", err)
|
||||
}
|
||||
|
||||
configDetails := types.ConfigDetails{
|
||||
Environment: env,
|
||||
WorkingDir: filepath.Dir(filePaths[0]),
|
||||
}
|
||||
|
||||
if options.ProjectDir != "" {
|
||||
// When relative paths are used in the compose file, the project directory is used as the base path
|
||||
configDetails.WorkingDir = options.ProjectDir
|
||||
}
|
||||
|
||||
for _, p := range filePaths {
|
||||
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
|
||||
}
|
||||
|
||||
project, err := loader.LoadWithContext(ctx, configDetails,
|
||||
func(o *loader.Options) {
|
||||
o.SkipResolveEnvironment = true
|
||||
o.ResolvePaths = !slices.Contains(options.ConfigOptions, "--no-path-resolution")
|
||||
|
||||
if options.ProjectName != "" {
|
||||
o.SetProjectName(options.ProjectName, true)
|
||||
}
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load the compose file: %w", err)
|
||||
}
|
||||
|
||||
// Work around compose path handling
|
||||
for i, service := range project.Services {
|
||||
for j, envFile := range service.EnvFiles {
|
||||
if !filepath.IsAbs(envFile.Path) {
|
||||
project.Services[i].EnvFiles[j].Path = filepath.Join(configDetails.WorkingDir, envFile.Path)
|
||||
}
|
||||
parallel := 0
|
||||
if v, ok := project.Environment[cmdcompose.ComposeParallelLimit]; ok {
|
||||
i, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s must be an integer (found: %q)", cmdcompose.ComposeParallelLimit, v)
|
||||
}
|
||||
parallel = i
|
||||
}
|
||||
|
||||
// Set the services environment variables
|
||||
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
|
||||
project = p
|
||||
} else {
|
||||
return fmt.Errorf("failed to resolve services environment: %w", err)
|
||||
if parallel > 0 {
|
||||
composeService.MaxConcurrency(parallel)
|
||||
}
|
||||
|
||||
return composeFn(composeService, project)
|
||||
|
@ -143,7 +111,7 @@ func withComposeService(
|
|||
|
||||
// Deploy creates and starts containers
|
||||
func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, options libstack.DeployOptions) error {
|
||||
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||
return c.withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||
addServiceLabels(project, false, options.EdgeStackID)
|
||||
|
||||
project = project.WithoutUnnecessaryResources()
|
||||
|
@ -154,6 +122,12 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
|
|||
}
|
||||
|
||||
opts.Create.RemoveOrphans = options.RemoveOrphans
|
||||
if removeOrphans, ok := project.Environment[cmdcompose.ComposeRemoveOrphans]; ok {
|
||||
opts.Create.RemoveOrphans = utils.StringToBool(removeOrphans)
|
||||
}
|
||||
if ignoreOrphans, ok := project.Environment[cmdcompose.ComposeIgnoreOrphans]; ok {
|
||||
opts.Create.IgnoreOrphans = utils.StringToBool(ignoreOrphans)
|
||||
}
|
||||
|
||||
if options.AbortOnContainerExit {
|
||||
opts.Start.OnExit = api.CascadeStop
|
||||
|
@ -175,7 +149,7 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
|
|||
|
||||
// Run runs the given service just once, without considering dependencies
|
||||
func (c *ComposeDeployer) Run(ctx context.Context, filePaths []string, serviceName string, options libstack.RunOptions) error {
|
||||
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||
return c.withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
|
||||
addServiceLabels(project, true, 0)
|
||||
|
||||
for name, service := range project.Services {
|
||||
|
@ -227,7 +201,7 @@ func (c *ComposeDeployer) Remove(ctx context.Context, projectName string, filePa
|
|||
|
||||
// Pull pulls images
|
||||
func (c *ComposeDeployer) Pull(ctx context.Context, filePaths []string, options libstack.Options) error {
|
||||
if err := withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
if err := c.withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
return composeService.Pull(ctx, project, api.PullOptions{})
|
||||
}); err != nil {
|
||||
return fmt.Errorf("compose pull operation failed: %w", err)
|
||||
|
@ -240,7 +214,7 @@ func (c *ComposeDeployer) Pull(ctx context.Context, filePaths []string, options
|
|||
|
||||
// Validate validates stack file
|
||||
func (c *ComposeDeployer) Validate(ctx context.Context, filePaths []string, options libstack.Options) error {
|
||||
return withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
return c.withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -249,7 +223,7 @@ func (c *ComposeDeployer) Validate(ctx context.Context, filePaths []string, opti
|
|||
func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, options libstack.Options) ([]byte, error) {
|
||||
var payload []byte
|
||||
|
||||
if err := withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
if err := c.withComposeService(ctx, filePaths, options, func(composeService api.Service, project *types.Project) error {
|
||||
var err error
|
||||
payload, err = project.MarshalYAML()
|
||||
if err != nil {
|
||||
|
@ -267,7 +241,7 @@ func (c *ComposeDeployer) Config(ctx context.Context, filePaths []string, option
|
|||
func (c *ComposeDeployer) GetExistingEdgeStacks(ctx context.Context) ([]libstack.EdgeStack, error) {
|
||||
m := make(map[int]libstack.EdgeStack)
|
||||
|
||||
if err := withComposeService(ctx, nil, libstack.Options{}, func(composeService api.Service, project *types.Project) error {
|
||||
if err := c.withComposeService(ctx, nil, libstack.Options{}, func(composeService api.Service, project *types.Project) error {
|
||||
stacks, err := composeService.List(ctx, api.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
|
@ -332,43 +306,71 @@ func addServiceLabels(project *types.Project, oneOff bool, edgeStackID portainer
|
|||
}
|
||||
}
|
||||
|
||||
func parseEnvironment(options libstack.Options, filePaths []string) (map[string]string, error) {
|
||||
env := make(map[string]string)
|
||||
|
||||
for _, envLine := range options.Env {
|
||||
e, err := dotenv.UnmarshalWithLookup(envLine, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse environment variables: %w", err)
|
||||
}
|
||||
|
||||
maps.Copy(env, e)
|
||||
func createProject(ctx context.Context, configFilepaths []string, options libstack.Options) (*types.Project, error) {
|
||||
var workingDir string
|
||||
if len(configFilepaths) > 0 {
|
||||
workingDir = filepath.Dir(configFilepaths[0])
|
||||
}
|
||||
|
||||
if options.EnvFilePath == "" {
|
||||
if len(filePaths) == 0 {
|
||||
return env, nil
|
||||
}
|
||||
|
||||
defaultDotEnv := filepath.Join(filepath.Dir(filePaths[0]), ".env")
|
||||
s, err := os.Stat(defaultDotEnv)
|
||||
if os.IsNotExist(err) {
|
||||
return env, nil
|
||||
}
|
||||
if err != nil {
|
||||
return env, err
|
||||
}
|
||||
if s.IsDir() {
|
||||
return env, nil
|
||||
}
|
||||
options.EnvFilePath = defaultDotEnv
|
||||
if options.WorkingDir != "" {
|
||||
workingDir = options.WorkingDir
|
||||
}
|
||||
|
||||
e, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
|
||||
if options.ProjectDir != "" {
|
||||
// When relative paths are used in the compose file, the project directory is used as the base path
|
||||
workingDir = options.ProjectDir
|
||||
}
|
||||
|
||||
var envFiles []string
|
||||
if options.EnvFilePath != "" {
|
||||
envFiles = append(envFiles, options.EnvFilePath)
|
||||
}
|
||||
|
||||
projectOptions, err := cli.NewProjectOptions(configFilepaths,
|
||||
cli.WithWorkingDirectory(workingDir),
|
||||
cli.WithName(options.ProjectName),
|
||||
cli.WithoutEnvironmentResolution,
|
||||
cli.WithResolvedPaths(!slices.Contains(options.ConfigOptions, "--no-path-resolution")),
|
||||
cli.WithEnv(options.Env),
|
||||
cli.WithEnvFiles(envFiles...),
|
||||
func(o *cli.ProjectOptions) error {
|
||||
if len(o.EnvFiles) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if fs, ok := o.Environment[cmdcompose.ComposeEnvFiles]; ok {
|
||||
o.EnvFiles = strings.Split(fs, ",")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
cli.WithDotEnv,
|
||||
cli.WithDefaultProfiles(),
|
||||
cli.WithConfigFileEnv,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get the environment from the env file: %w", err)
|
||||
return nil, fmt.Errorf("failed to load the compose file options : %w", err)
|
||||
}
|
||||
|
||||
maps.Copy(env, e)
|
||||
project, err := projectOptions.LoadProject(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load the compose file : %w", err)
|
||||
}
|
||||
|
||||
return env, nil
|
||||
// Work around compose path handling
|
||||
for i, service := range project.Services {
|
||||
for j, envFile := range service.EnvFiles {
|
||||
if !filepath.IsAbs(envFile.Path) {
|
||||
project.Services[i].EnvFiles[j].Path = filepath.Join(workingDir, envFile.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the services environment variables
|
||||
if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
|
||||
project = p
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to resolve services environment: %w", err)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
|
|
@ -2,14 +2,23 @@ package compose
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
zerolog "github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -349,3 +358,735 @@ networks:
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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{
|
||||
WorkingDir: "/something-totally-different",
|
||||
ProjectName: projectName,
|
||||
},
|
||||
expectedProject: expectedSimpleComposeProject("/something-totally-different", nil),
|
||||
},
|
||||
{
|
||||
name: "Relative Working Directory",
|
||||
filesToCreate: map[string]string{
|
||||
"docker-compose.yml": testSimpleComposeConfig,
|
||||
},
|
||||
configFilepaths: []string{dir + "/docker-compose.yml"},
|
||||
options: libstack.Options{
|
||||
WorkingDir: "something-totally-different",
|
||||
ProjectName: projectName,
|
||||
},
|
||||
expectedProject: expectedSimpleComposeProject("something-totally-different", nil),
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
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
|
||||
}
|
||||
|
|
109
pkg/libstack/compose/composeplugin_windows_test.go
Normal file
109
pkg/libstack/compose/composeplugin_windows_test.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package compose
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
)
|
||||
|
||||
func Test_createProject_win(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)
|
||||
}
|
||||
}()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
createFilesFn func() []string
|
||||
configFilepaths []string
|
||||
options libstack.Options
|
||||
expectedProject *types.Project
|
||||
}{
|
||||
{
|
||||
name: "Convert windows paths",
|
||||
createFilesFn: func() []string {
|
||||
var filepaths []string
|
||||
filepaths = append(filepaths, createFile(t, dir, "docker-compose.yml", `services:
|
||||
nginx:
|
||||
container_name: nginx
|
||||
image: nginx:latest
|
||||
volumes:
|
||||
- "C:\\Users\\Joey\\Desktop\\backend:/var/www/html"`))
|
||||
return filepaths
|
||||
},
|
||||
configFilepaths: []string{dir + "/docker-compose.yml"},
|
||||
options: libstack.Options{
|
||||
WorkingDir: dir,
|
||||
ProjectName: projectName,
|
||||
Env: []string{"COMPOSE_CONVERT_WINDOWS_PATHS=true"},
|
||||
},
|
||||
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},
|
||||
Volumes: []types.ServiceVolumeConfig{
|
||||
{
|
||||
Type: "bind",
|
||||
Source: "/c/Users/Joey/Desktop/backend",
|
||||
Target: "/var/www/html",
|
||||
ReadOnly: false,
|
||||
Bind: &types.ServiceVolumeBind{CreateHostPath: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Networks: types.Networks{"default": {Name: "create-project-test_default"}},
|
||||
ComposeFiles: []string{
|
||||
dir + "/docker-compose.yml",
|
||||
},
|
||||
Environment: types.Mapping{"COMPOSE_PROJECT_NAME": "create-project-test", "COMPOSE_CONVERT_WINDOWS_PATHS": "true"},
|
||||
DisabledServices: types.Services{},
|
||||
Profiles: []string{""},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
createdFiles := tc.createFilesFn()
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -125,7 +125,7 @@ func (c *ComposeDeployer) WaitForStatus(ctx context.Context, name string, status
|
|||
|
||||
var containerSummaries []api.ContainerSummary
|
||||
|
||||
if err := withComposeService(ctx, nil, libstack.Options{ProjectName: name}, func(composeService api.Service, project *types.Project) error {
|
||||
if err := c.withComposeService(ctx, nil, libstack.Options{ProjectName: name}, func(composeService api.Service, project *types.Project) error {
|
||||
var err error
|
||||
|
||||
psCtx, cancelFunc := context.WithTimeout(context.Background(), time.Minute)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue