diff --git a/Gemfile b/Gemfile index e895887..45c8d88 100644 --- a/Gemfile +++ b/Gemfile @@ -29,6 +29,7 @@ gem "aws-sdk-s3" gem "dalli" gem "dotenv-rails", "~> 2.7" gem "letter_opener_web", "~> 2.0" +gem "multipart-post" gem "spring" group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 4bc13f4..57cd867 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -301,14 +301,6 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - carrierwave (2.2.6) - activemodel (>= 5.0.0) - activesupport (>= 5.0.0) - addressable (~> 2.6) - image_processing (~> 1.1) - marcel (~> 1.0.0) - mini_mime (>= 0.1.3) - ssrf_filter (~> 1.0) cells (4.1.8) declarative-builder (~> 0.2.0) declarative-option (< 0.2.0) @@ -409,23 +401,13 @@ GEM activemodel (>= 3.2) mime-types (>= 1.0) flamegraph (0.9.5) - fog-aws (3.21.0) - fog-core (~> 2.1) - fog-json (~> 1.1) - fog-xml (~> 0.1) fog-core (2.6.0) builder excon (~> 1.0) formatador (>= 0.2, < 2.0) mime-types - fog-json (1.2.0) - fog-core - multi_json (~> 1.10) fog-local (0.8.0) fog-core (>= 1.27, < 3.0) - fog-xml (0.1.4) - fog-core - nokogiri (>= 1.5.11, < 2.0.0) formatador (1.1.0) foundation_rails_helper (4.0.1) actionpack (>= 4.1, < 7.1) @@ -532,9 +514,9 @@ GEM mini_mime (1.1.5) minitest (5.25.1) msgpack (1.7.5) - multi_json (1.15.0) multi_xml (0.7.1) bigdecimal (~> 3.1) + multipart-post (2.4.1) net-http (0.5.0) uri net-imap (0.5.1) @@ -813,7 +795,6 @@ GEM spring-watcher-listen (2.1.0) listen (>= 2.7, < 4.0) spring (>= 4) - ssrf_filter (1.1.2) stackprof (0.2.26) stringio (3.1.2) temple (0.10.3) @@ -892,7 +873,6 @@ DEPENDENCIES brakeman (~> 6.1) bullet byebug (~> 11.0) - carrierwave dalli decidim-accountability! decidim-admin! @@ -914,10 +894,10 @@ DEPENDENCIES decidim-verifications! dotenv-rails (~> 2.7) flamegraph - fog-aws letter_opener_web (~> 2.0) listen (~> 3.1) memory_profiler + multipart-post parallel_tests (~> 4.2) puma (>= 6.3.1) rack-mini-profiler diff --git a/app/views/layouts/decidim/admin/assemblies.html.erb b/app/views/layouts/decidim/admin/assemblies.html.erb new file mode 100644 index 0000000..c68689e --- /dev/null +++ b/app/views/layouts/decidim/admin/assemblies.html.erb @@ -0,0 +1,18 @@ +<% content_for :breadcrumb_context_menu do %> +
+<% end %> + +<%= render "layouts/decidim/admin/application" do %> + <%= yield %> +<% end %> diff --git a/lib/decidim_app/k8s/commands/admin.rb b/lib/decidim_app/k8s/commands/admin.rb new file mode 100644 index 0000000..affdc48 --- /dev/null +++ b/lib/decidim_app/k8s/commands/admin.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "decidim_app/k8s/manager" +require "decidim/core" + +module DecidimApp + module K8s + module Commands + class Admin + def self.run(configuration, organization) + new(configuration, organization).run + end + + def initialize(configuration, organization) + @configuration = configuration + @organization = organization + end + + def run + mapped_attributes = Decidim::AccountForm.from_model(existing_admin) + .attributes_with_values + .except(:avatar) + form = Decidim::AccountForm.from_params(mapped_attributes.merge(admin_params)) + .with_context(current_user: existing_admin, + current_organization: @organization) + + Decidim::UpdateAccount.call(existing_admin, form) do + on(:ok) do + K8s::Manager.logger.info("Admin user #{form.nickname} updated") + end + + on(:invalid) do + K8s::Manager.logger.info("Admin user #{form.nickname} could not be updated") + form.tap(&:valid?).errors.messages.each do |error| + K8s::Manager.logger.info(error) + end + + raise "Admin user #{form.nickname} could not be updated" + end + end + + existing_admin.reload + end + + def existing_admin + @existing_admin ||= Decidim::User.find_by(email: @configuration[:email], organization: @organization).tap(&:skip_confirmation!) + end + + def admin_params + @admin_params ||= { + password_confirmation: @configuration[:password], + tos_agreement: "1", + newsletter_notifications_at: existing_admin.confirmed_at || Time.zone.now, + admin_terms_accepted_at: existing_admin.confirmed_at || Time.zone.now, + confirmed_at: existing_admin.confirmed_at || Time.zone.now + }.merge(@configuration) + end + end + end + end +end diff --git a/lib/decidim_app/k8s/commands/organization.rb b/lib/decidim_app/k8s/commands/organization.rb new file mode 100644 index 0000000..87a34b5 --- /dev/null +++ b/lib/decidim_app/k8s/commands/organization.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require "decidim_app/k8s/manager" + +module DecidimApp + module K8s + module Commands + class Organization + def self.run(configuration, default_admin_configuration) + new(configuration, default_admin_configuration).run + end + + def initialize(configuration, default_admin_configuration) + @configuration = configuration + @default_admin_name = default_admin_configuration[:name] + @default_admin_email = default_admin_configuration[:email] + end + + def run + if existing_organization + K8s::Manager.logger.info("Organization #{@configuration[:name]} already exist") + + update + else + K8s::Manager.logger.info("Installing organization : '#{@configuration[:name]}'") + + install + end + end + + def install + form = Decidim::System::RegisterOrganizationForm.from_params( + @configuration.merge( + organization_admin_email: @default_admin_email, + organization_admin_name: @default_admin_name + ) + ) + + Decidim::System::RegisterOrganization.call(form) do + on(:ok) do + K8s::Manager.logger.info("Organization #{form.name} created") + update + end + + on(:invalid) do + K8s::Manager.logger.info("Organization #{form.name} could not be created") + form.tap(&:valid?).errors.messages.each do |error| + K8s::Manager.logger.info(error) + end + + raise "Organization #{form.name} could not be created" + end + end + + existing_organization + end + + def update + form = Decidim::System::UpdateOrganizationForm.from_params(update_params) + + Decidim::System::UpdateOrganization.call(existing_organization.id, form) do + on(:ok) do + K8s::Manager.logger.info("Organization #{form.name} updated") + end + + on(:invalid) do + K8s::Manager.logger.info("Organization #{form.name} could not be updated") + form.tap(&:valid?).errors.messages.each do |error| + K8s::Manager.logger.info(error) + end + + raise "Organization #{form.name} could not be updated" + end + end + + existing_organization.reload + end + + def existing_organization + Decidim::Organization.find_by(name: @configuration[:name]) || Decidim::Organization.find_by(host: @configuration[:host]) + end + + def existing_organization_attributes + Decidim::System::UpdateOrganizationForm.from_model(existing_organization).attributes_with_values.deep_symbolize_keys + end + + def update_params + params = existing_organization_attributes.deep_merge( + @configuration.except(:smtp_settings, :omniauth_settings) + ).merge(id: existing_organization.id) + + @configuration.fetch(:smtp_settings, {}).each do |key, value| + params.merge!(key => value) + end + + @configuration.fetch(:omniauth_settings, {}).each do |provider, config| + config.each do |key, value| + params.merge!("omniauth_settings_#{provider}_#{key}" => value) + end + end + + params[:encrypted_password] = nil if @configuration.dig(:smtp_settings, :password).present? + + params + end + end + end + end +end diff --git a/lib/decidim_app/k8s/commands/system_admin.rb b/lib/decidim_app/k8s/commands/system_admin.rb new file mode 100644 index 0000000..0eb2531 --- /dev/null +++ b/lib/decidim_app/k8s/commands/system_admin.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "decidim_app/k8s/manager" + +module DecidimApp + module K8s + module Commands + class SystemAdmin + def self.run(configuration) + new(configuration).run + end + + def initialize(configuration) + @configuration = configuration + end + + def run + system_admin = Decidim::System::Admin.find_or_initialize_by(email: @configuration[:email]) + + if system_admin.update(@configuration) + K8s::Manager.logger.info("System admin user #{system_admin.email} updated") + else + K8s::Manager.logger.info("System admin user #{system_admin.email} could not be updated") + system_admin.tap(&:valid?).errors.messages.each do |error| + K8s::Manager.logger.info(error) + end + + raise "System admin user #{system_admin.email} could not be updated" + end + + system_admin.reload + end + end + end + end +end diff --git a/lib/decidim_app/k8s/configuration.rb b/lib/decidim_app/k8s/configuration.rb new file mode 100644 index 0000000..f5d6b11 --- /dev/null +++ b/lib/decidim_app/k8s/configuration.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module DecidimApp + module K8s + class Configuration + attr_reader :organizations, :system_admin, :default_admin + + TRANSFORMS_METHODS = { + to_string_separated_by_new_line: ->(value) { value.join("\n") }, + to_string_separated_by_comma: ->(value) { value.join(",") } + }.freeze + + TRANSFORMS = { + secondary_hosts: :to_string_separated_by_new_line, + file_upload_settings_allowed_file_extensions_admin: :to_string_separated_by_comma, + file_upload_settings_allowed_file_extensions_image: :to_string_separated_by_comma, + file_upload_settings_allowed_file_extensions_default: :to_string_separated_by_comma, + file_upload_settings_allowed_content_types_admin: :to_string_separated_by_comma, + file_upload_settings_allowed_content_types_default: :to_string_separated_by_comma + }.freeze + + def initialize(path) + @parsed_configuration = YAML.load_file(path).deep_symbolize_keys + @organizations = set_organizations + @system_admin = @parsed_configuration[:system_admin] + @default_admin = @parsed_configuration[:default_admin] + end + + def valid? + instance_variables.none? do |variable| + instance_variable_get(variable).nil? + end + end + + def errors + return [] if valid? + + instance_variables.select { |variable| instance_variable_get(variable).nil? } + .map { |variable| "#{variable.to_s.gsub("@", "")} is required" } + .join(", ") + end + + private + + def set_organizations + organizations = @parsed_configuration[:organizations].is_a?(Hash) ? [@parsed_configuration[:organizations]] : @parsed_configuration[:organizations] + + organizations&.map { |organization| deep_transform(organization) } || [] + end + + # Transforms the keys based on the TRANSFORMS present + # Return a new hash with the transformed keys + # Example: + # To match against { file_upload_settings: { allowed_file_extensions: { admin } } } + # file_upload_settings_allowed_file_extensions_admin: ->(value) { value.join(",") } + def deep_transform(hash, prefix = "") + hash.each_with_object({}) do |(key, value), new_hash| + match_key = prefix.present? ? "#{prefix}_#{key}".to_sym : key + + new_hash[key] = if value.is_a?(Hash) + deep_transform(value, match_key) + else + transform(match_key, value) + end + end + end + + def transform(match_key, value) + return value unless TRANSFORMS[match_key] + + TRANSFORMS_METHODS[TRANSFORMS[match_key]].call(value) + end + end + end +end diff --git a/lib/decidim_app/k8s/configuration_exporter.rb b/lib/decidim_app/k8s/configuration_exporter.rb new file mode 100644 index 0000000..4470869 --- /dev/null +++ b/lib/decidim_app/k8s/configuration_exporter.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "logger_with_stdout" +require "decidim_app/k8s/organization_exporter" + +module DecidimApp + module K8s + class ConfigurationExporter + EXPORT_PATH = Rails.root.join("tmp/k8s-migration") + + def initialize(image = "") + @image = image + @organizations = Decidim::Organization.all + @logger = LoggerWithStdout.new("log/k8s-export-#{Time.zone.now.strftime("%Y-%m-%d-%H-%M-%S")}.log") + end + + def self.dump_db + new.dump_db + end + + def dump_db + @logger.info("found #{@organizations.count} organization#{"s" if @organizations.count.positive?}") + @logger.info("-------------------------") + @organizations.find_each do |organization| + @logger.info("Dumping database organization with host #{organization.host}") + K8s::OrganizationExporter.dumping_database(organization, @logger, EXPORT_PATH) + end + end + + def self.export!(image) + new(image).export! + end + + def export! + clean_migration_directory + + @logger.info("found #{@organizations.count} organization#{"s" if @organizations.count.positive?}") + @logger.info("-------------------------") + @organizations.find_each do |organization| + @logger.info("exporting organization with host #{organization.host}") + K8s::OrganizationExporter.export!(organization, @logger, EXPORT_PATH, @image) + end + end + + def clean_migration_directory + @logger.info("cleaning migration directory #{EXPORT_PATH}") + FileUtils.rm_rf(EXPORT_PATH) + @logger.info("creating migration directory #{EXPORT_PATH}") + FileUtils.mkdir_p(EXPORT_PATH) + end + end + end +end diff --git a/lib/decidim_app/k8s/manager.rb b/lib/decidim_app/k8s/manager.rb new file mode 100644 index 0000000..6c72b98 --- /dev/null +++ b/lib/decidim_app/k8s/manager.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require "yaml" + +require "decidim_app/k8s/configuration" +require "decidim_app/k8s/commands/organization" +require "decidim_app/k8s/commands/system_admin" +require "decidim_app/k8s/commands/admin" + +module DecidimApp + module K8s + class Manager + def initialize(path) + @configuration = Configuration.new(path) + end + + def self.run(path) + new(path).run + end + + def self.logger + @logger ||= LoggerWithStdout.new("log/decidim-app-k8s.log") + end + + def run + raise "Invalid configuration: #{@configuration.errors}" unless @configuration.valid? + + Commands::SystemAdmin.run(@configuration.system_admin) + @configuration.organizations.each do |organization| + organization = Commands::Organization.run(organization, @configuration.default_admin) + Commands::Admin.run(@configuration.default_admin, organization) + end + end + end + end +end diff --git a/lib/decidim_app/k8s/organization_exporter.rb b/lib/decidim_app/k8s/organization_exporter.rb new file mode 100644 index 0000000..0dd1519 --- /dev/null +++ b/lib/decidim_app/k8s/organization_exporter.rb @@ -0,0 +1,210 @@ +# frozen_string_literal: true + +require "uri" +require "net/http" +require "decidim_app/k8s/secondary_hosts_checker" + +module DecidimApp + module K8s + class OrganizationExporter + FORBIDDEN_ENVIRONMENT_KEYS = %w(BACKUP_ENABLED + BACKUP_S3SYNC_ENABLED + BACKUP_S3SYNC_ACCESS_KEY + BACKUP_S3SYNC_SECRET_KEY + BACKUP_S3SYNC_BUCKET + BACKUP_S3RETENTION_ENABLED + DEFAULT_LOCALE + AVAILABLE_LOCALES + FORCE_SSL + SCALEWAY_ID + SCALEWAY_TOKEN + SCALEWAY_BUCKET_NAME + SECRET_KEY_BASE + ENABLE_RACK_ATTACK).freeze + + DEFAULT_ENVIRONMENT_VARIABLES = { + "ENABLE_RACK_ATTACK" => 0 + }.freeze + + ORGANIZATION_COLUMNS = %w(id + default_locale + available_locales + users_registration_mode + force_users_to_authenticate_before_access_organization + available_authorizations + file_upload_settings).freeze + + def initialize(organization, logger, export_path, image = "") + @organization = organization + @logger = logger + @export_path = export_path + @image = image + @database_configuration = Rails.configuration.database_configuration[Rails.env].deep_symbolize_keys + end + + def self.export!(organization, logger, export_path, image) + new(organization, logger, export_path, image).export! + end + + def self.dumping_database(organization, logger, export_path) + new(organization, logger, export_path).dumping_database + end + + def export! + creating_directories + exporting_env_vars + exporting_configuration + end + + def dumping_database + @logger.info("dumping database #{@database_configuration[:database]} to #{organization_export_path}/postgres/#{resource_name}--de.dump") + + cmd = "pg_dump -Fc" + cmd += " -h '#{@database_configuration[:host]}'" if @database_configuration[:host].present? + cmd += " -p '#{@database_configuration[:port]}'" if @database_configuration[:port].present? + cmd += " -U '#{@database_configuration[:username]}'" if @database_configuration[:username].present? + cmd = "PGPASSWORD=#{@database_configuration[:password]} #{cmd}" if @database_configuration[:password].present? + cmd += " -d '#{@database_configuration[:database]}'" if @database_configuration[:database].present? + cmd += " -f #{organization_export_path}/postgres/#{resource_name}--de.dump" + + system(cmd) + end + + def exporting_configuration + @logger.info("exporting application configuration to #{organization_export_path}/application.yml") + File.write("#{organization_export_path}/application.yml", YAML.dump(organization_settings)) + end + + def exporting_env_vars + @logger.info("exporting env variables to #{organization_export_path}/manifests/#{resource_name}-custom-env.yml") + File.write("#{organization_export_path}/manifests/#{resource_name}-custom-env.yml", + YAML.dump(all_env_vars)) + @logger.info("exporting env variables to #{organization_export_path}/manifests/#{resource_name}--de.yml") + File.write("#{organization_export_path}/manifests/#{resource_name}--de.yml", + YAML.dump(secret_key_base_env_var)) + end + + def creating_directories + @logger.info("creating organization directories") + @logger.info("#{organization_export_path}/manifests") + FileUtils.mkdir_p("#{organization_export_path}/manifests") + @logger.info("#{organization_export_path}/postgres") + FileUtils.mkdir_p("#{organization_export_path}/postgres") + end + + def all_env_vars + { + apiVersion: "v1", + kind: "Secret", + metadata: { + name: "#{resource_name}-custom-env" + }, + stringData: env_vars.merge(smtp_settings).merge(omniauth_settings) + }.deep_stringify_keys + end + + def env_vars + @env_vars ||= Dotenv.parse(".env") + .except(*FORBIDDEN_ENVIRONMENT_KEYS) + .merge(DEFAULT_ENVIRONMENT_VARIABLES) + .transform_values(&:to_s) + end + + def secret_key_base_env_var + { + apiVersion: "v1", + kind: "Secret", + metadata: { + name: "#{resource_name}--de" + }, + stringData: { + SECRET_KEY_BASE: (Dotenv.parse(".env")["SECRET_KEY_BASE"]).to_s + } + }.deep_stringify_keys + end + + def omniauth_settings + return {} unless @organization.omniauth_settings + + settings = @organization.omniauth_settings + .deep_dup + .each_with_object({}) do |(key, value), hash| + hash[key.upcase] = Decidim::OmniauthProvider.value_defined?(value) ? decrypt(value) : value + end + + settings.deep_transform_values(&:to_s) + end + + def smtp_settings + settings = @organization.smtp_settings.deep_dup || {} + settings["password"] = Decidim::AttributeEncryptor.decrypt(settings["encrypted_password"]) if settings["encrypted_password"].present? + settings.delete("encrypted_password") + + settings = settings.transform_keys do |key| + "SMTP_#{key.upcase}" + end + + settings.deep_transform_values(&:to_s) + end + + def organization_columns + org_columns_sql = "SELECT row_to_json(o,true) FROM (SELECT #{ORGANIZATION_COLUMNS.join(", ")} FROM decidim_organizations WHERE id=#{@organization.id}) AS o;" + org_columns_record = ActiveRecord::Base.connection.execute(org_columns_sql) + JSON.parse(org_columns_record.first["row_to_json"]) + end + + def organization_settings + { + apiVersion: "apps.libre.sh/v1alpha1", + kind: "Decidim", + metadata: { + name: resource_name, + namespace: name_space + }, + spec: { + image: @image, + host: @organization.host, + additionalHosts: DecidimApp::K8s::SecondaryHostsChecker.valid_secondary_hosts(host: @organization.host, secondary_hosts: @organization.secondary_hosts), + organization: { id: organization_columns["id"] }, + locale: { + default: organization_columns["default_locale"], + available: organization_columns["available_locales"] + }, + usersRegistrationMode: organization_columns["users_registration_mode"], + forceUsersToAuthenticateBeforeAccessOrganization: organization_columns["force_users_to_authenticate_before_access_organization"], + availableAuthorizations: organization_columns["available_authorizations"], + fileUploadSettings: organization_columns["file_upload_settings"], + timeZone: @organization.time_zone, + envFrom: [ + { + secretRef: { + name: "#{resource_name}-custom-env" + } + } + ] + } + }.deep_stringify_keys + end + + def organization_export_path + @organization_export_path ||= "#{@export_path}/#{name_space}--#{resource_name}" + end + + def resource_name + @resource_name ||= @organization.host.split(".").first + end + + def name_space + @name_space ||= @organization.host.split(".", 2).last.gsub(".", "-") + end + + private + + def decrypt(value) + Decidim::AttributeEncryptor.decrypt(value) + rescue ActiveSupport::MessageEncryptor::InvalidMessage + value + end + end + end +end diff --git a/lib/decidim_app/k8s/secondary_hosts_checker.rb b/lib/decidim_app/k8s/secondary_hosts_checker.rb new file mode 100644 index 0000000..0c13f74 --- /dev/null +++ b/lib/decidim_app/k8s/secondary_hosts_checker.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module DecidimApp + module K8s + module SecondaryHostsChecker + def self.valid_secondary_hosts(host:, secondary_hosts:) + secondary_hosts.select do |secondary_host| + valid_secondary_host?(host, secondary_host) + end + end + + def self.valid_secondary_host?(host, secondary_host) + return true if host == secondary_host + + host == get_redirection_target(secondary_host) + end + + def self.get_redirection_target(host, limit = 3) + return nil if limit.zero? # Avoid infinite loops + return nil unless host.is_a?(URI) || host.is_a?(String) + + url = URI(host) + host = (url.host || url.path) + req = Net::HTTP::Get.new("/") + response = Net::HTTP.start(host, 80) { |http| http.request(req) } + + case response + when Net::HTTPSuccess + host + when Net::HTTPRedirection + get_redirection_target(response["location"], limit - 1) + end + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + nil + end + end + end +end diff --git a/lib/logger_with_stdout.rb b/lib/logger_with_stdout.rb new file mode 100644 index 0000000..c05aebe --- /dev/null +++ b/lib/logger_with_stdout.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class LoggerWithStdout < Logger + def initialize(*) + super + + def @logdev.write(msg) + super + + $stdout.puts(msg) + end + end +end diff --git a/lib/tasks/decidim_app.rake b/lib/tasks/decidim_app.rake new file mode 100644 index 0000000..7142a05 --- /dev/null +++ b/lib/tasks/decidim_app.rake @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require "net/http/post/multipart" +require "decidim_app/k8s/configuration_exporter" +require "decidim_app/k8s/organization_exporter" +require "decidim_app/k8s/manager" + +namespace :decidim_app do + desc "Setup Decidim-app" + task setup: :environment do + # :nocov: + puts "Running bundler installation" + system("bundle install") + puts "Installing engine migrations..." + system("bundle exec rake railties:install:migrations") + puts "Checking for migrations to apply..." + migrations = `bundle exec rake db:migrate:status | grep down` + if migrations.present? + puts "Missing migrations : +#{migrations}" + puts "Applying missing migrations..." + system("bundle exec rake db:migrate") + else + puts "All migrations are up" + end + + puts "Setup successfully terminated" + # :nocov: + end + + namespace :k8s do + # This task is used to install your decidim-app to the latest version + # Meant to be used in a CI/CD pipeline or a k8s job/operator + # You can add your own customizations here + desc "Install decidim-app" + task install: :environment do + puts "Running db:migrate" + Rake::Task["db:migrate"].invoke + Rake::Task["decidim_anonymous_proposals:generate_anonymous_group"].invoke + end + + # This task is used to upgrade your decidim-app to the latest version + # Meant to be used in a CI/CD pipeline or a k8s job/operator + # You can add your own customizations here + desc "Upgrade decidim-app" + task upgrade: :environment do + puts "Running upgrade db:migrate" + Rake::Task["db:migrate"].invoke + puts "Running decidim:repair:url_in_content" + Rake::Task["decidim:repair:url_in_content"].invoke + puts "Running decidim:repair:translations" + Rake::Task["decidim:repair:translations"].invoke + rescue StandardError => e + puts "Ignoring error: #{e.message}" + puts "Running decidim:db:migrate" + Rake::Task["decidim:db:migrate"].invoke + end + + desc "usage: bundle exec rails k8s:dump_db" + task dump_db: :environment do + DecidimApp::K8s::ConfigurationExporter.dump_db + end + + desc "usage: bundle exec rails k8s:export_configuration IMAGE=