diff --git a/cmd/magebox/domain.go b/cmd/magebox/domain.go index 5f4b687..5da1dd1 100644 --- a/cmd/magebox/domain.go +++ b/cmd/magebox/domain.go @@ -53,12 +53,14 @@ var domainListCmd = &cobra.Command{ var ( domainStoreCode string + domainStoreType string domainRoot string domainSSL bool ) func init() { - domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store code (default: \"default\")") + domainAddCmd.Flags().StringVar(&domainStoreCode, "store-code", "", "Magento store/website code for multi-store setup") + domainAddCmd.Flags().StringVar(&domainStoreType, "store-type", "", "Magento run type: \"store\" or \"website\" (default: \"store\")") domainAddCmd.Flags().StringVar(&domainRoot, "root", "", "Document root relative to project (default: \"pub\" for Magento, \"public\" for Laravel)") domainAddCmd.Flags().BoolVar(&domainSSL, "ssl", true, "Enable SSL for the domain") @@ -92,9 +94,10 @@ func runDomainAdd(cmd *cobra.Command, args []string) error { // Create new domain newDomain := config.Domain{ - Host: host, - Root: domainRoot, - MageRunCode: domainStoreCode, + Host: host, + Root: domainRoot, + StoreCode: domainStoreCode, + StoreType: domainStoreType, } // Only set SSL if explicitly changed from default (true) @@ -163,7 +166,9 @@ func runDomainAdd(cmd *cobra.Command, args []string) error { } fmt.Println() - cli.PrintInfo("Domain %s configured with store code: %s", host, newDomain.GetStoreCode()) + if newDomain.StoreCode != "" { + cli.PrintInfo("Domain %s configured with store code: %s (%s)", host, newDomain.StoreCode, newDomain.GetStoreType()) + } return nil } @@ -284,7 +289,9 @@ func runDomainList(cmd *cobra.Command, args []string) error { fmt.Printf(" %s\n", cli.Highlight(d.Host)) fmt.Printf(" URL: %s://%s\n", protocol, d.Host) fmt.Printf(" Root: %s\n", d.GetRoot()) - fmt.Printf(" Store Code: %s\n", d.GetStoreCode()) + if d.StoreCode != "" { + fmt.Printf(" Store Code: %s (%s)\n", d.StoreCode, d.GetStoreType()) + } fmt.Printf(" SSL: %s\n", sslStatus) fmt.Println() } diff --git a/internal/config/types.go b/internal/config/types.go index ca4ea08..c1aa867 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -173,11 +173,11 @@ func (c *Command) UnmarshalYAML(unmarshal func(interface{}) error) error { // Domain represents a domain configuration type Domain struct { - Host string `yaml:"host"` - Root string `yaml:"root,omitempty"` - SSL *bool `yaml:"ssl,omitempty"` - MageRunCode string `yaml:"mage_run_code,omitempty"` // Magento store/website code for multi-store setup - MageRunType string `yaml:"mage_run_type,omitempty"` // "store" or "website" (default: "store") + Host string `yaml:"host"` + Root string `yaml:"root,omitempty"` + SSL *bool `yaml:"ssl,omitempty"` + StoreCode string `yaml:"store_code,omitempty"` // Magento store/website code for multi-store setup + StoreType string `yaml:"store_type,omitempty"` // "store" or "website" (default: "store") } // Services represents the services configuration @@ -320,20 +320,22 @@ func (d *Domain) IsSSLEnabled() bool { return *d.SSL } -// GetStoreCode returns the Magento store code, defaulting to "default" -func (d *Domain) GetStoreCode() string { - if d.MageRunCode == "" { - return "default" +// GetStoreType returns the Magento run type, defaulting to "store" +func (d *Domain) GetStoreType() string { + if d.StoreType == "" { + return "store" } - return d.MageRunCode + return d.StoreType } -// GetMageRunType returns the Magento run type, defaulting to "store" -func (d *Domain) GetMageRunType() string { - if d.MageRunType == "" { - return "store" +// HasMultiStore returns true if any domain has a store code configured +func (c *Config) HasMultiStore() bool { + for _, d := range c.Domains { + if d.StoreCode != "" { + return true + } } - return d.MageRunType + return false } // Validate checks if the configuration is valid diff --git a/internal/lib/templates.go b/internal/lib/templates.go index e357883..d665cc6 100644 --- a/internal/lib/templates.go +++ b/internal/lib/templates.go @@ -100,6 +100,7 @@ var TemplateNames = map[string][]string{ "vhost.conf.tmpl", "proxy.conf.tmpl", "upstream.conf.tmpl", + "map.conf.tmpl", }, TemplatePHP: { "pool.conf.tmpl", diff --git a/internal/nginx/templates/map.conf.tmpl b/internal/nginx/templates/map.conf.tmpl new file mode 100644 index 0000000..6d1a360 --- /dev/null +++ b/internal/nginx/templates/map.conf.tmpl @@ -0,0 +1,17 @@ +# MageBox multi-store map for {{.ProjectName}} +# Generated from store_code settings in domains configuration +# Do not edit manually - regenerated on magebox start + +map $host $MAGE_RUN_CODE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.StoreCode}}; +{{- end}}{{end}} +} + +map $host $MAGE_RUN_TYPE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.GetStoreType}}; +{{- end}}{{end}} +} diff --git a/internal/nginx/templates/vhost.conf.tmpl b/internal/nginx/templates/vhost.conf.tmpl index d4e494a..b83babf 100644 --- a/internal/nginx/templates/vhost.conf.tmpl +++ b/internal/nginx/templates/vhost.conf.tmpl @@ -106,8 +106,6 @@ server { set $MAGE_ROOT {{.DocumentRoot}}; set $MAGE_MODE developer; - set $MAGE_RUN_CODE {{.StoreCode}}; - set $MAGE_RUN_TYPE {{.MageRunType}}; root $MAGE_ROOT; index index.php; @@ -221,8 +219,10 @@ server { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE; - fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE; +{{- if .HasStoreCodes}} + fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty; + fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty; +{{- end}} include fastcgi_params; } diff --git a/internal/nginx/vhost.go b/internal/nginx/vhost.go index 8fd0592..17dc991 100644 --- a/internal/nginx/vhost.go +++ b/internal/nginx/vhost.go @@ -29,12 +29,16 @@ var proxyTemplateEmbed string //go:embed templates/upstream.conf.tmpl var upstreamTemplateEmbed string +//go:embed templates/map.conf.tmpl +var mapTemplateEmbed string + func init() { // Register embedded templates as fallbacks lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost.conf.tmpl", vhostTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "vhost-laravel.conf.tmpl", vhostLaravelTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "proxy.conf.tmpl", proxyTemplateEmbed) lib.RegisterFallbackTemplate(lib.TemplateNginx, "upstream.conf.tmpl", upstreamTemplateEmbed) + lib.RegisterFallbackTemplate(lib.TemplateNginx, "map.conf.tmpl", mapTemplateEmbed) } // Template variables available in vhost.conf.tmpl: @@ -74,13 +78,18 @@ type VhostConfig struct { HTTPSPort int // 443 on Linux, 8443 on macOS (port forwarding) BackendPort int // Backend port for Varnish (always 8080 when Varnish enabled) EnableIPv6 bool // true on Linux to add [::]:port listen directives - StoreCode string // Magento store code for multi-store setup (default: "default") - MageRunType string // Magento run type: "store" or "website" (default: "store") + HasStoreCodes bool // True when any domain has a store_code; enables MAGE_RUN_* fastcgi params AccessLog string // Path to access log file ErrorLog string // Path to error log file CustomNginxDir string // Path to project-level custom nginx snippets directory (if it exists) } +// MapConfig contains data needed to generate the multi-store map config +type MapConfig struct { + ProjectName string + Domains []config.Domain +} + // ProxyConfig contains data needed to generate a proxy vhost type ProxyConfig struct { Name string @@ -132,6 +141,18 @@ func (g *VhostGenerator) Generate(cfg *config.Config, projectPath string) error return fmt.Errorf("failed to generate upstream config: %w", err) } + // Generate or remove the multi-store map config depending on whether any domain has a store_code + hasStoreCodes := cfg.HasMultiStore() + mapFile := filepath.Join(g.vhostsDir, fmt.Sprintf("%s-map.conf", cfg.Name)) + if hasStoreCodes { + mapCfg := MapConfig{ProjectName: cfg.Name, Domains: cfg.Domains} + if err := g.generateMap(mapCfg, mapFile); err != nil { + return fmt.Errorf("failed to generate map config: %w", err) + } + } else { + os.Remove(mapFile) // clean up stale map file if store codes were removed + } + // Determine ports based on platform // macOS uses port forwarding (80->8080, 443->8443), Linux uses standard ports httpPort := 80 @@ -167,8 +188,7 @@ func (g *VhostGenerator) Generate(cfg *config.Config, projectPath string) error HTTPSPort: httpsPort, BackendPort: backendPort, EnableIPv6: enableIPv6, - StoreCode: domain.GetStoreCode(), - MageRunType: domain.GetMageRunType(), + HasStoreCodes: hasStoreCodes, AccessLog: filepath.Join(logsDir, fmt.Sprintf("%s-access.log", sanitizedDomain)), ErrorLog: filepath.Join(logsDir, fmt.Sprintf("%s-error.log", sanitizedDomain)), } @@ -269,6 +289,35 @@ func (g *VhostGenerator) renderVhost(cfg VhostConfig) (string, error) { return buf.String(), nil } +// generateMap generates the multi-store map config file +func (g *VhostGenerator) generateMap(cfg MapConfig, destFile string) error { + content, err := g.renderMap(cfg) + if err != nil { + return err + } + return os.WriteFile(destFile, []byte(content), 0644) +} + +// renderMap renders the map template +func (g *VhostGenerator) renderMap(cfg MapConfig) (string, error) { + tmplContent, err := lib.GetTemplate(lib.TemplateNginx, "map.conf.tmpl") + if err != nil { + return "", err + } + + tmpl, err := template.New("map").Parse(tmplContent) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, cfg); err != nil { + return "", err + } + + return buf.String(), nil +} + // generateUpstream generates the upstream config file for a project func (g *VhostGenerator) generateUpstream(cfg UpstreamConfig) error { content, err := g.renderUpstream(cfg) diff --git a/internal/nginx/vhost_test.go b/internal/nginx/vhost_test.go index 886c9f0..2623d10 100644 --- a/internal/nginx/vhost_test.go +++ b/internal/nginx/vhost_test.go @@ -287,6 +287,163 @@ func TestRenderVhost_SSLEnabled_NoIPv6(t *testing.T) { } } +func TestRenderVhost_NoStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + cfg := VhostConfig{ + ProjectName: "mystore", + Domain: "mystore.test", + DocumentRoot: "/var/www/mystore/pub", + PHPVersion: "8.2", + PHPSocketPath: filepath.Join(tmpDir, ".magebox", "run", "mystore-php8.2.sock"), + HasStoreCodes: false, + HTTPPort: 80, + HTTPSPort: 443, + } + + content, err := g.renderVhost(cfg) + if err != nil { + t.Fatalf("renderVhost failed: %v", err) + } + + if strings.Contains(content, "MAGE_RUN_CODE") { + t.Error("Vhost without store codes should not contain MAGE_RUN_CODE") + } + if strings.Contains(content, "MAGE_RUN_TYPE") { + t.Error("Vhost without store codes should not contain MAGE_RUN_TYPE") + } + if strings.Contains(content, "set $MAGE_RUN") { + t.Error("Vhost without store codes should not contain set $MAGE_RUN_* directives") + } +} + +func TestRenderVhost_WithStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + cfg := VhostConfig{ + ProjectName: "mystore", + Domain: "mystore.test", + DocumentRoot: "/var/www/mystore/pub", + PHPVersion: "8.2", + PHPSocketPath: filepath.Join(tmpDir, ".magebox", "run", "mystore-php8.2.sock"), + HasStoreCodes: true, + HTTPPort: 80, + HTTPSPort: 443, + } + + content, err := g.renderVhost(cfg) + if err != nil { + t.Fatalf("renderVhost failed: %v", err) + } + + checks := []string{ + "fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty", + "fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty", + } + for _, check := range checks { + if !strings.Contains(content, check) { + t.Errorf("Vhost with store codes should contain %q", check) + } + } + // map-based setup: no set directives in the server block + if strings.Contains(content, "set $MAGE_RUN_CODE") { + t.Error("Vhost should not use set $MAGE_RUN_CODE (handled by map block)") + } +} + +func TestRenderMap(t *testing.T) { + g, _ := setupTestGenerator(t) + + cfg := MapConfig{ + ProjectName: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", StoreCode: "default"}, + {Host: "de.mystore.test", StoreCode: "german"}, + {Host: "fr.mystore.test", StoreCode: "french", StoreType: "website"}, + {Host: "admin.mystore.test"}, // no store code — should be omitted + }, + } + + content, err := g.renderMap(cfg) + if err != nil { + t.Fatalf("renderMap failed: %v", err) + } + + checks := []string{ + "map $host $MAGE_RUN_CODE", + "map $host $MAGE_RUN_TYPE", + "hostnames;", + ".mystore.test default", + ".de.mystore.test german", + ".fr.mystore.test french", + ".fr.mystore.test website", + } + for _, check := range checks { + if !strings.Contains(content, check) { + t.Errorf("Map content should contain %q\nGot:\n%s", check, content) + } + } + + // Domains without store code should not appear in the map + if strings.Contains(content, "admin.mystore.test") { + t.Error("Domain without store code should not appear in map") + } + // Default store type should be "store" + if !strings.Contains(content, ".mystore.test store") { + t.Error("Domain with no store_type should default to store in MAGE_RUN_TYPE map") + } +} + +func TestGenerate_CreatesMapFileWhenStoreCodesPresent(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + projectPath := filepath.Join(tmpDir, "projects", "mystore") + cfg := &config.Config{ + Name: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", Root: "pub", StoreCode: "default"}, + {Host: "de.mystore.test", Root: "pub", StoreCode: "german"}, + }, + PHP: "8.2", + } + + if err := g.Generate(cfg, projectPath); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") + if _, err := os.Stat(mapFile); os.IsNotExist(err) { + t.Error("Map file should have been created when store codes are present") + } + + content, _ := os.ReadFile(mapFile) + if !strings.Contains(string(content), "map $host $MAGE_RUN_CODE") { + t.Error("Map file should contain MAGE_RUN_CODE map block") + } +} + +func TestGenerate_NoMapFileWithoutStoreCodes(t *testing.T) { + g, tmpDir := setupTestGenerator(t) + + projectPath := filepath.Join(tmpDir, "projects", "mystore") + cfg := &config.Config{ + Name: "mystore", + Domains: []config.Domain{ + {Host: "mystore.test", Root: "pub"}, + }, + PHP: "8.2", + } + + if err := g.Generate(cfg, projectPath); err != nil { + t.Fatalf("Generate failed: %v", err) + } + + mapFile := filepath.Join(g.vhostsDir, "mystore-map.conf") + if _, err := os.Stat(mapFile); !os.IsNotExist(err) { + t.Error("Map file should NOT be created when no store codes are configured") + } +} + func TestRenderVhost_SSLDisabled(t *testing.T) { g, tmpDir := setupTestGenerator(t) diff --git a/lib/templates/nginx/map.conf.tmpl b/lib/templates/nginx/map.conf.tmpl new file mode 100644 index 0000000..6d1a360 --- /dev/null +++ b/lib/templates/nginx/map.conf.tmpl @@ -0,0 +1,17 @@ +# MageBox multi-store map for {{.ProjectName}} +# Generated from store_code settings in domains configuration +# Do not edit manually - regenerated on magebox start + +map $host $MAGE_RUN_CODE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.StoreCode}}; +{{- end}}{{end}} +} + +map $host $MAGE_RUN_TYPE { + hostnames; +{{- range .Domains}}{{if .StoreCode}} + .{{.Host}} {{.GetStoreType}}; +{{- end}}{{end}} +} diff --git a/lib/templates/nginx/vhost.conf.tmpl b/lib/templates/nginx/vhost.conf.tmpl index d4e494a..b83babf 100644 --- a/lib/templates/nginx/vhost.conf.tmpl +++ b/lib/templates/nginx/vhost.conf.tmpl @@ -106,8 +106,6 @@ server { set $MAGE_ROOT {{.DocumentRoot}}; set $MAGE_MODE developer; - set $MAGE_RUN_CODE {{.StoreCode}}; - set $MAGE_RUN_TYPE {{.MageRunType}}; root $MAGE_ROOT; index index.php; @@ -221,8 +219,10 @@ server { fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE; - fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE; +{{- if .HasStoreCodes}} + fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE if_not_empty; + fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE if_not_empty; +{{- end}} include fastcgi_params; }