1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-19 05:19:39 +02:00

feat(libstack): expose env vars with PORTAINER_ prefix [BE-11661] (#687)

This commit is contained in:
Devon Steenberg 2025-05-12 11:18:04 +12:00 committed by GitHub
parent 9fdc535d6b
commit 1abdf42f99
3 changed files with 232 additions and 0 deletions

View file

@ -87,6 +87,11 @@ export function EdgeScriptSettingsFieldset({
/>
</FormControl>
<TextTip color="orange" className="mb-2 icon-orange">
For security purposes, only environment variables prefixed with
&apos;PORTAINER_&apos; will be accessible.
</TextTip>
<div className="form-group">
<div className="col-sm-12">
<SwitchField

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"slices"
"strconv"
@ -29,6 +30,8 @@ import (
const PortainerEdgeStackLabel = "io.portainer.edge_stack_id"
const portainerEnvVarsPrefix = "PORTAINER_"
var mu sync.Mutex
func init() {
@ -322,11 +325,19 @@ func createProject(ctx context.Context, configFilepaths []string, options libsta
envFiles = append(envFiles, options.EnvFilePath)
}
var osPortainerEnvVars []string
for _, ev := range os.Environ() {
if strings.HasPrefix(ev, portainerEnvVarsPrefix) {
osPortainerEnvVars = append(osPortainerEnvVars, ev)
}
}
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(osPortainerEnvVars),
cli.WithEnv(options.Env),
cli.WithEnvFiles(envFiles...),
func(o *cli.ProjectOptions) error {

View file

@ -719,6 +719,7 @@ func Test_createProject(t *testing.T) {
filesToCreate map[string]string
configFilepaths []string
options libstack.Options
osEnv map[string]string
expectedProject *types.Project
}{
{
@ -1049,6 +1050,217 @@ func Test_createProject(t *testing.T) {
},
expectedProject: expectedSimpleComposeProject("/project-dir", nil),
},
{
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{""},
},
},
}
for _, tc := range testcases {
@ -1070,6 +1282,10 @@ func Test_createProject(t *testing.T) {
}
}()
for k, v := range tc.osEnv {
t.Setenv(k, v)
}
gotProject, err := createProject(ctx, tc.configFilepaths, tc.options)
if err != nil {
t.Fatalf("Failed to create new project: %v", err)