mirror of
https://github.com/docker/compose.git
synced 2026-08-27 03:45:29 +00:00
Handle rawsetenv collisions with overwrite and warning
rawsetenv injects provider variables without the service-name prefix, so a key can collide with a value already set on the dependent service, whether declared by the user in environment or emitted by another provider. Log a warning and overwrite on collision, document the precedence and the non-deterministic ordering between concurrent providers, and cover the user-environment override with an e2e test. Signed-off-by: Yohta Kimura <38206553+rajyan@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
332e0add14
commit
5c9d611b5f
4 changed files with 55 additions and 12 deletions
|
|
@ -106,9 +106,14 @@ When the provider command sends a `rawsetenv` JSON message, Compose injects the
|
|||
```
|
||||
The `app` service will receive `SECRET_KEY` exactly as specified, regardless of the provider service name.
|
||||
This is useful when injecting secrets or configuration values that must match exact variable names expected by
|
||||
applications or frameworks. Unlike `setenv`, which avoids collisions through automatic prefixing, `rawsetenv` keys
|
||||
are the provider's responsibility to keep unique. If multiple providers emit the same `rawsetenv` key, the last one
|
||||
to run will overwrite previous values.
|
||||
applications or frameworks.
|
||||
|
||||
Unlike `setenv`, which avoids collisions through automatic prefixing, `rawsetenv` keys are the provider's
|
||||
responsibility to keep unique. If a `rawsetenv` key collides with a variable already set on the dependent service,
|
||||
the existing value is overwritten and Compose logs a warning. This includes variables declared by the user in the
|
||||
service `environment` section as well as values emitted by other providers. Providers that are not linked by a
|
||||
`depends_on` relationship may run concurrently, so when several of them emit the same `rawsetenv` key the resulting
|
||||
value is not deterministic.
|
||||
|
||||
> __Note:__ The `compose up` provider command _MUST_ be idempotent. If resource is already running, the command _MUST_ set
|
||||
> the same environment variables to ensure consistent configuration of dependent services.
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
|||
return nil
|
||||
}
|
||||
|
||||
vars, err := s.executePlugin(cmd, command, service)
|
||||
variables, err := s.executePlugin(cmd, command, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -90,10 +90,13 @@ func (s *composeService) runPlugin(ctx context.Context, project *types.Project,
|
|||
for name, s := range project.Services {
|
||||
if _, ok := s.DependsOn[service.Name]; ok {
|
||||
prefix := strings.ToUpper(service.Name) + "_"
|
||||
for key, val := range vars.prefixed {
|
||||
for key, val := range variables.prefixed {
|
||||
s.Environment[prefix+key] = &val
|
||||
}
|
||||
for key, val := range vars.raw {
|
||||
for key, val := range variables.raw {
|
||||
if existing, ok := s.Environment[key]; ok && existing != nil && *existing != val {
|
||||
logrus.Warnf("provider %q overrides environment variable %q in service %q", service.Name, key, name)
|
||||
}
|
||||
s.Environment[key] = &val
|
||||
}
|
||||
project.Services[name] = s
|
||||
|
|
@ -131,7 +134,7 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
|||
decoder := json.NewDecoder(stdout)
|
||||
defer func() { _ = stdout.Close() }()
|
||||
|
||||
vars := pluginVariables{
|
||||
variables := pluginVariables{
|
||||
prefixed: types.Mapping{},
|
||||
raw: types.Mapping{},
|
||||
}
|
||||
|
|
@ -156,13 +159,13 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
|||
if !found {
|
||||
return pluginVariables{}, fmt.Errorf("invalid response from plugin: %s", msg.Message)
|
||||
}
|
||||
vars.prefixed[key] = val
|
||||
variables.prefixed[key] = val
|
||||
case RawSetEnvType:
|
||||
key, val, found := strings.Cut(msg.Message, "=")
|
||||
if !found {
|
||||
return pluginVariables{}, fmt.Errorf("invalid response from plugin: %s", msg.Message)
|
||||
}
|
||||
vars.raw[key] = val
|
||||
variables.raw[key] = val
|
||||
case DebugType:
|
||||
logrus.Debugf("%s: %s", service.Name, msg.Message)
|
||||
default:
|
||||
|
|
@ -183,7 +186,7 @@ func (s *composeService) executePlugin(cmd *exec.Cmd, command string, service ty
|
|||
case "stop":
|
||||
s.events.On(stoppedEvent(service.Name))
|
||||
}
|
||||
return vars, nil
|
||||
return variables, nil
|
||||
}
|
||||
|
||||
func (s *composeService) getPluginBinaryPath(provider string) (path string, err error) {
|
||||
|
|
|
|||
15
pkg/e2e/fixtures/providers/rawsetenv-override.yaml
Normal file
15
pkg/e2e/fixtures/providers/rawsetenv-override.yaml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
services:
|
||||
test:
|
||||
image: alpine
|
||||
command: env
|
||||
environment:
|
||||
CLOUD_REGION: user-defined-region
|
||||
depends_on:
|
||||
- secrets
|
||||
secrets:
|
||||
provider:
|
||||
type: example-provider
|
||||
options:
|
||||
name: secrets
|
||||
type: test1
|
||||
size: 1
|
||||
|
|
@ -76,7 +76,6 @@ func TestDependsOnMultipleProviders(t *testing.T) {
|
|||
env := getEnv(res.Combined())
|
||||
assert.Check(t, slices.Contains(env, "PROVIDER1_URL=https://magic.cloud/provider1"), env)
|
||||
assert.Check(t, slices.Contains(env, "PROVIDER2_URL=https://magic.cloud/provider2"), env)
|
||||
assert.Check(t, slices.Contains(env, "CLOUD_REGION=us-east-1"), env)
|
||||
}
|
||||
|
||||
func TestProviderRawSetEnv(t *testing.T) {
|
||||
|
|
@ -92,13 +91,34 @@ func TestProviderRawSetEnv(t *testing.T) {
|
|||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/providers/rawsetenv.yaml", "--project-name", projectName, "up")
|
||||
res.Assert(t, icmd.Success)
|
||||
env := getEnv(res.Combined(), false)
|
||||
env := getEnv(res.Combined())
|
||||
// setenv: prefixed with service name
|
||||
assert.Check(t, slices.Contains(env, "SECRETS_URL=https://magic.cloud/secrets"), env)
|
||||
// rawsetenv: injected as-is without prefix
|
||||
assert.Check(t, slices.Contains(env, "CLOUD_REGION=us-east-1"), env)
|
||||
}
|
||||
|
||||
func TestProviderRawSetEnvOverridesUserEnv(t *testing.T) {
|
||||
provider, err := findExecutable("example-provider")
|
||||
assert.NilError(t, err)
|
||||
|
||||
path := fmt.Sprintf("%s%s%s", os.Getenv("PATH"), string(os.PathListSeparator), filepath.Dir(provider))
|
||||
c := NewParallelCLI(t, WithEnv("PATH="+path))
|
||||
const projectName = "rawsetenv-override"
|
||||
t.Cleanup(func() {
|
||||
c.cleanupWithDown(t, projectName)
|
||||
})
|
||||
|
||||
res := c.RunDockerComposeCmd(t, "-f", "fixtures/providers/rawsetenv-override.yaml", "--project-name", projectName, "up")
|
||||
res.Assert(t, icmd.Success)
|
||||
env := getEnv(res.Combined())
|
||||
// rawsetenv overrides a user-defined environment variable
|
||||
assert.Check(t, slices.Contains(env, "CLOUD_REGION=us-east-1"), env)
|
||||
assert.Check(t, !slices.Contains(env, "CLOUD_REGION=user-defined-region"), env)
|
||||
// the override is surfaced to the user rather than happening silently
|
||||
assert.Check(t, strings.Contains(res.Combined(), "overrides environment variable"), res.Combined())
|
||||
}
|
||||
|
||||
func getEnv(out string) []string {
|
||||
var env []string
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue