mirror of https://github.com/portainer/portainer
fix(compose): merge default and in-place stack env vars [EE-2860] (#7076)
parent
dd4d126934
commit
33861a834b
|
@ -6,7 +6,6 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -14,7 +13,6 @@ import (
|
|||
libstack "github.com/portainer/docker-compose-wrapper"
|
||||
"github.com/portainer/docker-compose-wrapper/compose"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
|
@ -56,13 +54,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
envFilePath, err := createEnvFile(stack)
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
|
||||
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
|
||||
return errors.Wrap(err, "failed to deploy a stack")
|
||||
}
|
||||
|
||||
|
@ -76,12 +74,14 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
|
|||
defer proxy.Close()
|
||||
}
|
||||
|
||||
if err := updateNetworkEnvFile(stack); err != nil {
|
||||
return err
|
||||
envFile, err := createEnvFile(stack)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create env file")
|
||||
}
|
||||
|
||||
filePaths := stackutils.GetStackFilePaths(stack)
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
|
||||
|
||||
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
|
||||
return errors.Wrap(err, "failed to remove a stack")
|
||||
}
|
||||
|
||||
|
@ -103,200 +103,42 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
|
|||
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
|
||||
}
|
||||
|
||||
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
|
||||
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
|
||||
func createEnvFile(stack *portainer.Stack) (string, error) {
|
||||
// workaround for EE-1862. It will have to be removed when
|
||||
// docker/compose upgraded to v2.x.
|
||||
if err := createNetworkEnvFile(stack); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create network env file")
|
||||
}
|
||||
|
||||
if stack.Env == nil || len(stack.Env) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
envFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
|
||||
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer envfile.Close()
|
||||
|
||||
copyDefaultEnvFile(stack, envfile)
|
||||
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
envfile.Close()
|
||||
|
||||
return "stack.env", nil
|
||||
}
|
||||
|
||||
func fileNotExist(filePath string) bool {
|
||||
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func updateNetworkEnvFile(stack *portainer.Stack) error {
|
||||
envFilePath := path.Join(stack.ProjectPath, ".env")
|
||||
stackFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
if fileNotExist(envFilePath) {
|
||||
if fileNotExist(stackFilePath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := os.O_WRONLY | os.O_SYNC | os.O_CREATE
|
||||
envFile, err := os.OpenFile(envFilePath, flags, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer envFile.Close()
|
||||
|
||||
stackFile, err := os.Open(stackFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stackFile.Close()
|
||||
|
||||
_, err = io.Copy(envFile, stackFile)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createNetworkEnvFile(stack *portainer.Stack) error {
|
||||
networkNameSet := NewStringSet()
|
||||
|
||||
for _, filePath := range stackutils.GetStackFilePaths(stack) {
|
||||
networkNames, err := extractNetworkNames(filePath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to extract network name")
|
||||
}
|
||||
|
||||
if networkNames == nil || networkNames.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
networkNameSet.Union(networkNames)
|
||||
}
|
||||
|
||||
for _, s := range networkNameSet.List() {
|
||||
if _, ok := os.LookupEnv(s); ok {
|
||||
networkNameSet.Remove(s)
|
||||
}
|
||||
}
|
||||
|
||||
if networkNameSet.Len() == 0 && stack.Env == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
envfile, err := os.OpenFile(path.Join(stack.ProjectPath, ".env"),
|
||||
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
|
||||
func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) {
|
||||
defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open env file")
|
||||
// If cannot open a default file, then don't need to copy it.
|
||||
// We could as well stat it and check if it exists, but this is more efficient.
|
||||
return
|
||||
}
|
||||
|
||||
defer envfile.Close()
|
||||
defer defaultEnvFile.Close()
|
||||
|
||||
var scanEnvSettingFunc = func(name string) (string, bool) {
|
||||
if stack.Env != nil {
|
||||
for _, v := range stack.Env {
|
||||
if name == v.Name {
|
||||
return v.Value, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
if _, err = io.Copy(w, defaultEnvFile); err == nil {
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
|
||||
for _, s := range networkNameSet.List() {
|
||||
if _, ok := scanEnvSettingFunc(s); !ok {
|
||||
stack.Env = append(stack.Env, portainer.Pair{
|
||||
Name: s,
|
||||
Value: "None",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if stack.Env != nil {
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(
|
||||
fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractNetworkNames(filePath string) (StringSet, error) {
|
||||
if info, err := os.Stat(filePath); errors.Is(err,
|
||||
os.ErrNotExist) || info.IsDir() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
stackFileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open yaml file")
|
||||
}
|
||||
|
||||
config, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
// invalid stack file
|
||||
return nil, errors.Wrap(err, "invalid stack file")
|
||||
}
|
||||
|
||||
var version string
|
||||
if _, ok := config["version"]; ok {
|
||||
version, _ = config["version"].(string)
|
||||
}
|
||||
|
||||
var networks map[string]interface{}
|
||||
if value, ok := config["networks"]; ok {
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if networks, ok = value.(map[string]interface{}); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
networkContent, err := loader.LoadNetworks(networks, version)
|
||||
if err != nil {
|
||||
return nil, nil // skip the error
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^\$\{?([^\}]+)\}?$`)
|
||||
networkNames := NewStringSet()
|
||||
|
||||
for _, v := range networkContent {
|
||||
matched := re.FindAllStringSubmatch(v.Name, -1)
|
||||
if matched != nil && matched[0] != nil {
|
||||
if strings.Contains(matched[0][1], ":-") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(matched[0][1], "?") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.Contains(matched[0][1], "-") {
|
||||
continue
|
||||
}
|
||||
|
||||
networkNames.Add(matched[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
if networkNames.Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return networkNames, nil
|
||||
// If couldn't copy the .env file, then ignore the error and try to continue
|
||||
}
|
||||
|
|
|
@ -65,56 +65,22 @@ func Test_createEnvFile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_createNetworkEnvFile(t *testing.T) {
|
||||
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
buf := []byte(`
|
||||
version: '3.6'
|
||||
services:
|
||||
nginx-example:
|
||||
image: nginx:latest
|
||||
networks:
|
||||
default:
|
||||
name: ${test}
|
||||
driver: bridge
|
||||
`)
|
||||
if err := ioutil.WriteFile(path.Join(dir,
|
||||
"docker-compose.yml"), buf, 0644); err != nil {
|
||||
t.Fatalf("Failed to create yaml file: %s", err)
|
||||
}
|
||||
|
||||
stackWithoutEnv := &portainer.Stack{
|
||||
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
|
||||
stack := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: "docker-compose.yml",
|
||||
Env: []portainer.Pair{},
|
||||
}
|
||||
|
||||
if err := createNetworkEnvFile(stackWithoutEnv); err != nil {
|
||||
t.Fatalf("Failed to create network env file: %s", err)
|
||||
}
|
||||
|
||||
content, err := ioutil.ReadFile(path.Join(dir, ".env"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read network env file: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test=None\n", string(content))
|
||||
|
||||
stackWithEnv := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: "docker-compose.yml",
|
||||
Env: []portainer.Pair{
|
||||
{Name: "test", Value: "test-value"},
|
||||
{Name: "VAR1", Value: "NEW_VAL1"},
|
||||
{Name: "VAR3", Value: "VAL3"},
|
||||
},
|
||||
}
|
||||
result, err := createEnvFile(stack)
|
||||
assert.Equal(t, "stack.env", result)
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, path.Join(dir, "stack.env"))
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
content, _ := ioutil.ReadAll(f)
|
||||
|
||||
if err := createNetworkEnvFile(stackWithEnv); err != nil {
|
||||
t.Fatalf("Failed to create network env file: %s", err)
|
||||
}
|
||||
|
||||
content, err = ioutil.ReadFile(path.Join(dir, ".env"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read network env file: %s", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "test=test-value\n", string(content))
|
||||
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ require (
|
|||
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
|
||||
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220703222411-e3cf664b39c6
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
|
||||
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
|
||||
|
|
|
@ -809,8 +809,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410 h1:LjxLd8UGR8ae73ov/vLrt/0jedj/nh98XnONkr8DJj8=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220703222411-e3cf664b39c6 h1:6VQZsYaJGfEq1LSKiNQ8HIW3olB04MpnW6HTnLnpMSQ=
|
||||
github.com/portainer/docker-compose-wrapper v0.0.0-20220703222411-e3cf664b39c6/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
|
||||
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
|
||||
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
|
||||
|
|
Loading…
Reference in New Issue