Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 29 additions & 10 deletions pkg/compose/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ func buildConfigContentPromptMessage(configs []string) string {

// loadUnresolvedFile loads a single compose file with interpolation and
// environment resolution skipped, so callers can inspect raw user-provided
// values. Used by both checkEnvironmentVariables and composeFileAsByteReader.
// values. Used by checkEnvironmentVariables.
func loadUnresolvedFile(ctx context.Context, project *types.Project, filePath string) (*types.Project, error) {
return loader.LoadWithContext(ctx, types.ConfigDetails{
dict, err := loader.LoadModelWithContext(ctx, types.ConfigDetails{
WorkingDir: project.WorkingDir,
Environment: project.Environment,
ConfigFiles: []types.ConfigFile{{Filename: filePath}},
Expand All @@ -651,6 +651,29 @@ func loadUnresolvedFile(ctx context.Context, project *types.Project, filePath st
options.SkipResolveEnvironment = true
options.Profiles = project.Profiles
})
if err != nil {
return nil, err
}

// Remove ports from services so loader.Transform doesn't fail on short-syntax
// ports containing variable references (which cannot be parsed into
// ServicePortConfig when interpolation is skipped). Callers of loadUnresolvedFile
// only inspect service environment, env_files, extends, and project configs.
if services, ok := dict["services"].(map[string]any); ok {
for _, s := range services {
if serviceMap, ok := s.(map[string]any); ok {
delete(serviceMap, "ports")
}
}
}

var p types.Project
p.WorkingDir = project.WorkingDir
p.Environment = project.Environment
if err := loader.Transform(dict, &p); err != nil {
return nil, err
}
return &p, nil
}

func envFileLayers(files map[string]string) []v1.Descriptor {
Expand Down Expand Up @@ -793,14 +816,10 @@ func scanFiles(scan secrets.Scanner, kind string, paths []string) ([]secrets.Det
return allFindings, nil
}

func composeFileAsByteReader(ctx context.Context, filePath string, project *types.Project) (io.Reader, error) {
base, err := loadUnresolvedFile(ctx, project, filePath)
func composeFileAsByteReader(_ context.Context, filePath string, _ *types.Project) (io.Reader, error) {
f, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to load compose file %s: %w", filePath, err)
}
in, err := base.MarshalYAML()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read compose file %s: %w", filePath, err)
}
return bytes.NewBuffer(in), nil
return bytes.NewReader(f), nil
}
66 changes: 66 additions & 0 deletions pkg/compose/publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,56 @@ services:
assert.Equal(t, len(envFiles), 1, "present optional env file should be added")
}

func Test_loadUnresolvedFile_short_port_mapping(t *testing.T) {
dir := t.TempDir()
composePath := filepath.Join(dir, "compose.yaml")
composeContent := `name: test
services:
whoami:
image: docker.io/traefik/whoami:v1.11
ports:
- ${DASHBOARD_PORT:-3000}:3000
- $PORT:80
- 8080:${TARGET_PORT:-8080}
environment:
API_KEY: "$ENV_KEY"
`
assert.NilError(t, os.WriteFile(composePath, []byte(composeContent), 0o600))

project := &types.Project{
WorkingDir: dir,
ComposeFiles: []string{composePath},
}

unresolved, err := loadUnresolvedFile(t.Context(), project, composePath)
assert.NilError(t, err)
assert.Assert(t, unresolved.Services["whoami"].Environment != nil)
assert.Equal(t, *unresolved.Services["whoami"].Environment["API_KEY"], "$ENV_KEY")
}

func Test_checkForSensitiveData_short_port_mapping(t *testing.T) {
dir := t.TempDir()
composePath := filepath.Join(dir, "compose.yaml")
composeContent := `name: test
services:
whoami:
image: docker.io/traefik/whoami:v1.11
ports:
- ${DASHBOARD_PORT:-3000}:3000
`
assert.NilError(t, os.WriteFile(composePath, []byte(composeContent), 0o600))

project := &types.Project{
WorkingDir: dir,
ComposeFiles: []string{composePath},
}

svc := &composeService{}
findings, err := svc.checkForSensitiveData(t.Context(), project)
assert.NilError(t, err)
assert.Equal(t, len(findings), 0)
}

func Test_checkForSensitiveData_optional_env_file_missing(t *testing.T) {
dir := t.TempDir()
project := &types.Project{
Expand Down Expand Up @@ -326,6 +376,22 @@ services:
environment:
DB_PASSWORD: "${DB_PASSWORD}"
API_KEY: "$API_KEY"
`,
},
},
{
name: "short-form port mapping with variable interpolation does not fail env check",
files: map[string]string{
"compose.yaml": `name: test
services:
whoami:
image: traefik/whoami:v1.11
ports:
- ${DASHBOARD_PORT:-3000}:3000
- $PORT:80
- 8080:${TARGET_PORT:-8080}
environment:
API_KEY: "$ENV_KEY"
`,
},
},
Expand Down