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
35 changes: 35 additions & 0 deletions cmd/project/executor.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package project

import (
"database/sql"

"github.com/spf13/cobra"

"github.com/shopware/shopware-cli/internal/executor"
Expand All @@ -21,3 +23,36 @@ func resolveExecutor(cmd *cobra.Command, projectRoot string) (executor.Executor,

return executor.New(projectRoot, envCfg, cfg)
}

// resolveProjectDatabaseConnection resolves the database credentials of the
// current environment through its executor.
func resolveProjectDatabaseConnection(cmd *cobra.Command) (*executor.DatabaseConnection, error) {
projectRoot, err := findClosestShopwareProject()
if err != nil {
return nil, err
}

cmdExecutor, err := resolveExecutor(cmd, projectRoot)
if err != nil {
return nil, err
}

return cmdExecutor.DatabaseConnection(cmd.Context())
}

// connectProjectDatabase resolves the database of the current environment and
// opens a single dedicated connection to it. The returned cleanup closes
// connection and pool.
func connectProjectDatabase(cmd *cobra.Command) (*sql.Conn, *executor.DatabaseConnection, func(), error) {
dbConn, err := resolveProjectDatabaseConnection(cmd)
if err != nil {
return nil, nil, nil, err
}

conn, cleanup, err := dbConn.Open(cmd.Context())
if err != nil {
return nil, nil, nil, err
}

return conn, dbConn, cleanup, nil
}
94 changes: 22 additions & 72 deletions cmd/project/project_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,19 @@ package project

import (
"compress/gzip"
"context"
"database/sql"
"errors"
"fmt"
"io"
"net"
"net/url"
"os"
"strings"
"time"

"github.com/charmbracelet/x/term"
"github.com/go-sql-driver/mysql"
"github.com/klauspost/compress/zstd"
"github.com/spf13/cobra"

"github.com/shopware/shopware-cli/internal/envfile"
"github.com/shopware/shopware-cli/internal/executor"
"github.com/shopware/shopware-cli/internal/mysqldump"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/internal/system"
Expand Down Expand Up @@ -143,22 +139,9 @@ var projectDatabaseDumpCmd = &cobra.Command{
}

func assembleConnectionURI(cmd *cobra.Command) (*mysql.Config, error) {
cfg := &mysql.Config{
Loc: time.UTC,
Net: "tcp",
ParseTime: false,
AllowNativePasswords: true,
CheckConnLiveness: true,
User: "root",
Passwd: "root",
Addr: "127.0.0.1:3306",
DBName: "shopware",
}

if projectRoot, err := findClosestShopwareProject(); err == nil {
if err := loadDatabaseURLIntoConnection(cmd.Context(), projectRoot, cfg); err != nil {
return nil, err
}
dbConn, err := resolveDumpDatabaseConnection(cmd)
if err != nil {
return nil, err
}

host, _ := cmd.Flags().GetString("host")
Expand All @@ -168,20 +151,20 @@ func assembleConnectionURI(cmd *cobra.Command) (*mysql.Config, error) {
db, _ := cmd.Flags().GetString("database")

if host != "" {
if port != "" {
cfg.Addr = fmt.Sprintf("%s:%s", host, port)
} else {
cfg.Addr = host
}
dbConn.Host = host
}

if port != "" {
dbConn.Port = port
}

if db != "" {
cfg.DBName = db
dbConn.Database = db
}

if username != "" {
cfg.User = username
cfg.Passwd = ""
dbConn.Username = username
dbConn.Password = ""
}

if cmd.Flags().Changed("password") {
Expand All @@ -202,57 +185,24 @@ func assembleConnectionURI(cmd *cobra.Command) (*mysql.Config, error) {
return nil, fmt.Errorf("could not read password: %w", err)
}

cfg.Passwd = string(pass)
dbConn.Password = string(pass)
} else {
cfg.Passwd = password
dbConn.Password = password
}
}

return cfg, nil
return dbConn.MySQLConfig(), nil
}

func loadDatabaseURLIntoConnection(ctx context.Context, projectRoot string, cfg *mysql.Config) error {
if err := envfile.LoadSymfonyEnvFile(projectRoot); err != nil {
return err
}

databaseUrl := os.Getenv("DATABASE_URL")

if databaseUrl == "" {
return nil
}

logging.FromContext(ctx).Info("Using DATABASE_URL env as default connection string. options can override specific parts (--username=foo)")

parsedUri, err := url.Parse(databaseUrl)
if err != nil {
return fmt.Errorf("could not parse DATABASE_URL: %w", err)
}

if parsedUri.User != nil {
cfg.User = parsedUri.User.Username()

if password, ok := parsedUri.User.Password(); ok {
cfg.Passwd = password
} else {
// Reset password if it is not set
cfg.Passwd = ""
}
}

if parsedUri.Host != "" {
cfg.Addr = parsedUri.Host

if parsedUri.Port() == "" {
cfg.Addr = net.JoinHostPort(parsedUri.Host, "3306")
}
}

if parsedUri.Path != "" {
cfg.DBName = strings.Trim(parsedUri.Path, "/")
// resolveDumpDatabaseConnection resolves credentials like the other database
// commands, but keeps dump usable outside a Shopware project: there the
// process environment and the connection flags are all that is needed.
func resolveDumpDatabaseConnection(cmd *cobra.Command) (*executor.DatabaseConnection, error) {
if _, err := findClosestShopwareProject(); err != nil {
return executor.NewLocal("").DatabaseConnection(cmd.Context())
}

return nil
return resolveProjectDatabaseConnection(cmd)
}

func init() {
Expand Down
102 changes: 102 additions & 0 deletions cmd/project/project_dump_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package project

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func newDumpFlagCommand(t *testing.T, flags map[string]string) *cobra.Command {
t.Helper()

cmd := &cobra.Command{}
cmd.Flags().String("host", "", "")
cmd.Flags().String("port", "", "")
cmd.Flags().String("username", "", "")
cmd.Flags().String("password", "", "")
cmd.Flags().Lookup("password").NoOptDefVal = passwordFlagPrompt
cmd.Flags().String("database", "", "")
cmd.SetContext(t.Context())

for name, value := range flags {
require.NoError(t, cmd.Flags().Set(name, value))
}

return cmd
}

// chdirOutsideProject moves into an empty directory so the project lookup
// fails and the environment-only fallback is used.
func chdirOutsideProject(t *testing.T) {
t.Helper()
t.Setenv("PROJECT_ROOT", "")
t.Chdir(t.TempDir())
}

func TestAssembleConnectionURIDefaults(t *testing.T) {
chdirOutsideProject(t)
t.Setenv("DATABASE_URL", "")

cfg, err := assembleConnectionURI(newDumpFlagCommand(t, nil))
require.NoError(t, err)

assert.Equal(t, "127.0.0.1:3306", cfg.Addr)
assert.Equal(t, "root", cfg.User)
assert.Equal(t, "root", cfg.Passwd)
assert.Equal(t, "shopware", cfg.DBName)
}

func TestAssembleConnectionURIFlagOverrides(t *testing.T) {
chdirOutsideProject(t)
t.Setenv("DATABASE_URL", "")

cfg, err := assembleConnectionURI(newDumpFlagCommand(t, map[string]string{
"host": "db.internal",
"port": "3307",
"username": "backup",
"password": "secret",
"database": "shop_prod",
}))
require.NoError(t, err)

assert.Equal(t, "db.internal:3307", cfg.Addr)
assert.Equal(t, "backup", cfg.User)
assert.Equal(t, "secret", cfg.Passwd)
assert.Equal(t, "shop_prod", cfg.DBName)
}

func TestAssembleConnectionURIUsernameClearsPassword(t *testing.T) {
chdirOutsideProject(t)
t.Setenv("DATABASE_URL", "")

cfg, err := assembleConnectionURI(newDumpFlagCommand(t, map[string]string{"username": "backup"}))
require.NoError(t, err)

assert.Equal(t, "backup", cfg.User)
assert.Empty(t, cfg.Passwd)
}

func TestAssembleConnectionURIDatabaseURLInsideProject(t *testing.T) {
projectRoot := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(projectRoot, "bin"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(projectRoot, "bin", "console"), nil, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(projectRoot, "composer.json"), []byte(`{"require": {"shopware/core": "6.6.0"}}`), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(projectRoot, ".env"), []byte("DATABASE_URL=mysql://app:secret@db.example.com:3307/shop\n"), 0o644))

t.Setenv("PROJECT_ROOT", "")
t.Setenv("DATABASE_URL", "")
t.Setenv("SHOPWARE_CLI_NO_SYMFONY_CLI", "1")
t.Chdir(projectRoot)

cfg, err := assembleConnectionURI(newDumpFlagCommand(t, map[string]string{"database": "other"}))
require.NoError(t, err)

assert.Equal(t, "db.example.com:3307", cfg.Addr)
assert.Equal(t, "app", cfg.User)
assert.Equal(t, "secret", cfg.Passwd)
assert.Equal(t, "other", cfg.DBName, "flag overrides the URL database")
}
Loading
Loading