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
9 changes: 7 additions & 2 deletions internal/compose/custom_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ func coreEnvVars(cfg *config.Config, svc config.CustomService) map[string]string

// buildCustomService returns the service configuration for a user-defined
// custom service (CS_1..CS_10). Each custom service is built from a Dockerfile
// in ./services/{name}/ and exposes a single port with a /health endpoint.
// in ./services/{name}/ by default, or from CS_N_PATH when set.
func (g *Generator) buildCustomService(cs config.CustomService) ServiceConfig {
cfg := g.cfg

buildContext := cs.BuildPath
if buildContext == "" {
buildContext = fmt.Sprintf("./services/%s", cs.Name)
}

return ServiceConfig{
Build: &BuildConfig{
Context: fmt.Sprintf("./services/%s", cs.Name),
Context: buildContext,
Dockerfile: "Dockerfile",
},
ContainerName: fmt.Sprintf("%s_%s", cfg.ProjectName, cs.Name),
Expand Down
14 changes: 14 additions & 0 deletions internal/config/custom_services.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ func parseCustomServices() ([]CustomService, error) {
cs.TablePrefix = os.Getenv(fmt.Sprintf("CS_%d_TABLE_PREFIX", i))
cs.ExtraEnv = os.Getenv(fmt.Sprintf("CS_%d_ENV", i))

// Optional build context path override. Rejects absolute paths and
// path traversal so a misconfigured env can't escape the project root.
if p := os.Getenv(fmt.Sprintf("CS_%d_PATH", i)); p != "" {
if strings.HasPrefix(p, "/") {
return nil, fmt.Errorf("CS_%d_PATH must be a relative path, got %q", i, p)
}
for _, seg := range strings.Split(p, "/") {
if seg == ".." {
return nil, fmt.Errorf("CS_%d_PATH must not contain '..', got %q", i, p)
}
}
cs.BuildPath = p
}

// Override port/route if explicitly set
if p := getEnvInt(fmt.Sprintf("CS_%d_PORT", i), 0); p != 0 {
cs.Port = p
Expand Down
51 changes: 51 additions & 0 deletions internal/config/parse_services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,57 @@ func TestCustomServicesSanitization_PathTraversal(t *testing.T) {
}
}

// TestCustomServicesBuildPath_Valid verifies that CS_N_PATH sets BuildPath
// when the value is a clean relative path.
func TestCustomServicesBuildPath_Valid(t *testing.T) {
t.Setenv("CS_1", "myservice:go")
t.Setenv("CS_1_PATH", "./backend/services/myservice")
for i := 2; i <= 10; i++ {
t.Setenv("CS_"+itoa(i), "")
}

services, err := parseCustomServices()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(services) == 0 {
t.Fatal("expected at least one custom service")
}
if got := services[0].BuildPath; got != "./backend/services/myservice" {
t.Errorf("BuildPath = %q, want %q", got, "./backend/services/myservice")
}
}

// TestCustomServicesBuildPath_AbsoluteRejected verifies that an absolute path
// in CS_N_PATH is rejected.
func TestCustomServicesBuildPath_AbsoluteRejected(t *testing.T) {
t.Setenv("CS_1", "myservice:go")
t.Setenv("CS_1_PATH", "/etc/secrets")
for i := 2; i <= 10; i++ {
t.Setenv("CS_"+itoa(i), "")
}

_, err := parseCustomServices()
if err == nil {
t.Fatal("expected error for absolute CS_N_PATH, got nil")
}
}

// TestCustomServicesBuildPath_TraversalRejected verifies that a path containing
// '..' in CS_N_PATH is rejected.
func TestCustomServicesBuildPath_TraversalRejected(t *testing.T) {
t.Setenv("CS_1", "myservice:go")
t.Setenv("CS_1_PATH", "../../outside")
for i := 2; i <= 10; i++ {
t.Setenv("CS_"+itoa(i), "")
}

_, err := parseCustomServices()
if err == nil {
t.Fatal("expected error for traversal CS_N_PATH, got nil")
}
}

// ---------------------------------------------------------------------------
// T07 — Frontend apps sanitization
// ---------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,7 @@ type CustomService struct {
CPU string
TablePrefix string // CS_N_TABLE_PREFIX
ExtraEnv string // CS_N_ENV (raw key=val pairs, comma-separated)
BuildPath string // CS_N_PATH: overrides default ./services/{name} build context
}

// FrontendApp represents a frontend application (FRONTEND_APP_1..FRONTEND_APP_20).
Expand Down
Loading