mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 10:04:49 +00:00
fix(swarm): relative env_file [BE-13157] (#3122)
Co-authored-by: zacxihu <4939037+zacxihu@users.noreply.github.com>
This commit is contained in:
@@ -52,7 +52,6 @@ type Options struct {
|
||||
ProjectName string
|
||||
Host string
|
||||
Env []string
|
||||
WorkingDir string
|
||||
Registries []configtypes.AuthConfig
|
||||
}
|
||||
|
||||
@@ -104,7 +103,7 @@ func (d *SwarmDeployer) Deploy(ctx context.Context, filePaths []string, options
|
||||
|
||||
// Validate loads and parses the compose file(s), returning an error if they are invalid.
|
||||
func (d *SwarmDeployer) Validate(_ context.Context, filePaths []string, options Options) error {
|
||||
_, err := getConfig(filePaths, options.WorkingDir, options.Env)
|
||||
_, err := getConfig(filePaths, options.Env)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -186,7 +185,7 @@ func deployStack(ctx context.Context, dockerCLI *command.DockerCli, filePaths []
|
||||
return errors.New(`this node is not a swarm manager. Use "docker swarm init" or "docker swarm join" to connect this node to swarm and try again`)
|
||||
}
|
||||
|
||||
config, err := getConfig(filePaths, options.WorkingDir, options.Env)
|
||||
config, err := getConfig(filePaths, options.Env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load compose file: %w", err)
|
||||
}
|
||||
@@ -617,9 +616,9 @@ func isTerminalState(state swarm.TaskState) bool {
|
||||
return taskStateOrdinal[state] > taskStateOrdinal[swarm.TaskStateRunning]
|
||||
}
|
||||
|
||||
func getConfig(filePaths []string, workingDir string, env []string) (*composetypes.Config, error) {
|
||||
func getConfig(filePaths []string, env []string) (*composetypes.Config, error) {
|
||||
// Load and parse the compose file(s).
|
||||
configDetails, err := getConfigDetails(filePaths, workingDir, env)
|
||||
configDetails, err := getConfigDetails(filePaths, env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load compose file: %w", err)
|
||||
}
|
||||
@@ -659,14 +658,17 @@ func getConfig(filePaths []string, workingDir string, env []string) (*composetyp
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func getConfigDetails(filePaths []string, workingDir string, env []string) (composetypes.ConfigDetails, error) {
|
||||
func getConfigDetails(filePaths []string, env []string) (composetypes.ConfigDetails, error) {
|
||||
var details composetypes.ConfigDetails
|
||||
|
||||
if len(filePaths) == 0 {
|
||||
return details, errors.New("at least one compose file must be specified")
|
||||
}
|
||||
|
||||
details.WorkingDir = workingDir
|
||||
// Resolve relative references against the compose file's own directory, matching
|
||||
// Docker Compose semantics (the default project directory is the directory of the
|
||||
// first compose file) and the compose (non-swarm) libstack deployer.
|
||||
details.WorkingDir = filepath.Dir(filePaths[0])
|
||||
|
||||
details.ConfigFiles = make([]composetypes.ConfigFile, 0, len(filePaths))
|
||||
for _, fp := range filePaths {
|
||||
@@ -680,7 +682,7 @@ func getConfigDetails(filePaths []string, workingDir string, env []string) (comp
|
||||
return details, err
|
||||
}
|
||||
|
||||
resolveEnvFilePaths(config, workingDir)
|
||||
resolveEnvFilePaths(config, filepath.Dir(fp))
|
||||
|
||||
details.ConfigFiles = append(details.ConfigFiles, composetypes.ConfigFile{
|
||||
Filename: fp,
|
||||
|
||||
@@ -3,6 +3,7 @@ package swarm
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
@@ -260,6 +261,7 @@ func Test_getConfig(t *testing.T) {
|
||||
|
||||
writeFile := func(name, content string) string {
|
||||
path := filesystem.JoinPaths(dir, name)
|
||||
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
|
||||
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
|
||||
return path
|
||||
}
|
||||
@@ -270,7 +272,6 @@ func Test_getConfig(t *testing.T) {
|
||||
name string
|
||||
composeFiles map[string]string
|
||||
files map[string]string
|
||||
workingDir string
|
||||
env []string
|
||||
osEnv map[string]string
|
||||
expectedCfg *composetypes.Config
|
||||
@@ -284,7 +285,6 @@ services:
|
||||
web:
|
||||
image: nginx:latest`,
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/valid.yml",
|
||||
Version: "3.13",
|
||||
@@ -306,7 +306,6 @@ services:
|
||||
composeFiles: map[string]string{
|
||||
"invalid.yml": `not: valid: yaml: content`,
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedErr: "failed to load compose file: yaml: mapping values are not allowed in this context",
|
||||
},
|
||||
{
|
||||
@@ -321,7 +320,6 @@ services:
|
||||
web:
|
||||
command: echo hello`,
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedErr: "invalid image reference for service web: no image specified",
|
||||
},
|
||||
{
|
||||
@@ -336,7 +334,6 @@ services:
|
||||
worker:
|
||||
image: alpine:latest`,
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/base.yml",
|
||||
Version: "3.13",
|
||||
@@ -366,8 +363,7 @@ services:
|
||||
web:
|
||||
image: nginx:${TAG}`,
|
||||
},
|
||||
workingDir: dir,
|
||||
env: []string{"TAG=1.25"},
|
||||
env: []string{"TAG=1.25"},
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/envvar.yml",
|
||||
Version: "3.13",
|
||||
@@ -392,7 +388,6 @@ services:
|
||||
web:
|
||||
image: nginx:${PORTAINER_TAG}`,
|
||||
},
|
||||
workingDir: dir,
|
||||
osEnv: map[string]string{
|
||||
libstack.PortainerEnvVarsPrefix + "TAG": "1.25",
|
||||
},
|
||||
@@ -413,7 +408,7 @@ services:
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "env_file with relative path is resolved using workingDir",
|
||||
name: "env_file with relative path is resolved against the compose file directory",
|
||||
composeFiles: map[string]string{
|
||||
"docker-compose.yaml": `services:
|
||||
configtest:
|
||||
@@ -424,7 +419,6 @@ services:
|
||||
files: map[string]string{
|
||||
"stack.env": "A=junk",
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/docker-compose.yaml",
|
||||
Version: "3.13",
|
||||
@@ -456,7 +450,6 @@ services:
|
||||
files: map[string]string{
|
||||
"stack.env": "A=junk",
|
||||
},
|
||||
workingDir: dir,
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/docker-compose.yaml",
|
||||
Version: "3.13",
|
||||
@@ -476,6 +469,41 @@ services:
|
||||
Configs: map[string]composetypes.ConfigObjConfig{},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Regression test for BE-13157: a Git stack whose compose file lives in a
|
||||
// sub-directory must resolve a sibling env_file relative to that sub-directory,
|
||||
// not the project root.
|
||||
name: "env_file with relative path is resolved within the compose file sub-directory",
|
||||
composeFiles: map[string]string{
|
||||
"sub/docker-compose.yaml": `services:
|
||||
configtest:
|
||||
image: nginx:latest
|
||||
env_file:
|
||||
- stack.env`,
|
||||
},
|
||||
files: map[string]string{
|
||||
"sub/stack.env": "A=junk",
|
||||
"stack.env": "A=not-this-one",
|
||||
},
|
||||
expectedCfg: &composetypes.Config{
|
||||
Filename: dir + "/sub/docker-compose.yaml",
|
||||
Version: "3.13",
|
||||
Services: composetypes.Services{
|
||||
composetypes.ServiceConfig{
|
||||
Name: "configtest",
|
||||
Environment: composetypes.MappingWithEquals{
|
||||
"A": getStrPointer("junk"),
|
||||
},
|
||||
Image: "nginx:latest",
|
||||
EnvFile: []string{dir + "/sub/stack.env"},
|
||||
},
|
||||
},
|
||||
Networks: map[string]composetypes.NetworkConfig{},
|
||||
Volumes: map[string]composetypes.VolumeConfig{},
|
||||
Secrets: map[string]composetypes.SecretConfig{},
|
||||
Configs: map[string]composetypes.ConfigObjConfig{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -494,7 +522,7 @@ services:
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
cfg, err := getConfig(filePaths, tt.workingDir, tt.env)
|
||||
cfg, err := getConfig(filePaths, tt.env)
|
||||
if err != nil {
|
||||
if tt.expectedErr == "" {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user