-
Notifications
You must be signed in to change notification settings - Fork 61
feat(proxy): run multiple local shops in parallel behind a shared proxy #1208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
49032c8
fffb28a
bcf7d19
beb27cb
cce71d4
f7661be
194d5e3
6e79b88
f4d9dfd
ad2e282
4f6e902
68114f3
cd43959
c635a54
ff45ff8
0e75e36
0599705
98a3ceb
e96201c
5750952
fdd9492
76ca1fc
3ef61c0
cb170e6
02c58f4
c26ca18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| package project | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| "github.com/shyim/go-composer/repository" | ||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/shopware/shopware-cli/internal/proxy" | ||
| "github.com/shopware/shopware-cli/internal/shop" | ||
| "github.com/shopware/shopware-cli/internal/system" | ||
| "github.com/shopware/shopware-cli/internal/tui" | ||
|
|
@@ -46,6 +51,14 @@ type createOptions struct { | |
| withElasticsearch bool | ||
| withAMQP bool | ||
| noAudit bool | ||
| // useLocalDomain serves the shop at a stable hostname | ||
| // (<name>.<baseDomain>) through the shared proxy instead of a fixed port. | ||
| // Only meaningful with Docker. | ||
| useLocalDomain bool | ||
| // setupProxyNow runs the one-time machine setup (DNS + HTTPS trust, needs | ||
| // sudo) inline during create, so the local domain works immediately. Set | ||
| // only when the user opts in and the machine is not configured yet. | ||
| setupProxyNow bool | ||
|
|
||
| interactive bool | ||
| elasticsearchExplicit bool | ||
|
|
@@ -66,6 +79,35 @@ func (o *createOptions) clearPHP() { | |
| } | ||
| } | ||
|
|
||
| // localDomainHostname returns the stable proxy hostname for a project name, | ||
| // e.g. "my-shop.shopware.local". Underscores (valid in a project name but not | ||
| // in a hostname) become dashes. | ||
| func localDomainHostname(name, baseDomain string) string { | ||
| label := strings.ReplaceAll(filepath.Base(name), "_", "-") | ||
| return label + "." + baseDomain | ||
| } | ||
|
Comment on lines
+82
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Malformed hostname when the project folder is
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| // proxyBaseDomain returns the machine-wide proxy base domain, falling back to | ||
| // the default when no settings are stored yet. | ||
| func proxyBaseDomain() string { | ||
| if s, err := proxy.LoadSettings(); err == nil { | ||
| return s.BaseDomain() | ||
| } | ||
| return proxy.DefaultDomain | ||
| } | ||
|
|
||
| // resolveLocalDomainChoice derives the final local-domain settings from the | ||
| // individual inputs. Local domains require Docker, so useLocalDomain is always | ||
| // gated on useDocker regardless of how the choice was made (interactive or the | ||
| // --local-domain flag). setupProxyNow — which triggers the one-time sudo setup | ||
| // inline — is only ever true when the choice came from the interactive prompt | ||
| // (promptShown), so passing --local-domain never runs sudo without asking. | ||
| func resolveLocalDomainChoice(useDocker, wantLocalDomain, promptShown, machineSetupDone, setupNowAnswer bool) (useLocalDomain, setupProxyNow bool) { | ||
| useLocalDomain = useDocker && wantLocalDomain | ||
| setupProxyNow = useLocalDomain && promptShown && !machineSetupDone && setupNowAnswer | ||
| return useLocalDomain, setupProxyNow | ||
| } | ||
|
|
||
| var projectCreateCmd = &cobra.Command{ | ||
| Use: "create [name] [version]", | ||
| Short: "Create a new Shopware 6 project", | ||
|
|
@@ -136,6 +178,16 @@ var projectCreateCmd = &cobra.Command{ | |
| return err | ||
| } | ||
|
|
||
| // Do the one-time machine setup up front (while the user is still at the | ||
| // keyboard for the sudo prompt), before the long composer install. It is | ||
| // best-effort: a blocked/declined sudo just means the domain resolves | ||
| // once the user runs `project proxy setup` later. | ||
| if opts.setupProxyNow { | ||
| fmt.Println() | ||
| fmt.Println(tui.BoldText.Render("Setting up local domains (one-time, needs sudo)")) | ||
| _ = runInlineProxySetup(cmd.Context(), proxyBaseDomain()) | ||
| } | ||
|
|
||
| if err := scaffoldProject(cmd.Context(), &opts, chosenVersion); err != nil { | ||
| return err | ||
| } | ||
|
|
@@ -150,6 +202,7 @@ func parseCreateFlags(cmd *cobra.Command, args []string) createOptions { | |
| withAMQP, _ := cmd.PersistentFlags().GetBool("with-amqp") | ||
| noAudit, _ := cmd.PersistentFlags().GetBool("no-audit") | ||
| initGit, _ := cmd.PersistentFlags().GetBool("git") | ||
| localDomain, _ := cmd.PersistentFlags().GetBool("local-domain") | ||
| versionFlag, _ := cmd.PersistentFlags().GetString("version") | ||
| deploymentMethod, _ := cmd.PersistentFlags().GetString("deployment") | ||
| ciSystem, _ := cmd.PersistentFlags().GetString("ci") | ||
|
|
@@ -169,6 +222,7 @@ func parseCreateFlags(cmd *cobra.Command, args []string) createOptions { | |
| withAMQP: withAMQP, | ||
| noAudit: noAudit, | ||
| initGit: initGit, | ||
| useLocalDomain: localDomain, | ||
| selectedVersion: versionFlag, | ||
| selectedDeployment: deploymentMethod, | ||
| selectedCI: ciSystem, | ||
|
|
@@ -205,6 +259,10 @@ func applyNonInteractiveDefaults(opts *createOptions) error { | |
| if !opts.elasticsearchExplicit { | ||
| opts.withElasticsearch = true | ||
| } | ||
| // Local domains need Docker; drop the flag if Docker is off. Never run the | ||
| // one-time sudo setup non-interactively. | ||
| opts.useLocalDomain = opts.useDocker && opts.useLocalDomain | ||
| opts.setupProxyNow = false | ||
| return nil | ||
| } | ||
|
|
||
|
|
@@ -217,6 +275,7 @@ func init() { | |
| projectCreateCmd.PersistentFlags().Bool("with-amqp", false, "Include AMQP queue support (symfony/amqp-messenger)") | ||
| projectCreateCmd.PersistentFlags().Bool("no-audit", false, "Disable composer audit blocking insecure packages") | ||
| projectCreateCmd.PersistentFlags().Bool("git", false, "Initialize a Git repository") | ||
| projectCreateCmd.PersistentFlags().Bool("local-domain", false, "Serve the shop at a stable local hostname (<name>.shopware.local) via the shared proxy instead of a port (requires Docker)") | ||
| projectCreateCmd.PersistentFlags().String("version", "", "Shopware version to install (e.g., 6.6.0.0, latest)") | ||
| projectCreateCmd.PersistentFlags().String("deployment", "", "Deployment method: none, deployer, platformsh, shopware-paas") | ||
| projectCreateCmd.PersistentFlags().String("ci", "", "CI/CD system: none, github, gitlab") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ import ( | |
| "github.com/shyim/go-version" | ||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/shopware/shopware-cli/internal/proxy" | ||
| "github.com/shopware/shopware-cli/internal/shop" | ||
| "github.com/shopware/shopware-cli/internal/system" | ||
| "github.com/shopware/shopware-cli/internal/tui" | ||
|
|
@@ -72,6 +73,14 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor | |
| selectElasticsearch := tui.No | ||
| selectAMQP := tui.Yes | ||
|
|
||
| baseDomain := proxyBaseDomain() | ||
| // Default to the stable hostname (recommended); only applies with Docker. | ||
| selectLocalDomain := true | ||
| // Whether this machine already resolves the proxy domain. When it does, the | ||
| // one-time sudo setup is already done, so we never ask for it again. | ||
| machineSetupDone := proxy.CheckResolverConfigured(baseDomain).Configured | ||
| selectSetupNow := tui.Yes | ||
|
|
||
| if !system.IsGitInstalled() { | ||
| selectGit = tui.No | ||
| } | ||
|
|
@@ -196,6 +205,55 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor | |
| )) | ||
| } | ||
|
|
||
| if !cmd.PersistentFlags().Changed("local-domain") { | ||
|
Comment on lines
207
to
+208
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you tested also headless usage aka --no-interaction with those flags and without?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc ngocblue FYI |
||
| formGroups = append(formGroups, huh.NewGroup( | ||
| huh.NewSelect[bool](). | ||
| Title("Local domains"). | ||
| Description("Reach this shop at a stable hostname instead of a changing port"). | ||
| OptionsFunc(func() []huh.Option[bool] { | ||
| host := "<name>." + baseDomain | ||
| if opts.projectFolder != "" { | ||
| host = localDomainHostname(opts.projectFolder, baseDomain) | ||
| } | ||
| return []huh.Option[bool]{ | ||
| huh.NewOption("Yes (recommended) — https://"+host, true), | ||
| huh.NewOption("No — use a port (http://localhost:8000)", false), | ||
| } | ||
| }, &opts.projectFolder). | ||
| Value(&selectLocalDomain), | ||
| // The shared proxy is Docker-only, so this choice is irrelevant | ||
| // without Docker (respecting a --docker flag override). | ||
| ).WithHideFunc(func() bool { | ||
| if cmd.PersistentFlags().Changed("docker") { | ||
| return !opts.useDocker | ||
| } | ||
| return selectDocker != tui.Yes | ||
| })) | ||
|
|
||
| // Offer the one-time machine setup inline, but only when it is | ||
| // actually needed: local domains chosen, Docker on, and the machine | ||
| // not configured yet. Every later project skips this automatically. | ||
| formGroups = append(formGroups, huh.NewGroup( | ||
| tui.NewYesNo(). | ||
| Title("Set up local domains on this machine now?"). | ||
| Description("One-time sudo: makes *."+baseDomain+" resolve and trusts its HTTPS certificate. Skip to run `shopware-cli project proxy setup` later."). | ||
| Value(&selectSetupNow), | ||
| ).WithHideFunc(func() bool { | ||
| if machineSetupDone { | ||
| return true | ||
| } | ||
| dockerOn := selectDocker == tui.Yes | ||
| if cmd.PersistentFlags().Changed("docker") { | ||
| dockerOn = opts.useDocker | ||
| } | ||
| localOn := selectLocalDomain | ||
| if cmd.PersistentFlags().Changed("local-domain") { | ||
| localOn = opts.useLocalDomain | ||
| } | ||
| return !dockerOn || !localOn | ||
| })) | ||
| } | ||
|
|
||
| selectAdvanced := tui.No | ||
| if needsAdvanced { | ||
| formGroups = append(formGroups, huh.NewGroup( | ||
|
|
@@ -329,6 +387,17 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor | |
| if !cmd.PersistentFlags().Changed("docker") { | ||
| opts.useDocker = selectDocker == tui.Yes | ||
| } | ||
| // The local-domain choice comes from the --local-domain flag when set, | ||
| // otherwise from the prompt. The one-time setup is only offered inline | ||
| // when we actually prompted for it (not via the flag), so the flag never | ||
| // triggers an unprompted sudo. | ||
| localFlagChanged := cmd.PersistentFlags().Changed("local-domain") | ||
| wantLocalDomain := opts.useLocalDomain | ||
| if !localFlagChanged { | ||
| wantLocalDomain = selectLocalDomain | ||
| } | ||
| opts.useLocalDomain, opts.setupProxyNow = resolveLocalDomainChoice( | ||
| opts.useDocker, wantLocalDomain, !localFlagChanged, machineSetupDone, selectSetupNow == tui.Yes) | ||
| if !cmd.PersistentFlags().Changed("git") { | ||
| opts.initGit = selectGit == tui.Yes | ||
| } | ||
|
|
@@ -384,6 +453,13 @@ func runCreateForm(cmd *cobra.Command, opts *createOptions, releases []repositor | |
| } | ||
| fmt.Printf(" %s %s\n", labelStyle.Render("PHP:"), phpDisplay) | ||
| } | ||
| if opts.useDocker { | ||
| localDomainValue := onOff(opts.useLocalDomain) | ||
| if opts.useLocalDomain { | ||
| localDomainValue = tui.GreenText.Render("https://" + localDomainHostname(opts.projectFolder, baseDomain)) | ||
| } | ||
| fmt.Printf(" %s %s\n", labelStyle.Render("Local domain:"), localDomainValue) | ||
| } | ||
| fmt.Printf(" %s %s\n", labelStyle.Render("Git Repository:"), onOff(opts.initGit)) | ||
| fmt.Printf(" %s %s\n", labelStyle.Render("OpenSearch:"), onOff(opts.withElasticsearch)) | ||
| fmt.Printf(" %s %s\n", labelStyle.Render("AMQP:"), onOff(opts.withAMQP)) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we may move somet of those functions into the proxy pkg?