diff --git a/commands/project_convert.go b/commands/project_convert.go index 58114ff2..0364a7f3 100644 --- a/commands/project_convert.go +++ b/commands/project_convert.go @@ -2,22 +2,14 @@ package commands import ( "fmt" - "io" - "log" "os" - "path/filepath" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/symfony-cli/terminal" - "github.com/upsun/lib-sun/detector" - "github.com/upsun/lib-sun/entity" - "github.com/upsun/lib-sun/readers" - utils "github.com/upsun/lib-sun/utility" - "github.com/upsun/lib-sun/writers" orderedmap "github.com/wk8/go-ordered-map/v2" "github.com/platformsh/cli/internal/config" + "github.com/platformsh/cli/internal/convert" ) // innerProjectConvertCommand returns the Command struct for the convert config command. @@ -109,81 +101,14 @@ func newProjectConvertCommand(cnf *config.Config) *cobra.Command { // runProjectConvert is the entry point for the convert config command. func runProjectConvert(cmd *cobra.Command, _ []string) error { - if viper.GetString("provider") != "platformsh" { - return fmt.Errorf("only the 'platformsh' provider is currently supported") - } - return runPlatformShConvert(cmd) -} - -// runPlatformShConvert performs the conversion from Platform.sh config to Upsun config. -func runPlatformShConvert(cmd *cobra.Command) error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("could not get current working directory: %w", err) } - cwd, err = filepath.Abs(filepath.Clean(cwd)) - if err != nil { - return fmt.Errorf("could not normalize project workspace path: %w", err) - } - - // Disable log for lib-sun - log.Default().SetOutput(io.Discard) - - // Find config files - configFiles, err := detector.FindConfig(cwd) - if err != nil { - return fmt.Errorf("could not detect configuration files: %w", err) - } - - // Read PSH application config files - var metaConfig entity.MetaConfig - readers.ReadApplications(&metaConfig, configFiles[entity.PSH_APPLICATION], cwd) - readers.ReadPlatforms(&metaConfig, configFiles[entity.PSH_PLATFORM], cwd) - if metaConfig.Applications.IsZero() { - return fmt.Errorf("no Platform.sh applications found") + if viper.GetString("provider") == "platformsh" { + return convert.PlatformshToUpsun(cwd, cmd.ErrOrStderr()) } - // Read PSH services and routes config files - readers.ReadServices(&metaConfig, configFiles[entity.PSH_SERVICE]) - readers.ReadRoutes(&metaConfig, configFiles[entity.PSH_ROUTE]) - - // Remove size and resources entries - readers.RemoveAllEntry(&metaConfig.Services, "size") - readers.RemoveAllEntry(&metaConfig.Applications, "size") - readers.RemoveAllEntry(&metaConfig.Services, "resources") - readers.RemoveAllEntry(&metaConfig.Applications, "resources") - - // Fix storage to match Upsun format - readers.ReplaceAllEntry(&metaConfig.Applications, "local", "instance") - readers.ReplaceAllEntry(&metaConfig.Applications, "shared", "storage") - readers.RemoveAllEntry(&metaConfig.Applications, "disk") - - upsunDir := filepath.Join(cwd, ".upsun") - if err := os.MkdirAll(upsunDir, os.ModePerm); err != nil { - return fmt.Errorf("could not create .upsun directory: %w", err) - } - - configPath := filepath.Join(upsunDir, "config.yaml") - stat, err := os.Stat(configPath) - if err == nil && !stat.IsDir() { - cmd.Printf("The file %v already exists.\n", configPath) - if !viper.GetBool("yes") { - if viper.GetBool("no-interaction") { - return fmt.Errorf("use the -y option to overwrite the file") - } - - if !terminal.AskConfirmation("Do you want to overwrite it?", true) { - return nil - } - } - } - writers.GenerateUpsunConfigFile(metaConfig, configPath) - - // Move extra config - utils.TransferConfigCustom(cwd, upsunDir) - - cmd.Println("Your configuration was successfully converted to the Upsun format.") - cmd.Printf("Check the generated files in %v\n", upsunDir) - return nil + return fmt.Errorf("only the 'platformsh' provider is currently supported") } diff --git a/internal/convert/platformsh.go b/internal/convert/platformsh.go new file mode 100644 index 00000000..67eaeee9 --- /dev/null +++ b/internal/convert/platformsh.go @@ -0,0 +1,93 @@ +package convert + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + + "github.com/fatih/color" + "github.com/spf13/viper" + "github.com/symfony-cli/terminal" + "github.com/upsun/lib-sun/detector" + "github.com/upsun/lib-sun/entity" + "github.com/upsun/lib-sun/readers" + utils "github.com/upsun/lib-sun/utility" + "github.com/upsun/lib-sun/writers" +) + +// PlatformshToUpsun performs the conversion from Platform.sh config to Upsun config. +func PlatformshToUpsun(path string, stderr io.Writer) error { + cwd, err := filepath.Abs(filepath.Clean(path)) + if err != nil { + return fmt.Errorf("could not normalize project workspace path: %w", err) + } + + upsunDir := filepath.Join(cwd, ".upsun") + configPath := filepath.Join(upsunDir, "config.yaml") + stat, err := os.Stat(configPath) + if err == nil && !stat.IsDir() { + fmt.Fprintln(stderr, "The file already exists:", color.YellowString(configPath)) + if !viper.GetBool("yes") { + if viper.GetBool("no-interaction") { + return fmt.Errorf("use the -y option to overwrite the file") + } + + if !terminal.AskConfirmation("Do you want to overwrite it?", true) { + return nil + } + } + } + + log.Default().SetOutput(stderr) + + // Find config files + configFiles, err := detector.FindConfig(cwd) + if err != nil { + return fmt.Errorf("could not detect configuration files: %w", err) + } + + // Read PSH application config files + var metaConfig entity.MetaConfig + readers.ReadApplications(&metaConfig, configFiles[entity.PSH_APPLICATION], cwd) + readers.ReadPlatforms(&metaConfig, configFiles[entity.PSH_PLATFORM], cwd) + if metaConfig.Applications.IsZero() { + return fmt.Errorf("no Platform.sh applications found") + } + + // Read PSH services and routes config files + readers.ReadServices(&metaConfig, configFiles[entity.PSH_SERVICE]) + readers.ReadRoutes(&metaConfig, configFiles[entity.PSH_ROUTE]) + + // Remove size and resources entries + fmt.Fprintln(stderr, "Removing any `size`, `resources` or `disk` keys.") + fmt.Fprintln(stderr, + "Upsun disk sizes are set using Console or the "+color.GreenString("upsun resources:set")+" command.") + readers.RemoveAllEntry(&metaConfig.Services, "size") + readers.RemoveAllEntry(&metaConfig.Applications, "size") + readers.RemoveAllEntry(&metaConfig.Services, "resources") + readers.RemoveAllEntry(&metaConfig.Applications, "resources") + readers.RemoveAllEntry(&metaConfig.Applications, "disk") + readers.RemoveAllEntry(&metaConfig.Services, "disk") + + // Fix storage to match Upsun format + fmt.Fprintln(stderr, "Replacing mount types (`local` becomes `instance`, and `shared` becomes `storage`).") + readers.ReplaceAllEntry(&metaConfig.Applications, "local", "instance") + readers.ReplaceAllEntry(&metaConfig.Applications, "shared", "storage") + + if err := os.MkdirAll(upsunDir, os.ModePerm); err != nil { + return fmt.Errorf("could not create .upsun directory: %w", err) + } + + fmt.Fprintln(stderr, "Creating combined configuration file.") + writers.GenerateUpsunConfigFile(metaConfig, configPath) + + // Move extra config + fmt.Fprintln(stderr, "Copying additional files if necessary.") + utils.TransferConfigCustom(cwd, upsunDir) + + fmt.Fprintln(stderr, "Your configuration was successfully converted to the Upsun format.") + fmt.Fprintln(stderr, "Check the generated files in:", color.GreenString(upsunDir)) + return nil +} diff --git a/internal/convert/platformsh_test.go b/internal/convert/platformsh_test.go new file mode 100644 index 00000000..fbb5d0bb --- /dev/null +++ b/internal/convert/platformsh_test.go @@ -0,0 +1,26 @@ +package convert + +import ( + _ "embed" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//go:embed testdata/platformsh/.upsun/config-ref.yaml +var configRef string + +func TestConvert(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.CopyFS(tmpDir, os.DirFS("testdata/platformsh"))) + assert.NoError(t, PlatformshToUpsun(tmpDir, t.Output())) + assert.FileExists(t, filepath.Join(tmpDir, ".upsun", "config.yaml")) + + b, err := os.ReadFile(filepath.Join(tmpDir, ".upsun", "config.yaml")) + assert.NoError(t, err) + + assert.Equal(t, configRef, string(b)) +} diff --git a/internal/convert/testdata/platformsh/.platform.app.yaml b/internal/convert/testdata/platformsh/.platform.app.yaml new file mode 100644 index 00000000..0da66d68 --- /dev/null +++ b/internal/convert/testdata/platformsh/.platform.app.yaml @@ -0,0 +1,67 @@ +name: app + +# Runtime pre-install +type: 'php:8.2' + +# Disk for App +disk: 2048 + +# Flexible Ressources +resources: + base_memory: 1024 + memory_ratio: 1024 + +dependencies: + php: + composer/composer: "^2" + +# vHost config +web: + locations: + "/": + root: "public" + passthru: "/index.php" + allow: true + scripts: true + +relationships: + database: "mysql:mysql" + +variables: + env: + CI_ENVIRONMENT: "production" + +# RW fs !! +mounts: + "writable/cache": + source: local + source_path: "writable/cache" + "writable/debugbar": { source: local, source_path: "writable/debugbar"} + "writable/logs": + source: local + source_path: "writable/logs" + "writable/session": + source: local + source_path: "writable/session" + "writable/upload": + source: local + source_path: "writable/upload" + "config": + source: local + source_path: "config" + +# Custom commands +hooks: + build: | + set -e + composer install --no-dev --optimize-autoloader + deploy: | + set -e + php generate_env.php + +source: + operations: + auto-update: + command: | + curl -fsS https://raw.githubusercontent.com/platformsh/source-operations/main/setup.sh | { bash /dev/fd/3 sop-autoupdate; } 3<&0 + diff --git a/internal/convert/testdata/platformsh/.platform/applications.yaml b/internal/convert/testdata/platformsh/.platform/applications.yaml new file mode 100644 index 00000000..e6379756 --- /dev/null +++ b/internal/convert/testdata/platformsh/.platform/applications.yaml @@ -0,0 +1,121 @@ +- name: drupal + type: php:8.1 + source: + root: drupal + dependencies: + php: + composer/composer: ^2 + nodejs: + n: "*" + variables: + env: + N_PREFIX: /app/.global + php: + memory_limit: "256M" + runtime: + extensions: + - redis + - newrelic + - apcu + relationships: + database: drupaldb:mysql + databox: drupaldb:databox + redis: cache:redis + auctionssearch: search_solr:auctionssearch + databasesearch: search_solr:databasesearch + userssearch: search_solr:userssearch + orderssearch: search_solr:orderssearch + collectionsearch: search_solr:collectionsearch + disk: 16384 + resources: + base_memory: 1024 + memory_ratio: 1024 + mounts: + web/sites/default/files: + source: local + source_path: files + /tmp: + source: local + source_path: tmp + /private: + source: local + source_path: private + /.drush: + source: local + source_path: drush + /drush-backups: + source: local + source_path: drush-backups + /.console: + source: local + source_path: console + /storage: + source: local + source_path: storage + build: + flavor: none + hooks: + build: | + set -e + n auto + hash -r + composer install --no-dev --prefer-dist --no-progress --no-interaction --optimize-autoloader --apcu-autoloader + composer dumpautoload -o + curl -fsS https://platform.sh/cli/installer | php + deploy: | + set -e + php ./drush/platformsh_generate_drush_yml.php + drush -y updatedb + drush -y config-import + drush -y cache-rebuild + drush locale:check + drush locale:update + web: + locations: + /: + root: web + expires: 1d + passthru: /index.php + allow: false + headers: + Access-Control-Allow-Origin: "*" + rules: + \.(jpe?g|png|gif|svgz?|css|js|map|ico|bmp|eot|woff2?|otf|ttf|webmanifest)$: + allow: true + ^/robots\.txt: + allow: true + ^/sitemap\.xml$: + allow: true + ^/sites/sites\.php$: + scripts: false + ^/sites/[^/]+/settings.*?\.php$: + scripts: false + /sites/default/files: + allow: true + expires: 2w + passthru: /index.php + root: web/sites/default/files + scripts: false + rules: + ^/sites/default/files/(css|js): + expires: 2w + crons: + drupal: + spec: '*/5 * * * *' + cmd: drush core-cron + backup: + spec: '0 5 * * *' + cmd: | + if [ "$PLATFORM_ENVIRONMENT_TYPE" = production ]; then + platform backup:create --yes --no-wait + fi + workers: + queues: + size: S + disk: 1024 + commands: + start: php worker.php +- name: app2 + type: php:8.1 + source: + root: app2 diff --git a/internal/convert/testdata/platformsh/.platform/local/.gitkeep b/internal/convert/testdata/platformsh/.platform/local/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/internal/convert/testdata/platformsh/.platform/routes.yaml b/internal/convert/testdata/platformsh/.platform/routes.yaml new file mode 100644 index 00000000..38b70f0b --- /dev/null +++ b/internal/convert/testdata/platformsh/.platform/routes.yaml @@ -0,0 +1,8 @@ +# This is my default route +“https://{default}/“: + type: upstream + upstream: app:http +# Redirect just... +“http://{default}“: + type: redirect + to: “https://{default}/” \ No newline at end of file diff --git a/internal/convert/testdata/platformsh/.platform/services.yaml b/internal/convert/testdata/platformsh/.platform/services.yaml new file mode 100644 index 00000000..e1aa78f6 --- /dev/null +++ b/internal/convert/testdata/platformsh/.platform/services.yaml @@ -0,0 +1,35 @@ +sqldb: + # (https://docs.platform.sh/configuration/services/mysql.html#supported-versions) + type: mysql:10.5 + disk: 1024 + size: M + +timedb: + # (https://docs.platform.sh/configuration/services/influxdb.html#supported-versions) + type: influxdb:1.8 + disk: 1024 + +searchelastic: + # (https://docs.platform.sh/configuration/services/elasticsearch.html#supported-versions) + type: elasticsearch:7.10 + size: AUTO + disk: 9216 + resources: + base_memory: 512 + memory_ratio: 512 + +queuerabbit: + # (https://docs.platform.sh/configuration/services/rabbitmq.html#supported-versions) + type: rabbitmq:3.8 + # Canot be down size at 512Mo but consom 500Mo by default => Alerting (but why 512Mo limit ???) + # https://docs.platform.sh/configuration/services/rabbitmq.html#example-configuration + # https://www.rabbitmq.com/quorum-queues.html#resource-use + disk: 1024 + +headlessbrowser: + # (https://docs.platform.sh/configuration/services/headless-chrome.html#supported-versions) + type: chrome-headless:91 + #size: 4XL + resources: + base_memory: 512 + memory_ratio: 512 \ No newline at end of file diff --git a/internal/convert/testdata/platformsh/.platform/solr-config/config.json b/internal/convert/testdata/platformsh/.platform/solr-config/config.json new file mode 100644 index 00000000..e69de29b diff --git a/internal/convert/testdata/platformsh/.upsun/.gitkeep b/internal/convert/testdata/platformsh/.upsun/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/internal/convert/testdata/platformsh/.upsun/config-ref.yaml b/internal/convert/testdata/platformsh/.upsun/config-ref.yaml new file mode 100644 index 00000000..98772d96 --- /dev/null +++ b/internal/convert/testdata/platformsh/.upsun/config-ref.yaml @@ -0,0 +1,192 @@ +applications: + drupal: + type: php:8.1 + source: + root: drupal + dependencies: + php: + composer/composer: ^2 + nodejs: + n: "*" + variables: + env: + N_PREFIX: /app/.global + php: + memory_limit: "256M" + runtime: + extensions: + - redis + - newrelic + - apcu + relationships: + database: drupaldb:mysql + databox: drupaldb:databox + redis: cache:redis + auctionssearch: search_solr:auctionssearch + databasesearch: search_solr:databasesearch + userssearch: search_solr:userssearch + orderssearch: search_solr:orderssearch + collectionsearch: search_solr:collectionsearch + mounts: + web/sites/default/files: + source: instance + source_path: files + /tmp: + source: instance + source_path: tmp + /private: + source: instance + source_path: private + /.drush: + source: instance + source_path: drush + /drush-backups: + source: instance + source_path: drush-backups + /.console: + source: instance + source_path: console + /storage: + source: instance + source_path: storage + build: + flavor: none + hooks: + build: | + set -e + n auto + hash -r + composer install --no-dev --prefer-dist --no-progress --no-interaction --optimize-autoloader --apcu-autoloader + composer dumpautoload -o + curl -fsS https://platform.sh/cli/installer | php + deploy: | + set -e + php ./drush/platformsh_generate_drush_yml.php + drush -y updatedb + drush -y config-import + drush -y cache-rebuild + drush locale:check + drush locale:update + web: + locations: + /: + root: web + expires: 1d + passthru: /index.php + allow: false + headers: + Access-Control-Allow-Origin: "*" + rules: + \.(jpe?g|png|gif|svgz?|css|js|map|ico|bmp|eot|woff2?|otf|ttf|webmanifest)$: + allow: true + ^/robots\.txt: + allow: true + ^/sitemap\.xml$: + allow: true + ^/sites/sites\.php$: + scripts: false + ^/sites/[^/]+/settings.*?\.php$: + scripts: false + /sites/default/files: + allow: true + expires: 2w + passthru: /index.php + root: web/sites/default/files + scripts: false + rules: + ^/sites/default/files/(css|js): + expires: 2w + crons: + drupal: + spec: '*/5 * * * *' + cmd: drush core-cron + backup: + spec: '0 5 * * *' + cmd: | + if [ "$PLATFORM_ENVIRONMENT_TYPE" = production ]; then + platform backup:create --yes --no-wait + fi + workers: + queues: + commands: + start: php worker.php + app2: + type: php:8.1 + source: + root: app2 + app: + # Runtime pre-install + type: 'php:8.2' + dependencies: + php: + composer/composer: "^2" + # vHost config + web: + locations: + "/": + root: "public" + passthru: "/index.php" + allow: true + scripts: true + relationships: + database: "mysql:mysql" + variables: + env: + CI_ENVIRONMENT: "production" + # RW fs !! + mounts: + "writable/cache": + source: instance + source_path: "writable/cache" + "writable/debugbar": {source: instance, source_path: "writable/debugbar"} + "writable/logs": + source: instance + source_path: "writable/logs" + "writable/session": + source: instance + source_path: "writable/session" + "writable/upload": + source: instance + source_path: "writable/upload" + "config": + source: instance + source_path: "config" + # Custom commands + hooks: + build: | + set -e + composer install --no-dev --optimize-autoloader + deploy: | + set -e + php generate_env.php + source: + operations: + auto-update: + command: | + curl -fsS https://raw.githubusercontent.com/platformsh/source-operations/main/setup.sh | { bash /dev/fd/3 sop-autoupdate; } 3<&0 + root: / +services: + sqldb: + # (https://docs.platform.sh/configuration/services/mysql.html#supported-versions) + type: mysql:10.5 + timedb: + # (https://docs.platform.sh/configuration/services/influxdb.html#supported-versions) + type: influxdb:1.8 + searchelastic: + # (https://docs.platform.sh/configuration/services/elasticsearch.html#supported-versions) + type: elasticsearch:7.10 + queuerabbit: + # (https://docs.platform.sh/configuration/services/rabbitmq.html#supported-versions) + type: rabbitmq:3.8 + headlessbrowser: + # (https://docs.platform.sh/configuration/services/headless-chrome.html#supported-versions) + type: chrome-headless:91 +routes: + # This is my default route + “https://{default}/“: + type: upstream + upstream: app:http + # Redirect just... + “http://{default}“: + type: redirect + to: “https://{default}/” diff --git a/internal/convert/testdata/platformsh/README b/internal/convert/testdata/platformsh/README new file mode 100644 index 00000000..0aab193b --- /dev/null +++ b/internal/convert/testdata/platformsh/README @@ -0,0 +1 @@ +This directory is embedded for testing.