#
# Copyright:: Copyright (c) 2015 GitLab Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'rainbow/ext/string'

require "#{base_path}/embedded/service/omnibus-ctl/lib/gitlab_ctl"

# For testing purposes, if the first path cannot be found load the second
begin
  require_relative '../../cookbooks/gitlab/libraries/pg_version'
rescue LoadError
  require_relative '../gitlab-cookbooks/gitlab/libraries/pg_version'
end

add_command 'upgrade', 'Run migrations after a package upgrade', 1 do |cmd_name|
  # On a fresh installation, run reconfigure automatically if EXTERNAL_URL is set
  unless File.exist?("/var/opt/gitlab/bootstrapped") || external_url_unset?
    code = reconfigure
    print_welcome_and_exit if code.zero?
    Kernel.exit code
  end

  service_statuses = `#{base_path}/bin/gitlab-ctl status`

  if /: runsv not running/ =~ service_statuses || service_statuses.empty?
    log 'It looks like GitLab has not been configured yet; skipping the upgrade '\
      'script.'
    print_welcome_and_exit
  end

  if GitlabCtl::Util.public_attributes_broken?
    log 'It looks like there was a problem with public attributes; run gitlab-ctl reconfigure manually to fix.'
    Kernel.exit 1
  end

  unless GitlabCtl::Util.progress_message('Checking PostgreSQL executables') do
    remove_old_node_state
    status = GitlabCtl::Util.chef_run('solo.rb', 'postgresql-bin.json')
    $stdout.puts status.stdout
    !status.error?
  end
    log 'Could not update PostgreSQL executables.'
  end

  auto_migrations_skip_file = "#{etc_path}/skip-auto-reconfigure"
  if File.exist?(auto_migrations_skip_file)
    log "Found #{auto_migrations_skip_file}, exiting..."
    print_upgrade_and_exit
  end

  run_pg_upgrade

  # Refresh service_statuses to ensure we pick up any new services
  service_statuses = `#{base_path}/bin/gitlab-ctl status`

  log 'Shutting down all GitLab services except those needed for migrations'
  MIGRATION_SERVICES = %w(postgresql patroni consul redis geo-postgresql gitaly praefect).freeze
  (get_all_services - MIGRATION_SERVICES).each do |sv_name|
    run_sv_command_for_service('stop', sv_name)
  end

  log 'Ensuring the required services are running'
  MIGRATION_SERVICES.each do |sv_name|
    # If the service is disabled, e.g. because we are using an external
    # Postgres server, or it is already running, then this command is a no-op.
    run_sv_command_for_service('start', sv_name)
  end

  # in case of downgrades, it might be necessary to remove the redis dump
  redis_dump = '/var/opt/gitlab/redis/dump.rdb'
  try_redis_restart = File.exist? redis_dump

  SERVICE_WAIT = 30
  MIGRATION_SERVICES.each do |sv_name|
    status = -1
    SERVICE_WAIT.times do
      status = run_sv_command_for_service('status', sv_name)
      break if status.zero?

      sleep 1
    end

    next if status.zero?

    if sv_name == 'redis' && try_redis_restart
      try_redis_restart = false
      log "Failed starting Redis; retrying after removing #{redis_dump}"
      status = run_sv_command_for_service('stop', 'redis')
      abort "Failed trying to put Redis in a down state" unless status.zero?
      sleep 1 # hack necessary, o/wise runit won't try to start the service
      File.delete redis_dump
      run_sv_command_for_service('start', 'redis')
      redo
    end

    abort "Failed to start #{sv_name} for migrations"
  end

  # Do not show "WARN: Cookbook 'local-mode-cache' is empty or entirely chefignored at /opt/gitlab/embedded/cookbooks/local-mode-cache"
  local_mode_cache_path = "#{base_path}/embedded/cookbooks/local-mode-cache"
  run_command("rm -rf #{local_mode_cache_path}")

  log 'Reconfigure GitLab to apply migrations'
  results = reconfigure
  if results != 0
    message = <<~EOF
    ===
    There was an error running gitlab-ctl reconfigure. Please check the output above for more
    details.
    ===
    EOF
    warn(message.color(:red))
    ::Kernel.exit results
  end

  log 'Restarting previously running GitLab services'
  get_all_services.each do |sv_name|
    run_sv_command_for_service('start', sv_name) if /^run: #{sv_name}:/.match?(service_statuses)
  end

  print_upgrade_and_exit
end

def run_pg_upgrade
  return unless attempt_auto_pg_upgrade?

  if postgresql_upgrade_disabled? || geo_detected? || patroni_detected?
    log ''
    log '==='
    log 'Skipping the check for newer PostgreSQL version and automatic upgrade.'
    log "Please see #{pg_upgrade_doc_url}"
    log 'for details on how to manually upgrade the PostgreSQL server'
    log '==='
    log ''
  else
    unless GitlabCtl::Util.progress_message('Checking if a newer PostgreSQL version is available and attempting automatic upgrade to it') do
      command = %W(#{base_path}/bin/gitlab-ctl pg-upgrade -w --skip-disk-check)
      status = run_command(command.join(' '))
      status.success?
    end
      log 'Error ensuring PostgreSQL is updated. Please check the logs'
      Kernel.exit 1
    end
  end
end

# Check for stale files from previous/failed installs, and advise the user to remove them.
def stale_files_check
  # The ctime will always be reflective of when the file was installed, where mtime is not being
  #  set when the file is extracted from a package. By using ctime to sort the files, the newest
  #  file is always the file to keep, and it's name is excluded from the output to the user.
  sprocket_files = Dir.glob("#{base_path}/embedded/service/gitlab-rails/public/assets/.sprockets-manifest*").sort_by { |f| File.ctime(f) }
  return unless sprocket_files.size > 1

  puts "WARNING:"
  puts "GitLab discovered stale file(s) from a previous install that need to be cleaned up."
  puts "The following files need to be removed:"
  puts "\n"
  puts sprocket_files.take(sprocket_files.size - 1)
  puts "\n"
end

def print_tanuki_art
  # Check if terminal supports colored outputs
  use_colors = system("which tput > /dev/null 2>&1") && `tput colors`.strip.to_i >= 8

  if use_colors
    orange_01p = "\033[38;2;252;161;33m"
    orange_02p = "\033[38;2;252;109;38m"
    orange_03p = "\033[38;2;226;67;41m"
    reset = "\033[0m"
  else
    orange_01p = orange_02p = orange_03p = reset = ''
  end

  tanuki_art = <<-LOGO
         #{orange_03p}.#{reset}                        #{orange_03p}..#{reset}
        #{orange_03p}:c:#{reset}                      #{orange_03p},cc'#{reset}
       #{orange_03p}:ccc:#{reset}                    #{orange_03p}'cccc.#{reset}
      #{orange_03p}:ccccc,#{reset}                   #{orange_03p}cccccc#{reset}
     #{orange_03p}.ccccccc#{reset}                  #{orange_03p}:cccccc:#{reset}
     #{orange_03p}cccccccc:#{reset}                #{orange_03p}.cccccccc'#{reset}
    #{orange_03p}:ccccccccc;..............'cccccccccc'#{reset}
   #{orange_03p}cccccccccccccccccccccccccccccccccccccc'#{reset}
  #{orange_02p}:ooolc#{reset}#{orange_03p}cccccccccccccccccccccccccccc#{reset}#{orange_02p}cllooo#{reset}
  #{orange_02p}oooooooll#{reset}#{orange_03p}cccccccccccccccccccccc#{reset}#{orange_02p}clooooooo#{reset}
  #{orange_02p};oooooooooll#{reset}#{orange_03p}cccccccccccccccc#{reset}#{orange_02p}cloooooooool#{reset}
   #{orange_02p}ooooooooooool#{reset}#{orange_03p}cccccccccccc#{reset}#{orange_02p}loooooooooooo.#{reset}
    #{orange_02p}.oooooooooooool#{reset}#{orange_03p}cccccc#{reset}#{orange_02p}lloooooooooooo;#{reset}
       #{orange_02p}coooooooooooool#{reset}#{orange_03p}l#{reset}#{orange_02p}looooooooooooo.#{reset}
          #{orange_02p}looooooood#{reset}#{orange_01p}xkkx#{reset}#{orange_02p}ddoooooooo.#{reset}
            #{orange_02p}.oood#{reset}#{orange_01p}xkkkkkkkkx#{reset}#{orange_02p}dooo;#{reset}
               #{orange_01p}.kkkkkkkkkkkk:#{reset}
                  #{orange_01p};kkkkkkx#{reset}
                     #{orange_01p}:d#{reset}
  LOGO

  puts tanuki_art
  puts "\n"
end

def pg_upgrade_check
  return unless postgresql_detected?

  manifest_file = '/opt/gitlab/version-manifest.txt'
  return unless File.exist?(manifest_file)

  # If postgresql_new doesn't exist, we are shipping only one PG version. This
  # check becomes irrelevant then, and we return early.
  manifest_entry = File.readlines(manifest_file).grep(/postgresql_new/).first
  return unless manifest_entry

  # Skip upgrade check if we do not recommend upgrading to the new PostgreSQL version.
  return unless recommend_pg_upgrade?

  new_version = PGVersion.parse(manifest_entry&.split&.[](1))

  pg_version_file = '/var/opt/gitlab/postgresql/data/PG_VERSION'
  installed_version = PGVersion.parse(File.read(pg_version_file).strip) if File.exist?(pg_version_file)

  # Print when upgrade
  #  - when we have a database and its not already on the new version
  outdated_db = installed_version && new_version && new_version.major.to_f > installed_version.major.to_f
  return unless outdated_db

  puts '=== INFO ==='
  puts "You are currently running PostgreSQL #{installed_version}."
  puts "Note that PostgreSQL #{new_version.major} will become the minimum required PostgreSQL version in GitLab 17.0 (May 2024)."
  puts "PostgreSQL #{installed_version} will be removed in GitLab 17.0. Please consider upgrading your PostgreSQL version soon."
  puts "To upgrade, please see: #{pg_upgrade_doc_url}"
  puts '=== INFO ==='
end

def survey_release_version
  manifest_file = '/opt/gitlab/version-manifest.json'
  return unless File.exist?(manifest_file)

  version_manifest = JSON.load_file(manifest_file)
  version_components = version_manifest['build_version'].split('.')
  version_components[0, 2].join('-')
end

def print_welcome_and_exit
  print_tanuki_art

  external_url = ENV['EXTERNAL_URL']
  puts "Thank you for installing GitLab!"
  if external_url == "http://gitlab.example.com"
    puts "GitLab was unable to detect a valid hostname for your instance."
    puts "Please configure a URL for your GitLab instance by setting `external_url`"
    puts "configuration in /etc/gitlab/gitlab.rb file."
    puts "Then, you can start your GitLab instance by running the following command:"
    puts "  sudo gitlab-ctl reconfigure"
  else
    puts "GitLab should be available at #{ENV['EXTERNAL_URL']}"
  end

  puts "\nFor a comprehensive list of configuration options please see the Omnibus GitLab readme"
  puts "https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md\n\n"

  pg_upgrade_check

  release_version = survey_release_version
  if release_version
    puts "Help us improve the installation experience, let us know how we did with a 1 minute survey:"
    puts "https://gitlab.fra1.qualtrics.com/jfe/form/SV_6kVqZANThUQ1bZb?installation=omnibus&release=#{release_version}\n\n"
  end

  stale_files_check
  Kernel.exit 0
end

def print_upgrade_and_exit
  puts "Upgrade complete! If your GitLab server is misbehaving try running"
  puts "  sudo gitlab-ctl restart"
  puts "before anything else."

  auto_backup_skip_file = "#{etc_path}/skip-auto-backup"
  if File.exist?(auto_backup_skip_file)
    puts "The automatic database backup was skipped as requested."
    puts "You may enable it again anytime by running the following command:"
    puts "  sudo rm #{auto_backup_skip_file}"
  else
    puts "If you need to roll back to the previous version you can use the database"
    puts "backup made during the upgrade (scroll up for the filename)."
  end

  puts "\n"
  pg_upgrade_check

  if display_upgrade_survey?
    release_version = survey_release_version
    if release_version
      puts "Help us improve the upgrade experience, let us know how we did with a 1 minute survey:"
      puts "https://gitlab.fra1.qualtrics.com/jfe/form/SV_0Hwcx9ncPfygMfj?installation=omnibus&release=#{release_version}\n\n"
    end
  end

  stale_files_check
  Kernel.exit 0
end

# Check if user already provided URL where GitLab should run
def external_url_unset?
  ENV['EXTERNAL_URL'].nil? || ENV['EXTERNAL_URL'].empty? || ENV['EXTERNAL_URL'] == "http://gitlab.example.com"
end

def postgresql_upgrade_disabled?
  File.exist?('/etc/gitlab/disable-postgresql-upgrade')
end

def geo_detected?
  (GitlabCtl::Util.roles(base_path) & %w[geo_primary geo_secondary]).any? || service_enabled?('geo-postgresql')
end

def patroni_detected?
  service_enabled?('patroni')
end

def attempt_auto_pg_upgrade?
  # This must return false when the opt-in PostgreSQL version is the default for pg-upgrade,
  # otherwise it must be true.
  false
end

def recommend_pg_upgrade?
  false
end

def display_upgrade_survey?
  false
end

def postgresql_detected?
  service_enabled?('postgresql') || service_enabled?('geo-postgresql') || service_enabled?('patroni')
end

def pg_upgrade_doc_url
  if geo_detected?
    'https://docs.gitlab.com/omnibus/settings/database.html#upgrading-a-geo-instance'
  elsif patroni_detected?
    'https://docs.gitlab.com/ee/administration/postgresql/replication_and_failover.html#upgrading-postgresql-major-version-in-a-patroni-cluster'
  else
    'https://docs.gitlab.com/omnibus/settings/database.html#upgrade-packaged-postgresql-server'
  end
end
