From da4db233fef7eced74faefab709d0c234832ead9 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Fri, 27 Sep 2024 11:50:42 +0200 Subject: [PATCH 01/10] Switch to MySQL 8 With this Local Beach uses MySQL 8 instead of MariaDB 10 to be closer to the setup used in Beach. Running `beach setup` will migrate all databases from MariaDB to MySQL if the path used previously exists. --- assets/local-beach/docker-compose.yml | 8 +- assets/local-beach/mariadb-compose.yml | 42 +++++++++ cmd/beach/cmd/helpers.go | 52 +++++++---- cmd/beach/cmd/setup.go | 119 +++++++++++++++++++++---- pkg/path/path_darwin.go | 6 +- pkg/path/path_linux.go | 6 +- 6 files changed, 195 insertions(+), 38 deletions(-) create mode 100644 assets/local-beach/mariadb-compose.yml diff --git a/assets/local-beach/docker-compose.yml b/assets/local-beach/docker-compose.yml index 7654b6c..4ec303f 100644 --- a/assets/local-beach/docker-compose.yml +++ b/assets/local-beach/docker-compose.yml @@ -17,12 +17,12 @@ services: environment: - DEFAULT_HOST=hello.localbeach.net database: - image: mariadb:10.11 + image: mysql:8 container_name: local_beach_database networks: - local_beach volumes: - - {{databasePath}}:/var/lib/mysql + - {{mysqlDatabasePath}}:/var/lib/mysql healthcheck: test: "/usr/bin/mysql --user=root --password=password --execute \"SHOW DATABASES;\"" interval: 3s @@ -32,4 +32,6 @@ services: - MYSQL_ROOT_PASSWORD=password ports: - 3307:3306 - command: 'mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci' + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci diff --git a/assets/local-beach/mariadb-compose.yml b/assets/local-beach/mariadb-compose.yml new file mode 100644 index 0000000..58af4bd --- /dev/null +++ b/assets/local-beach/mariadb-compose.yml @@ -0,0 +1,42 @@ +networks: + local_beach: + external: true + +services: + database: + image: mysql:8 + container_name: local_beach_database + networks: + - local_beach + volumes: + - {{mysqlDatabasePath}}:/var/lib/mysql + healthcheck: + test: "/usr/bin/mysql --user=root --password=password --execute \"SHOW DATABASES;\"" + interval: 3s + timeout: 1s + retries: 10 + environment: + - MYSQL_ROOT_PASSWORD=password + ports: + - 3307:3306 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + + mariadb: + image: mariadb:10.11 + container_name: local_beach_mariadb + networks: + - local_beach + volumes: + - {{mariadbDatabasePath}}:/var/lib/mysql + healthcheck: + test: "/usr/bin/mysql --user=root --password=password --execute \"SHOW DATABASES;\"" + interval: 3s + timeout: 1s + retries: 10 + environment: + - MYSQL_ROOT_PASSWORD=password + ports: + - 3306:3306 + command: 'mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci' diff --git a/cmd/beach/cmd/helpers.go b/cmd/beach/cmd/helpers.go index e6fa984..902abe4 100644 --- a/cmd/beach/cmd/helpers.go +++ b/cmd/beach/cmd/helpers.go @@ -144,6 +144,42 @@ func retrieveCloudStorageCredentials(instanceIdentifier string, projectNamespace return nil, bucketName, privateKey } +func writeLocalBeachComposeFile() { + composeFileContent := readFileFromAssets("local-beach/docker-compose.yml") + composeFileContent = strings.ReplaceAll(composeFileContent, "{{mysqlDatabasePath}}", path.MySQLDatabase) + composeFileContent = strings.ReplaceAll(composeFileContent, "{{certificatesPath}}", path.Certificates) + + destination, err := os.Create(filepath.Join(path.Base, "docker-compose.yml")) + if err != nil { + log.Error("failed creating docker-compose.yml: ", err) + } else { + _, err = destination.WriteString(composeFileContent) + if err != nil { + log.Error(err) + } + + } + _ = destination.Close() +} + +func writeMariaDBComposeFile() { + composeFileContent := readFileFromAssets("local-beach/mariadb-compose.yml") + composeFileContent = strings.ReplaceAll(composeFileContent, "{{mysqlDatabasePath}}", path.MySQLDatabase) + composeFileContent = strings.ReplaceAll(composeFileContent, "{{mariadbDatabasePath}}", path.MariaDBDatabase) + + destination, err := os.Create(filepath.Join(path.Base, "mariadb-compose.yml")) + if err != nil { + log.Error("failed creating mariadb-compose.yml: ", err) + } else { + _, err = destination.WriteString(composeFileContent) + if err != nil { + log.Error(err) + } + + } + _ = destination.Close() +} + func startLocalBeach() error { _, err := os.Stat(path.Base) if os.IsNotExist(err) { @@ -164,21 +200,7 @@ func startLocalBeach() error { } if len(nginxStatusOutput) == 0 || len(databaseStatusOutput) == 0 { - composeFileContent := readFileFromAssets("local-beach/docker-compose.yml") - composeFileContent = strings.ReplaceAll(composeFileContent, "{{databasePath}}", path.Database) - composeFileContent = strings.ReplaceAll(composeFileContent, "{{certificatesPath}}", path.Certificates) - - destination, err := os.Create(filepath.Join(path.Base, "docker-compose.yml")) - if err != nil { - log.Error("failed creating docker-compose.yml: ", err) - } else { - _, err = destination.WriteString(composeFileContent) - if err != nil { - log.Error(err) - } - - } - _ = destination.Close() + writeLocalBeachComposeFile() log.Info("Starting reverse proxy and database server ...") commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "docker-compose.yml"), "up", "--remove-orphans", "-d"} diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 0c249e8..7202065 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -15,14 +15,15 @@ package cmd import ( - "os" - "path/filepath" - "strings" - + "errors" "github.com/flownative/localbeach/pkg/exec" "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "os" + "path/filepath" + "strings" + "time" ) // setupCmd represents the setup command @@ -66,7 +67,7 @@ func migrateOldBase() error { } log.Info("moving database data") - err = os.Rename(filepath.Join(path.OldBase, "MariaDB"), path.Database) + err = os.Rename(filepath.Join(path.OldBase, "MariaDB"), path.MariaDBDatabase) if err != nil { if os.IsNotExist(err) { log.Error(err) @@ -114,27 +115,113 @@ func setupLocalBeach() error { log.Error(err) } - log.Debug("creating directory for databases at " + path.Database) - err = os.MkdirAll(path.Database, os.ModePerm) + log.Debug("creating directory for databases at " + path.MySQLDatabase) + err = os.MkdirAll(path.MySQLDatabase, os.ModePerm) if err != nil && !os.IsExist(err) { log.Error(err) } - composeFileContent := readFileFromAssets("local-beach/docker-compose.yml") - composeFileContent = strings.ReplaceAll(composeFileContent, "{{databasePath}}", path.Database) - composeFileContent = strings.ReplaceAll(composeFileContent, "{{certificatesPath}}", path.Certificates) - - destination, err := os.Create(filepath.Join(path.Base, "docker-compose.yml")) + err = migrateMariaDBToMySQL() if err != nil { - log.Error("failed creating docker-compose.yml: ", err) - } else { - _, err = destination.WriteString(composeFileContent) + return err + } + + writeLocalBeachComposeFile() + + return nil +} + +func migrateMariaDBToMySQL() error { + _, err := os.Stat(path.MariaDBDatabase) + if err == nil { + log.Info("Migrating MariaDB data from " + path.MariaDBDatabase + " to MySQL at " + path.MySQLDatabase) + log.Warn("Note: This may take a while, depending on DB size!") + + if err = startMariaDB(); err != nil { + return err + } + + log.Debug("dumping data from MariaDB to MySQL") + commandArgs := []string{"exec", "local_beach_mariadb", "bash", "-c"} + commandArgs = append(commandArgs, "mysql -h local_beach_mariadb -u root -ppassword --batch --skip-column-names -e \"SHOW DATABASES;\" | grep -E -v \"(information|performance)_schema|mysql|sys\"") + databases, err := exec.RunCommand("docker", commandArgs) if err != nil { log.Error(err) + return err + } + + for _, database := range strings.Split(strings.TrimSuffix(databases, "\n"), "\n") { + log.Debug("… " + database) + commandArgs = []string{"exec", "local_beach_database", "bash", "-c"} + commandArgs = append(commandArgs, "mysqldump -h local_beach_mariadb -u root -ppassword --add-drop-trigger --compress --comments --dump-date --hex-blob --quote-names --routines --triggers --no-autocommit --no-tablespaces --skip-lock-tables --single-transaction --quick --databases "+database+" | sed -e \"s/DEFAULT '{}' COMMENT '(DC2Type:json)'/DEFAULT (JSON_OBJECT()) COMMENT '(DC2Type:json)'/\" | mysql -h local_beach_database -u root -ppassword") + _, err := exec.RunCommand("docker", commandArgs) + if err != nil { + log.Error(err) + } } + if err = stopMariaDB(); err != nil { + return err + } } - _ = destination.Close() + + log.Info("Done with migration to MySQL at " + path.MySQLDatabase) + log.Info("If all works as expected, remove " + path.MariaDBDatabase) return nil } + +func startMariaDB() error { + log.Debug("starting MariaDB server ...") + + writeMariaDBComposeFile() + + commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "mariadb-compose.yml"), "up", "-d"} + err := exec.RunInteractiveCommand("docker", commandArgs) + if err != nil { + return errors.New("Database container startup failed") + } + + log.Debug("waiting for MariaDB server ...") + tries := 1 + for { + output, err := exec.RunCommand("docker", []string{"inspect", "-f", "{{.State.Health.Status}}", "local_beach_mariadb"}) + if err != nil { + return errors.New("failed to check for MariaDB server container health") + } + if strings.TrimSpace(output) == "healthy" { + break + } + if tries == 10 { + return errors.New("timeout waiting for MariaDB server to start") + } + tries++ + time.Sleep(3 * time.Second) + } + + log.Debug("waiting for MySQL server ...") + tries = 1 + for { + output, err := exec.RunCommand("docker", []string{"inspect", "-f", "{{.State.Health.Status}}", "local_beach_database"}) + if err != nil { + return errors.New("failed to check for MySQL server container health") + } + if strings.TrimSpace(output) == "healthy" { + break + } + if tries == 10 { + return errors.New("timeout waiting for MySQL server to start") + } + tries++ + time.Sleep(3 * time.Second) + } + + return nil +} +func stopMariaDB() error { + log.Debug("stopping MariaDB server ...") + commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "mariadb-compose.yml"), "rm", "--force", "--stop", "-v"} + _, err := exec.RunCommand("docker", commandArgs) + + return err +} diff --git a/pkg/path/path_darwin.go b/pkg/path/path_darwin.go index 427ab29..88e73de 100644 --- a/pkg/path/path_darwin.go +++ b/pkg/path/path_darwin.go @@ -27,7 +27,8 @@ import ( var OldBase = "" var Base = "" var Certificates = "" -var Database = "" +var MariaDBDatabase = "" +var MySQLDatabase = "" func init() { homeDir, err := os.UserHomeDir() @@ -40,5 +41,6 @@ func init() { OldBase = filepath.Join(homeDir, "Library", "Application Support", "Flownative", "Local Beach") Base = filepath.Join(homeDir, ".LocalBeach") Certificates = filepath.Join(Base, "Certificates") - Database = filepath.Join(Base, "MariaDB") + MariaDBDatabase = filepath.Join(Base, "MariaDB") + MySQLDatabase = filepath.Join(Base, "MySQL") } diff --git a/pkg/path/path_linux.go b/pkg/path/path_linux.go index bb1049a..0c8d12e 100644 --- a/pkg/path/path_linux.go +++ b/pkg/path/path_linux.go @@ -27,7 +27,8 @@ import ( var OldBase = "" var Base = "" var Certificates = "" -var Database = "" +var MariaDBDatabase = "" +var MySQLDatabase = "" func init() { homeDir, err := os.UserHomeDir() @@ -40,5 +41,6 @@ func init() { OldBase = filepath.Join(homeDir, ".Flownative", "Local Beach") Base = filepath.Join(homeDir, ".LocalBeach") Certificates = filepath.Join(Base, "Certificates") - Database = filepath.Join(Base, "MariaDB") + MariaDBDatabase = filepath.Join(Base, "MariaDB") + MySQLDatabase = filepath.Join(Base, "MySQL") } From 0a74a235b463776e14b95aa63ac4c4607e3c3f1d Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 2 Apr 2025 17:28:21 +0200 Subject: [PATCH 02/10] Use mysql:8.0 to avoid getting 8.4 --- assets/local-beach/docker-compose.yml | 2 +- assets/local-beach/mariadb-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/local-beach/docker-compose.yml b/assets/local-beach/docker-compose.yml index 4ec303f..93f7f10 100644 --- a/assets/local-beach/docker-compose.yml +++ b/assets/local-beach/docker-compose.yml @@ -17,7 +17,7 @@ services: environment: - DEFAULT_HOST=hello.localbeach.net database: - image: mysql:8 + image: mysql:8.0 container_name: local_beach_database networks: - local_beach diff --git a/assets/local-beach/mariadb-compose.yml b/assets/local-beach/mariadb-compose.yml index 58af4bd..d7d2684 100644 --- a/assets/local-beach/mariadb-compose.yml +++ b/assets/local-beach/mariadb-compose.yml @@ -4,7 +4,7 @@ networks: services: database: - image: mysql:8 + image: mysql:8.0 container_name: local_beach_database networks: - local_beach From a24239de9e480013de9ddd9ddf808d8df7196739 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Tue, 14 Oct 2025 13:11:01 +0200 Subject: [PATCH 03/10] Reformat code --- cmd/beach/cmd/helpers.go | 2 +- cmd/beach/cmd/pause.go | 3 +- cmd/beach/cmd/resource-download.go | 261 +++++++++++++++-------------- cmd/beach/cmd/resume.go | 3 +- cmd/beach/cmd/setup-https.go | 2 +- cmd/beach/cmd/setup.go | 9 +- cmd/beach/cmd/version.go | 1 + 7 files changed, 143 insertions(+), 138 deletions(-) diff --git a/cmd/beach/cmd/helpers.go b/cmd/beach/cmd/helpers.go index 0d23293..d14c74a 100644 --- a/cmd/beach/cmd/helpers.go +++ b/cmd/beach/cmd/helpers.go @@ -19,7 +19,6 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/flownative/localbeach/pkg/path" "io" "net/http" "os" @@ -28,6 +27,7 @@ import ( "time" "github.com/flownative/localbeach/pkg/exec" + "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" asset "github.com/flownative/localbeach/assets" diff --git a/cmd/beach/cmd/pause.go b/cmd/beach/cmd/pause.go index 7407942..46b8368 100644 --- a/cmd/beach/cmd/pause.go +++ b/cmd/beach/cmd/pause.go @@ -15,11 +15,12 @@ package cmd import ( + "path/filepath" + "github.com/flownative/localbeach/pkg/exec" "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "path/filepath" ) // pauseCmd represents the pause command diff --git a/cmd/beach/cmd/resource-download.go b/cmd/beach/cmd/resource-download.go index f1ab3bf..a571f5a 100644 --- a/cmd/beach/cmd/resource-download.go +++ b/cmd/beach/cmd/resource-download.go @@ -15,19 +15,20 @@ package cmd import ( - "cloud.google.com/go/storage" - "context" - "errors" - "fmt" - "github.com/flownative/localbeach/pkg/beachsandbox" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "google.golang.org/api/iterator" - "google.golang.org/api/option" - "hash/crc32" - "io" - "os" - "path/filepath" + "context" + "errors" + "fmt" + "hash/crc32" + "io" + "os" + "path/filepath" + + "cloud.google.com/go/storage" + "github.com/flownative/localbeach/pkg/beachsandbox" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "google.golang.org/api/iterator" + "google.golang.org/api/option" ) var sourceBucketName, targetResourcesPath string @@ -35,9 +36,9 @@ var synchronize bool // resourceDownloadCmd represents the resource-download command var resourceDownloadCmd = &cobra.Command{ - Use: "resource-download", - Short: "Download resources (assets) from a local Flow or Neos installation to Beach", - Long: `resource-download + Use: "resource-download", + Short: "Download resources (assets) from a local Flow or Neos installation to Beach", + Long: `resource-download This command downloads Flow resources from a Beach instance to a local Flow or Neos project. @@ -56,127 +57,127 @@ Notes: - existing data in the local Neos instance will be left unchanged - older Beach instances may use a namespace called "beach" `, - Args: cobra.ExactArgs(0), - Run: handleResourceDownloadRun, + Args: cobra.ExactArgs(0), + Run: handleResourceDownloadRun, } func init() { - resourceDownloadCmd.Flags().StringVar(&instanceIdentifier, "instance", "", "instance identifier of the Beach instance to download from, eg. 'instance-123abc45-def6-7890-abcd-1234567890ab'") - resourceDownloadCmd.Flags().StringVar(&projectNamespace, "namespace", "", "The project namespace of the Beach instance to download from, eg. 'beach-project-123abc45-def6-7890-abcd-1234567890ab'") - resourceDownloadCmd.Flags().StringVar(&clusterIdentifier, "cluster", "", "The cluster identifier of the Beach instance to download from, eg. 'h9acc4'") - resourceDownloadCmd.Flags().StringVar(&sourceBucketName, "bucket", "", "name of the bucket to download resources from") - resourceDownloadCmd.Flags().StringVar(&targetResourcesPath, "resources-path", "", "custom path where to store the downloaded resources, e.g. 'Data/Persistent/Protected'") - resourceDownloadCmd.Flags().BoolVar(&synchronize, "sync", false, "Skip unchanged existing files") - - _ = resourceDownloadCmd.MarkFlagRequired("instance") - _ = resourceDownloadCmd.MarkFlagRequired("namespace") - rootCmd.AddCommand(resourceDownloadCmd) + resourceDownloadCmd.Flags().StringVar(&instanceIdentifier, "instance", "", "instance identifier of the Beach instance to download from, eg. 'instance-123abc45-def6-7890-abcd-1234567890ab'") + resourceDownloadCmd.Flags().StringVar(&projectNamespace, "namespace", "", "The project namespace of the Beach instance to download from, eg. 'beach-project-123abc45-def6-7890-abcd-1234567890ab'") + resourceDownloadCmd.Flags().StringVar(&clusterIdentifier, "cluster", "", "The cluster identifier of the Beach instance to download from, eg. 'h9acc4'") + resourceDownloadCmd.Flags().StringVar(&sourceBucketName, "bucket", "", "name of the bucket to download resources from") + resourceDownloadCmd.Flags().StringVar(&targetResourcesPath, "resources-path", "", "custom path where to store the downloaded resources, e.g. 'Data/Persistent/Protected'") + resourceDownloadCmd.Flags().BoolVar(&synchronize, "sync", false, "Skip unchanged existing files") + + _ = resourceDownloadCmd.MarkFlagRequired("instance") + _ = resourceDownloadCmd.MarkFlagRequired("namespace") + rootCmd.AddCommand(resourceDownloadCmd) } func handleResourceDownloadRun(cmd *cobra.Command, args []string) { - sandbox, err := beachsandbox.GetActiveSandbox() - if err != nil { - log.Fatal("Could not activate sandbox: ", err) - return - } - - if targetResourcesPath == "" { - targetResourcesPath = sandbox.ProjectDataPersistentResourcesPath - } - - _, err = os.Stat(targetResourcesPath) - if err != nil { - log.Fatal(fmt.Sprintf("The path %v does not exist", targetResourcesPath)) - return - } - - err, bucketNameFromCredentials, privateKeyDecoded := retrieveCloudStorageCredentials(instanceIdentifier, projectNamespace, clusterIdentifier) - if err != nil { - log.Fatal(err) - return - } - - if sourceBucketName == "" { - sourceBucketName = bucketNameFromCredentials - } - - ctx := context.Background() - client, err := storage.NewClient(ctx, option.WithCredentialsJSON(privateKeyDecoded)) - if err != nil { - log.Fatal(fmt.Sprintf("Failed to initialize cloud storage client: %v", err)) - return - } - - log.Info(fmt.Sprintf("Downloading resources from bucket %v to local directory %v ...", sourceBucketName, targetResourcesPath)) - - bucket := client.Bucket(sourceBucketName) - it := bucket.Objects(ctx, nil) - for { - attributes, err := it.Next() - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - log.Error(err) - } else { - source := bucket.Object(attributes.Name) - targetPathAndFilename := filepath.Join(targetResourcesPath, getRelativePersistentResourcePathByHash(attributes.Name), filepath.Base(attributes.Name)) - - err = os.MkdirAll(filepath.Dir(targetPathAndFilename), 0755) - if err != nil { - log.Fatal(err) - return - } - - if synchronize == true { - if checkFileExists(targetPathAndFilename, attributes) { - log.Debug("Skipped " + attributes.Name + " as it already exists") - continue - } - } - - file, err := os.OpenFile(targetPathAndFilename, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - log.Fatal(err) - return - } - reader, err := source.NewReader(ctx) - if err != nil { - log.Fatal(err) - return - } - if _, err := io.Copy(file, reader); err != nil { - log.Fatal(err) - return - } - if err := reader.Close(); err != nil { - log.Fatal(err) - return - } - log.Debug("Downloaded " + attributes.Name) - } - } - - log.Info("Done") - return + sandbox, err := beachsandbox.GetActiveSandbox() + if err != nil { + log.Fatal("Could not activate sandbox: ", err) + return + } + + if targetResourcesPath == "" { + targetResourcesPath = sandbox.ProjectDataPersistentResourcesPath + } + + _, err = os.Stat(targetResourcesPath) + if err != nil { + log.Fatal(fmt.Sprintf("The path %v does not exist", targetResourcesPath)) + return + } + + err, bucketNameFromCredentials, privateKeyDecoded := retrieveCloudStorageCredentials(instanceIdentifier, projectNamespace, clusterIdentifier) + if err != nil { + log.Fatal(err) + return + } + + if sourceBucketName == "" { + sourceBucketName = bucketNameFromCredentials + } + + ctx := context.Background() + client, err := storage.NewClient(ctx, option.WithCredentialsJSON(privateKeyDecoded)) + if err != nil { + log.Fatal(fmt.Sprintf("Failed to initialize cloud storage client: %v", err)) + return + } + + log.Info(fmt.Sprintf("Downloading resources from bucket %v to local directory %v ...", sourceBucketName, targetResourcesPath)) + + bucket := client.Bucket(sourceBucketName) + it := bucket.Objects(ctx, nil) + for { + attributes, err := it.Next() + if errors.Is(err, iterator.Done) { + break + } + if err != nil { + log.Error(err) + } else { + source := bucket.Object(attributes.Name) + targetPathAndFilename := filepath.Join(targetResourcesPath, getRelativePersistentResourcePathByHash(attributes.Name), filepath.Base(attributes.Name)) + + err = os.MkdirAll(filepath.Dir(targetPathAndFilename), 0755) + if err != nil { + log.Fatal(err) + return + } + + if synchronize == true { + if checkFileExists(targetPathAndFilename, attributes) { + log.Debug("Skipped " + attributes.Name + " as it already exists") + continue + } + } + + file, err := os.OpenFile(targetPathAndFilename, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + log.Fatal(err) + return + } + reader, err := source.NewReader(ctx) + if err != nil { + log.Fatal(err) + return + } + if _, err := io.Copy(file, reader); err != nil { + log.Fatal(err) + return + } + if err := reader.Close(); err != nil { + log.Fatal(err) + return + } + log.Debug("Downloaded " + attributes.Name) + } + } + + log.Info("Done") + return } func checkFileExists(targetPathAndFilename string, attributes *storage.ObjectAttrs) bool { - if _, err := os.Stat(targetPathAndFilename); err == nil { - file, err := os.Open(targetPathAndFilename) - if err != nil { - return false - } - defer file.Close() - - crc32c := crc32.New(crc32.MakeTable(crc32.Castagnoli)) - if _, err := io.Copy(crc32c, file); err != nil { - return false - } - - if crc32c.Sum32() == attributes.CRC32C { - return true - } - } - return false + if _, err := os.Stat(targetPathAndFilename); err == nil { + file, err := os.Open(targetPathAndFilename) + if err != nil { + return false + } + defer file.Close() + + crc32c := crc32.New(crc32.MakeTable(crc32.Castagnoli)) + if _, err := io.Copy(crc32c, file); err != nil { + return false + } + + if crc32c.Sum32() == attributes.CRC32C { + return true + } + } + return false } diff --git a/cmd/beach/cmd/resume.go b/cmd/beach/cmd/resume.go index f85c492..8065100 100644 --- a/cmd/beach/cmd/resume.go +++ b/cmd/beach/cmd/resume.go @@ -15,11 +15,12 @@ package cmd import ( + "path/filepath" + "github.com/flownative/localbeach/pkg/exec" "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "path/filepath" ) // resumeCmd represents the resume command diff --git a/cmd/beach/cmd/setup-https.go b/cmd/beach/cmd/setup-https.go index 14424cf..2395bda 100644 --- a/cmd/beach/cmd/setup-https.go +++ b/cmd/beach/cmd/setup-https.go @@ -17,12 +17,12 @@ package cmd import ( "errors" "path/filepath" + "strings" "github.com/flownative/localbeach/pkg/exec" "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "strings" ) var host string diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index ac84baa..8dd67b4 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -16,14 +16,15 @@ package cmd import ( "errors" - "github.com/flownative/localbeach/pkg/exec" - "github.com/flownative/localbeach/pkg/path" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" "os" "path/filepath" "strings" "time" + + "github.com/flownative/localbeach/pkg/exec" + "github.com/flownative/localbeach/pkg/path" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" ) // setupCmd represents the setup command diff --git a/cmd/beach/cmd/version.go b/cmd/beach/cmd/version.go index b776452..6ba1628 100644 --- a/cmd/beach/cmd/version.go +++ b/cmd/beach/cmd/version.go @@ -16,6 +16,7 @@ package cmd import ( "fmt" + "github.com/flownative/localbeach/pkg/version" "github.com/spf13/cobra" ) From 061749e848e06c06e8cc3ef81fbef03b50ac9eac Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Tue, 14 Oct 2025 13:31:22 +0200 Subject: [PATCH 04/10] Fix MariaDB migration success output The success message was always output, even when no migration was done. --- cmd/beach/cmd/setup.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 8dd67b4..6c99d6e 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -124,6 +124,7 @@ func setupLocalBeach() error { err = migrateMariaDBToMySQL() if err != nil { + log.Error(err) return err } @@ -164,10 +165,10 @@ func migrateMariaDBToMySQL() error { if err = stopMariaDB(); err != nil { return err } - } - log.Info("Done with migration to MySQL at " + path.MySQLDatabase) - log.Info("If all works as expected, remove " + path.MariaDBDatabase) + log.Info("Done with migration to MySQL at " + path.MySQLDatabase) + log.Info("If all works as expected, remove " + path.MariaDBDatabase) + } return nil } From 2567a4907dc19652b1adfe8debac24264b8271d2 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Tue, 14 Oct 2025 13:15:42 +0200 Subject: [PATCH 05/10] Move "beach down" code into helper function --- cmd/beach/cmd/down.go | 82 +--------------------------------------- cmd/beach/cmd/helpers.go | 74 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 81 deletions(-) diff --git a/cmd/beach/cmd/down.go b/cmd/beach/cmd/down.go index 2a6a0fc..1bfc5a5 100644 --- a/cmd/beach/cmd/down.go +++ b/cmd/beach/cmd/down.go @@ -15,16 +15,6 @@ package cmd import ( - "errors" - "os" - "path/filepath" - "strings" - - "github.com/flownative/localbeach/pkg/path" - - "github.com/flownative/localbeach/pkg/beachsandbox" - "github.com/flownative/localbeach/pkg/exec" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -42,75 +32,5 @@ func init() { } func handleDownRun(cmd *cobra.Command, args []string) { - instanceRoots, err := findInstanceRoots() - if err != nil { - log.Fatal(err) - return - } - for _, instanceRoot := range instanceRoots { - log.Info("Stopping instance in " + instanceRoot + "...") - sandbox, err := beachsandbox.GetSandbox(instanceRoot) - if err != nil && !errors.Is(err, beachsandbox.ErrNoFlowFound) { - log.Fatal(err) - return - } - commandArgs := []string{"compose", "-f", sandbox.DockerComposeFilePath, "rm", "--force", "--stop", "-v"} - output, err := exec.RunCommand("docker", commandArgs) - if err != nil { - log.Fatal(output) - return - } - } - - log.Info("Stopping reverse proxy and database server ...") - commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "docker-compose.yml"), "rm", "--force", "--stop", "-v"} - output, err := exec.RunCommand("docker", commandArgs) - if err != nil { - log.Fatal(output) - return - } - - return -} - -func findInstanceRoots() ([]string, error) { - var configurationFiles []string - - output, err := exec.RunCommand("docker", []string{"ps", "-q", "--filter", "network=local_beach"}) - if err != nil { - return nil, errors.New(output) - } - for _, line := range strings.Split(output, "\n") { - containerID := strings.TrimSpace(line) - if len(containerID) > 0 { - output, err := exec.RunCommand("docker", []string{"inspect", "-f", "{{index .Config.Labels \"com.docker.compose.project.config_files\"}}", containerID}) - if err != nil { - return nil, errors.New(output) - } - projectDirectory := filepath.Dir(strings.TrimSpace(output)) - if containsLocalBeachInstance(projectDirectory) { - configurationFiles = append(configurationFiles, projectDirectory) - } - } - } - - return removeDuplicates(configurationFiles), nil -} - -func containsLocalBeachInstance(path string) bool { - path = filepath.Join(path, ".localbeach.docker-compose.yaml") - _, err := os.Stat(path) - return !errors.Is(err, os.ErrNotExist) -} - -func removeDuplicates(strSlice []string) []string { - allKeys := make(map[string]bool) - var list []string - for _, item := range strSlice { - if _, value := allKeys[item]; !value { - allKeys[item] = true - list = append(list, item) - } - } - return list + bringBeachDown() } diff --git a/cmd/beach/cmd/helpers.go b/cmd/beach/cmd/helpers.go index d14c74a..3eebfff 100644 --- a/cmd/beach/cmd/helpers.go +++ b/cmd/beach/cmd/helpers.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/flownative/localbeach/pkg/beachsandbox" "github.com/flownative/localbeach/pkg/exec" "github.com/flownative/localbeach/pkg/path" log "github.com/sirupsen/logrus" @@ -228,3 +229,76 @@ func startLocalBeach() error { } return nil } + +func bringBeachDown() bool { + instanceRoots, err := findInstanceRoots() + if err != nil { + log.Fatal(err) + return false + } + for _, instanceRoot := range instanceRoots { + log.Info("Stopping instance in " + instanceRoot + "...") + sandbox, err := beachsandbox.GetSandbox(instanceRoot) + if err != nil && !errors.Is(err, beachsandbox.ErrNoFlowFound) { + log.Fatal(err) + return false + } + commandArgs := []string{"compose", "-f", sandbox.DockerComposeFilePath, "rm", "--force", "--stop", "-v"} + output, err := exec.RunCommand("docker", commandArgs) + if err != nil { + log.Fatal(output) + return false + } + } + + log.Info("Stopping reverse proxy and database server ...") + commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "docker-compose.yml"), "rm", "--force", "--stop", "-v"} + output, err := exec.RunCommand("docker", commandArgs) + if err != nil { + log.Fatal(output) + return false + } + return true +} + +func findInstanceRoots() ([]string, error) { + var configurationFiles []string + + output, err := exec.RunCommand("docker", []string{"ps", "-q", "--filter", "network=local_beach"}) + if err != nil { + return nil, errors.New(output) + } + for _, line := range strings.Split(output, "\n") { + containerID := strings.TrimSpace(line) + if len(containerID) > 0 { + output, err := exec.RunCommand("docker", []string{"inspect", "-f", "{{index .Config.Labels \"com.docker.compose.project.config_files\"}}", containerID}) + if err != nil { + return nil, errors.New(output) + } + projectDirectory := filepath.Dir(strings.TrimSpace(output)) + if containsLocalBeachInstance(projectDirectory) { + configurationFiles = append(configurationFiles, projectDirectory) + } + } + } + + return removeDuplicates(configurationFiles), nil +} + +func containsLocalBeachInstance(path string) bool { + path = filepath.Join(path, ".localbeach.docker-compose.yaml") + _, err := os.Stat(path) + return !errors.Is(err, os.ErrNotExist) +} + +func removeDuplicates(strSlice []string) []string { + allKeys := make(map[string]bool) + var list []string + for _, item := range strSlice { + if _, value := allKeys[item]; !value { + allKeys[item] = true + list = append(list, item) + } + } + return list +} From 6e9bb8b62ebd1de30e093eb4339cc18596114f8c Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Tue, 14 Oct 2025 13:37:59 +0200 Subject: [PATCH 06/10] Bring Local Beach down before database migration --- cmd/beach/cmd/setup.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 6c99d6e..0d3f9cc 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -100,7 +100,16 @@ func migrateOldBase() error { func setupLocalBeach() error { log.Debug("setting up Local Beach with base path " + path.Base) - err := os.MkdirAll(path.Base, os.ModePerm) + databaseStatusOutput, err := exec.RunCommand("docker", []string{"ps", "--filter", "name=local_beach_database", "--filter", "status=running", "-q"}) + if err != nil { + log.Error(errors.New("failed checking status of container local_beach_database container")) + } + + if len(databaseStatusOutput) != 0 { + bringBeachDown() + } + + err = os.MkdirAll(path.Base, os.ModePerm) if err != nil { log.Error(err) } From 8a4a8eb531febefcfd0d095ac5eb7eeb8e518709 Mon Sep 17 00:00:00 2001 From: Robert Lemke Date: Tue, 14 Oct 2025 15:59:55 +0200 Subject: [PATCH 07/10] Improve database migration safety and UX - Add migration marker file to prevent re-running - Improve error handling with defer cleanup - Add progress indicators for each step - Add verification before suggesting data deletion --- cmd/beach/cmd/setup.go | 137 ++++++++++++++++++++++++++++++++++------- 1 file changed, 115 insertions(+), 22 deletions(-) diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 0d3f9cc..904ae05 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -16,6 +16,7 @@ package cmd import ( "errors" + "fmt" "os" "path/filepath" "strings" @@ -143,42 +144,134 @@ func setupLocalBeach() error { } func migrateMariaDBToMySQL() error { - _, err := os.Stat(path.MariaDBDatabase) - if err == nil { - log.Info("Migrating MariaDB data from " + path.MariaDBDatabase + " to MySQL at " + path.MySQLDatabase) - log.Warn("Note: This may take a while, depending on DB size!") + migrationMarkerPath := filepath.Join(path.Base, ".mariadb-to-mysql-migration-complete") - if err = startMariaDB(); err != nil { - return err + // Check if migration has already been completed + if _, err := os.Stat(migrationMarkerPath); err == nil { + log.Debug("MariaDB to MySQL migration already completed, skipping") + return nil + } + + // Check if MariaDB data exists + _, err := os.Stat(path.MariaDBDatabase) + if err != nil { + // No MariaDB data to migrate + if os.IsNotExist(err) { + log.Debug("No MariaDB data found, skipping migration") + return nil } + return fmt.Errorf("failed to check MariaDB database path: %w", err) + } - log.Debug("dumping data from MariaDB to MySQL") - commandArgs := []string{"exec", "local_beach_mariadb", "bash", "-c"} - commandArgs = append(commandArgs, "mysql -h local_beach_mariadb -u root -ppassword --batch --skip-column-names -e \"SHOW DATABASES;\" | grep -E -v \"(information|performance)_schema|mysql|sys\"") - databases, err := exec.RunCommand("docker", commandArgs) - if err != nil { - log.Error(err) - return err + log.Info("Migrating MariaDB data from " + path.MariaDBDatabase + " to MySQL at " + path.MySQLDatabase) + log.Warn("This process may take several minutes depending on database size.") + log.Warn("Please do not interrupt this process!") + + // Start both database servers + if err = startMariaDB(); err != nil { + return fmt.Errorf("failed to start database servers for migration: %w", err) + } + + // Ensure cleanup happens on error + defer func() { + if stopErr := stopMariaDB(); stopErr != nil { + log.Error("Failed to stop MariaDB after migration: ", stopErr) } + }() - for _, database := range strings.Split(strings.TrimSuffix(databases, "\n"), "\n") { - log.Debug("… " + database) + // Get list of databases to migrate + log.Info("Step 1/3: Discovering databases to migrate...") + commandArgs := []string{"exec", "local_beach_mariadb", "bash", "-c"} + commandArgs = append(commandArgs, "mysql -h local_beach_mariadb -u root -ppassword --batch --skip-column-names -e \"SHOW DATABASES;\" | grep -E -v \"(information|performance)_schema|mysql|sys\"") + databases, err := exec.RunCommand("docker", commandArgs) + if err != nil { + return fmt.Errorf("failed to list databases from MariaDB: %w", err) + } + + databaseList := strings.Split(strings.TrimSuffix(databases, "\n"), "\n") + if len(databaseList) == 0 || (len(databaseList) == 1 && databaseList[0] == "") { + log.Info("No databases found to migrate") + } else { + log.Info(fmt.Sprintf("Found %d database(s) to migrate", len(databaseList))) + + // Migrate each database + log.Info("Step 2/3: Migrating databases...") + migratedCount := 0 + for i, database := range databaseList { + if database == "" { + continue + } + log.Info(fmt.Sprintf(" [%d/%d] Migrating database: %s", i+1, len(databaseList), database)) commandArgs = []string{"exec", "local_beach_database", "bash", "-c"} commandArgs = append(commandArgs, "mysqldump -h local_beach_mariadb -u root -ppassword --add-drop-trigger --compress --comments --dump-date --hex-blob --quote-names --routines --triggers --no-autocommit --no-tablespaces --skip-lock-tables --single-transaction --quick --databases "+database+" | sed -e \"s/DEFAULT '{}' COMMENT '(DC2Type:json)'/DEFAULT (JSON_OBJECT()) COMMENT '(DC2Type:json)'/\" | mysql -h local_beach_database -u root -ppassword") - _, err := exec.RunCommand("docker", commandArgs) + output, err := exec.RunCommand("docker", commandArgs) if err != nil { - log.Error(err) + log.Error(fmt.Sprintf("Failed to migrate database %s: %v", database, err)) + if output != "" { + log.Error("Output: ", output) + } + return fmt.Errorf("migration failed for database %s: %w", database, err) } + migratedCount++ + log.Info(fmt.Sprintf(" [%d/%d] Successfully migrated: %s", i+1, len(databaseList), database)) } + log.Info(fmt.Sprintf("Successfully migrated %d database(s)", migratedCount)) + } - if err = stopMariaDB(); err != nil { - return err - } + // Verify migration + log.Info("Step 3/3: Verifying migration...") + if err = verifyMigration(); err != nil { + return fmt.Errorf("migration verification failed: %w", err) + } - log.Info("Done with migration to MySQL at " + path.MySQLDatabase) - log.Info("If all works as expected, remove " + path.MariaDBDatabase) + // Stop MariaDB + if err = stopMariaDB(); err != nil { + return fmt.Errorf("failed to stop MariaDB after migration: %w", err) + } + + // Create migration marker file + markerFile, err := os.Create(migrationMarkerPath) + if err != nil { + log.Warn("Failed to create migration marker file: ", err) + log.Warn("Migration completed successfully, but may run again on next setup") + } else { + timestamp := time.Now().Format(time.RFC3339) + _, _ = markerFile.WriteString(fmt.Sprintf("Migration completed at: %s\n", timestamp)) + markerFile.Close() } + log.Info("✓ Migration to MySQL completed successfully!") + log.Info("") + log.Info("Your MariaDB data has been preserved at: " + path.MariaDBDatabase) + log.Info("Once you've verified everything works correctly, you can safely remove it with:") + log.Info(" rm -rf " + path.MariaDBDatabase) + + return nil +} + +func verifyMigration() error { + // Check that MySQL server is running and accessible + commandArgs := []string{"exec", "local_beach_database", "bash", "-c"} + commandArgs = append(commandArgs, "mysql -h local_beach_database -u root -ppassword --batch --skip-column-names -e \"SELECT 'OK';\"") + output, err := exec.RunCommand("docker", commandArgs) + if err != nil { + return fmt.Errorf("failed to connect to MySQL: %w", err) + } + if !strings.Contains(output, "OK") { + return errors.New("MySQL connection test failed") + } + + // Get database count from MySQL + commandArgs = []string{"exec", "local_beach_database", "bash", "-c"} + commandArgs = append(commandArgs, "mysql -h local_beach_database -u root -ppassword --batch --skip-column-names -e \"SHOW DATABASES;\" | grep -E -v \"(information|performance)_schema|mysql|sys\" | wc -l") + mysqlDbCount, err := exec.RunCommand("docker", commandArgs) + if err != nil { + return fmt.Errorf("failed to count MySQL databases: %w", err) + } + + mysqlDbCount = strings.TrimSpace(mysqlDbCount) + log.Info(fmt.Sprintf("Verification: Found %s database(s) in MySQL", mysqlDbCount)) + return nil } From a53e8b8bf2a2279a80589eb64ae02db69fd61e20 Mon Sep 17 00:00:00 2001 From: Robert Lemke Date: Tue, 14 Oct 2025 16:03:08 +0200 Subject: [PATCH 08/10] Refactor helper functions for better error handling - Change bringBeachDown() to return error instead of bool - Consolidate compose file writing into writeComposeFile() - Update all callers to properly handle errors --- cmd/beach/cmd/down.go | 5 ++- cmd/beach/cmd/helpers.go | 75 +++++++++++++++++++++------------------- cmd/beach/cmd/setup.go | 4 ++- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/cmd/beach/cmd/down.go b/cmd/beach/cmd/down.go index 1bfc5a5..e9797dc 100644 --- a/cmd/beach/cmd/down.go +++ b/cmd/beach/cmd/down.go @@ -15,6 +15,7 @@ package cmd import ( + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -32,5 +33,7 @@ func init() { } func handleDownRun(cmd *cobra.Command, args []string) { - bringBeachDown() + if err := bringBeachDown(); err != nil { + log.Fatal(err) + } } diff --git a/cmd/beach/cmd/helpers.go b/cmd/beach/cmd/helpers.go index 3eebfff..0ca101a 100644 --- a/cmd/beach/cmd/helpers.go +++ b/cmd/beach/cmd/helpers.go @@ -145,40 +145,47 @@ func retrieveCloudStorageCredentials(instanceIdentifier string, projectNamespace return nil, bucketName, privateKey } -func writeLocalBeachComposeFile() { - composeFileContent := readFileFromAssets("local-beach/docker-compose.yml") - composeFileContent = strings.ReplaceAll(composeFileContent, "{{mysqlDatabasePath}}", path.MySQLDatabase) - composeFileContent = strings.ReplaceAll(composeFileContent, "{{certificatesPath}}", path.Certificates) +func writeComposeFile(assetPath, outputFileName string, replacements map[string]string) error { + composeFileContent := readFileFromAssets(assetPath) + + // Apply all replacements + for placeholder, value := range replacements { + composeFileContent = strings.ReplaceAll(composeFileContent, placeholder, value) + } - destination, err := os.Create(filepath.Join(path.Base, "docker-compose.yml")) + outputPath := filepath.Join(path.Base, outputFileName) + destination, err := os.Create(outputPath) if err != nil { - log.Error("failed creating docker-compose.yml: ", err) - } else { - _, err = destination.WriteString(composeFileContent) - if err != nil { - log.Error(err) - } + return fmt.Errorf("failed creating %s: %w", outputFileName, err) + } + defer destination.Close() + _, err = destination.WriteString(composeFileContent) + if err != nil { + return fmt.Errorf("failed writing to %s: %w", outputFileName, err) } - _ = destination.Close() -} -func writeMariaDBComposeFile() { - composeFileContent := readFileFromAssets("local-beach/mariadb-compose.yml") - composeFileContent = strings.ReplaceAll(composeFileContent, "{{mysqlDatabasePath}}", path.MySQLDatabase) - composeFileContent = strings.ReplaceAll(composeFileContent, "{{mariadbDatabasePath}}", path.MariaDBDatabase) + return nil +} - destination, err := os.Create(filepath.Join(path.Base, "mariadb-compose.yml")) - if err != nil { - log.Error("failed creating mariadb-compose.yml: ", err) - } else { - _, err = destination.WriteString(composeFileContent) - if err != nil { - log.Error(err) - } +func writeLocalBeachComposeFile() { + replacements := map[string]string{ + "{{mysqlDatabasePath}}": path.MySQLDatabase, + "{{certificatesPath}}": path.Certificates, + } + if err := writeComposeFile("local-beach/docker-compose.yml", "docker-compose.yml", replacements); err != nil { + log.Error(err) + } +} +func writeMariaDBComposeFile() { + replacements := map[string]string{ + "{{mysqlDatabasePath}}": path.MySQLDatabase, + "{{mariadbDatabasePath}}": path.MariaDBDatabase, + } + if err := writeComposeFile("local-beach/mariadb-compose.yml", "mariadb-compose.yml", replacements); err != nil { + log.Error(err) } - _ = destination.Close() } func startLocalBeach() error { @@ -230,24 +237,21 @@ func startLocalBeach() error { return nil } -func bringBeachDown() bool { +func bringBeachDown() error { instanceRoots, err := findInstanceRoots() if err != nil { - log.Fatal(err) - return false + return fmt.Errorf("failed to find instance roots: %w", err) } for _, instanceRoot := range instanceRoots { log.Info("Stopping instance in " + instanceRoot + "...") sandbox, err := beachsandbox.GetSandbox(instanceRoot) if err != nil && !errors.Is(err, beachsandbox.ErrNoFlowFound) { - log.Fatal(err) - return false + return fmt.Errorf("failed to get sandbox for %s: %w", instanceRoot, err) } commandArgs := []string{"compose", "-f", sandbox.DockerComposeFilePath, "rm", "--force", "--stop", "-v"} output, err := exec.RunCommand("docker", commandArgs) if err != nil { - log.Fatal(output) - return false + return fmt.Errorf("failed to stop instance in %s: %s", instanceRoot, output) } } @@ -255,10 +259,9 @@ func bringBeachDown() bool { commandArgs := []string{"compose", "-f", filepath.Join(path.Base, "docker-compose.yml"), "rm", "--force", "--stop", "-v"} output, err := exec.RunCommand("docker", commandArgs) if err != nil { - log.Fatal(output) - return false + return fmt.Errorf("failed to stop reverse proxy and database: %s", output) } - return true + return nil } func findInstanceRoots() ([]string, error) { diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 904ae05..dc83dbd 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -107,7 +107,9 @@ func setupLocalBeach() error { } if len(databaseStatusOutput) != 0 { - bringBeachDown() + if err := bringBeachDown(); err != nil { + return fmt.Errorf("failed to bring Beach down: %w", err) + } } err = os.MkdirAll(path.Base, os.ModePerm) From ab9b7f573f1c3de49927174b617d9738a4cde04d Mon Sep 17 00:00:00 2001 From: Robert Lemke Date: Tue, 14 Oct 2025 16:25:31 +0200 Subject: [PATCH 09/10] Fix JSON/BLOB default value handling in migration Remove all DEFAULT clauses from JSON/TEXT/BLOB columns instead of trying to convert them, as MySQL 8.0 doesn't support default values for these column types in the same way as MariaDB. Handles both quoted strings and function calls like json_object(). --- cmd/beach/cmd/setup.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index dc83dbd..807086d 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -205,7 +205,24 @@ func migrateMariaDBToMySQL() error { } log.Info(fmt.Sprintf(" [%d/%d] Migrating database: %s", i+1, len(databaseList), database)) commandArgs = []string{"exec", "local_beach_database", "bash", "-c"} - commandArgs = append(commandArgs, "mysqldump -h local_beach_mariadb -u root -ppassword --add-drop-trigger --compress --comments --dump-date --hex-blob --quote-names --routines --triggers --no-autocommit --no-tablespaces --skip-lock-tables --single-transaction --quick --databases "+database+" | sed -e \"s/DEFAULT '{}' COMMENT '(DC2Type:json)'/DEFAULT (JSON_OBJECT()) COMMENT '(DC2Type:json)'/\" | mysql -h local_beach_database -u root -ppassword") + // Remove DEFAULT clauses from JSON/TEXT/BLOB columns for MariaDB→MySQL 8.0 compatibility + // MySQL 8.0 is stricter about default values for these column types + // Need to handle both quoted strings ('{}'/'[]') and function calls (json_object()/json_array()) + sedPatterns := "" + + "sed -e \"s/ DEFAULT '[^']*' COMMENT '(DC2Type:json)'/ COMMENT '(DC2Type:json)'/g\" " + + "-e \"s/ DEFAULT ([^)]*) COMMENT '(DC2Type:json)'/ COMMENT '(DC2Type:json)'/g\" " + + "-e \"s/ DEFAULT '[^']*'\\( \\+COMMENT\\)/ \\1/g\" " + + "-e \"s/ DEFAULT ([^)]*)\\( \\+COMMENT\\)/ \\1/g\" " + + "-e \"s/ DEFAULT '[^']*'\\( \\+CHECK\\)/ \\1/g\" " + + "-e \"s/ DEFAULT ([^)]*)\\( \\+CHECK\\)/ \\1/g\" " + + "-e \"s/ DEFAULT [a-z_]*()\\( \\+CHECK\\)/ \\1/g\" " + + "-e \"s/ DEFAULT '[^']*'\\(,\\)/\\1/g\" " + + "-e \"s/ DEFAULT ([^)]*)\\(,\\)/\\1/g\" " + + "-e \"s/\\(json\\|text\\|blob\\|JSON\\|TEXT\\|BLOB\\) DEFAULT '[^']*'/\\1/g\" " + + "-e \"s/\\(json\\|text\\|blob\\|JSON\\|TEXT\\|BLOB\\) DEFAULT ([^)]*)/\\1/g\" " + + "-e \"s/\\(json\\|text\\|blob\\|JSON\\|TEXT\\|BLOB\\) DEFAULT [a-z_]*()\\( \\)/\\1\\2/g\" " + + "-e \"s/\\(json\\|text\\|blob\\|JSON\\|TEXT\\|BLOB\\) [a-z_]*()\\( \\)/\\1\\2/g\"" + commandArgs = append(commandArgs, "mysqldump -h local_beach_mariadb -u root -ppassword --add-drop-trigger --compress --comments --dump-date --hex-blob --quote-names --routines --triggers --no-autocommit --no-tablespaces --skip-lock-tables --single-transaction --quick --databases "+database+" | "+sedPatterns+" | mysql -h local_beach_database -u root -ppassword") output, err := exec.RunCommand("docker", commandArgs) if err != nil { log.Error(fmt.Sprintf("Failed to migrate database %s: %v", database, err)) From e677c494bcc758365f07d2b2fbf1a56ba1d7a047 Mon Sep 17 00:00:00 2001 From: Robert Lemke Date: Tue, 14 Oct 2025 16:29:24 +0200 Subject: [PATCH 10/10] Automatically start Local Beach after DB migration --- cmd/beach/cmd/setup.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/beach/cmd/setup.go b/cmd/beach/cmd/setup.go index 807086d..22132f8 100644 --- a/cmd/beach/cmd/setup.go +++ b/cmd/beach/cmd/setup.go @@ -142,6 +142,14 @@ func setupLocalBeach() error { writeLocalBeachComposeFile() + // Start Local Beach after setup/migration + log.Info("Starting Local Beach...") + err = startLocalBeach() + if err != nil { + log.Error("Failed to start Local Beach: ", err) + return err + } + return nil }