convox_installer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rubocop.yml +41 -0
- data/.vscode/settings.json +4 -0
- data/Gemfile +15 -0
- data/LICENSE +7 -0
- data/README.md +23 -0
- data/convox_installer.gemspec +28 -0
- data/examples/full_installation.rb +182 -0
- data/lib/convox/client.rb +402 -0
- data/lib/convox.rb +3 -0
- data/lib/convox_installer/config.rb +186 -0
- data/lib/convox_installer/requirements.rb +71 -0
- data/lib/convox_installer/version.rb +5 -0
- data/lib/convox_installer.rb +50 -0
- data/log/.gitkeep +0 -0
- data/spec/lib/convox/client_spec.rb +154 -0
- data/spec/lib/convox_installer/config_spec.rb +208 -0
- data/spec/lib/convox_installer/requirements_spec.rb +87 -0
- data/spec/spec_helper.rb +118 -0
- metadata +139 -0
| @@ -0,0 +1,402 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "logger"
         | 
| 4 | 
            +
            require "json"
         | 
| 5 | 
            +
            require "fileutils"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module Convox
         | 
| 8 | 
            +
              class Client
         | 
| 9 | 
            +
                CONVOX_DIR = File.expand_path("~/.convox").freeze
         | 
| 10 | 
            +
                AUTH_FILE = File.join(CONVOX_DIR, "auth")
         | 
| 11 | 
            +
                HOST_FILE = File.join(CONVOX_DIR, "host")
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                attr_accessor :logger, :config
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def auth
         | 
| 16 | 
            +
                  load_auth_from_file
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def initialize(options = {})
         | 
| 20 | 
            +
                  @logger = Logger.new(STDOUT)
         | 
| 21 | 
            +
                  logger.level = options[:log_level] || Logger::INFO
         | 
| 22 | 
            +
                  @config = options[:config] || {}
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def backup_convox_host_and_rack
         | 
| 26 | 
            +
                  %w[host rack].each do |f|
         | 
| 27 | 
            +
                    path = File.join(CONVOX_DIR, f)
         | 
| 28 | 
            +
                    if File.exist?(path)
         | 
| 29 | 
            +
                      bak_file = "#{path}.bak"
         | 
| 30 | 
            +
                      logger.info "Moving existing #{path} to #{bak_file}..."
         | 
| 31 | 
            +
                      FileUtils.mv(path, bak_file)
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def install_convox
         | 
| 37 | 
            +
                  require_config(%i[ aws_region stack_name ])
         | 
| 38 | 
            +
                  region = config.fetch(:aws_region)
         | 
| 39 | 
            +
                  stack_name = config.fetch(:stack_name)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  if rack_already_installed?
         | 
| 42 | 
            +
                    logger.info "There is already a Convox stack named #{stack_name} " \
         | 
| 43 | 
            +
                                "in the #{region} AWS region. Using this rack. "
         | 
| 44 | 
            +
                    return true
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  require_config(%i[
         | 
| 48 | 
            +
                    aws_region
         | 
| 49 | 
            +
                    aws_access_key_id
         | 
| 50 | 
            +
                    aws_secret_access_key
         | 
| 51 | 
            +
                    stack_name
         | 
| 52 | 
            +
                    instance_type
         | 
| 53 | 
            +
                  ])
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  logger.info "Installing Convox (#{stack_name})..."
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  env = {
         | 
| 58 | 
            +
                    "AWS_REGION" => region,
         | 
| 59 | 
            +
                    "AWS_ACCESS_KEY_ID" => config.fetch(:aws_access_key_id),
         | 
| 60 | 
            +
                    "AWS_SECRET_ACCESS_KEY" => config.fetch(:aws_secret_access_key),
         | 
| 61 | 
            +
                  }
         | 
| 62 | 
            +
                  command = %Q{rack install aws \
         | 
| 63 | 
            +
            --name "#{config.fetch(:stack_name)}" \
         | 
| 64 | 
            +
            "InstanceType=#{config.fetch(:instance_type)}" \
         | 
| 65 | 
            +
            "BuildInstance="}
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  run_convox_command!(command, env)
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                def rack_already_installed?
         | 
| 71 | 
            +
                  require_config(%i[ aws_region stack_name ])
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  unless File.exist?(AUTH_FILE)
         | 
| 74 | 
            +
                    raise "Could not find auth file at #{AUTH_FILE}!"
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  region = config.fetch(:aws_region)
         | 
| 78 | 
            +
                  stack_name = config.fetch(:stack_name)
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  auth.each do |host, password|
         | 
| 81 | 
            +
                    if host.match?(/^#{stack_name}-\d+\.#{region}\.elb\.amazonaws\.com$/)
         | 
| 82 | 
            +
                      return true
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
                  end
         | 
| 85 | 
            +
                  false
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def validate_convox_auth_and_set_host!
         | 
| 89 | 
            +
                  require_config(%i[ aws_region stack_name ])
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  unless File.exist?(AUTH_FILE)
         | 
| 92 | 
            +
                    raise "Could not find auth file at #{AUTH_FILE}!"
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  region = config.fetch(:aws_region)
         | 
| 96 | 
            +
                  stack = config.fetch(:stack_name)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  match_count = 0
         | 
| 99 | 
            +
                  matching_host = nil
         | 
| 100 | 
            +
                  auth.each do |host, password|
         | 
| 101 | 
            +
                    if host.match?(/^#{stack}-\d+\.#{region}\.elb\.amazonaws\.com$/)
         | 
| 102 | 
            +
                      matching_host = host
         | 
| 103 | 
            +
                      match_count += 1
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  if match_count == 1
         | 
| 108 | 
            +
                    set_host(matching_host)
         | 
| 109 | 
            +
                    return matching_host
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  if match_count > 1
         | 
| 113 | 
            +
                    error_message = "Found multiple matching hosts for "
         | 
| 114 | 
            +
                  else
         | 
| 115 | 
            +
                    error_message = "Could not find matching authentication for "
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                  error_message += "region: #{region}, stack: #{stack}"
         | 
| 118 | 
            +
                  raise error_message
         | 
| 119 | 
            +
                end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                def set_host(host)
         | 
| 122 | 
            +
                  logger.debug "Setting convox host to #{host} (in #{HOST_FILE})..."
         | 
| 123 | 
            +
                  File.open(HOST_FILE, "w") { |f| f.puts host }
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                def validate_convox_rack!
         | 
| 127 | 
            +
                  require_config(%i[
         | 
| 128 | 
            +
                    aws_region
         | 
| 129 | 
            +
                    stack_name
         | 
| 130 | 
            +
                    instance_type
         | 
| 131 | 
            +
                  ])
         | 
| 132 | 
            +
                  logger.debug "Validating that convox rack has the correct attributes..."
         | 
| 133 | 
            +
                  {
         | 
| 134 | 
            +
                    provider: "aws",
         | 
| 135 | 
            +
                    region: config.fetch(:aws_region),
         | 
| 136 | 
            +
                    type: config.fetch(:instance_type),
         | 
| 137 | 
            +
                    name: config.fetch(:stack_name),
         | 
| 138 | 
            +
                  }.each do |k, v|
         | 
| 139 | 
            +
                    convox_value = convox_rack_data[k.to_s]
         | 
| 140 | 
            +
                    if convox_value != v
         | 
| 141 | 
            +
                      raise "Convox data did not match! Expected #{k} to be '#{v}', " \
         | 
| 142 | 
            +
                            "but was: '#{convox_value}'"
         | 
| 143 | 
            +
                    end
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
                  logger.debug "=> Convox rack has the correct attributes."
         | 
| 146 | 
            +
                  true
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                def convox_rack_data
         | 
| 150 | 
            +
                  @convox_rack_data ||= begin
         | 
| 151 | 
            +
                    logger.debug "Fetching convox rack attributes..."
         | 
| 152 | 
            +
                    convox_output = `convox api get /system`
         | 
| 153 | 
            +
                    raise "convox command failed!" unless $?.success?
         | 
| 154 | 
            +
                    JSON.parse(convox_output)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def create_convox_app!
         | 
| 159 | 
            +
                  require_config(%i[convox_app_name])
         | 
| 160 | 
            +
                  return true if convox_app_exists?
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  app_name = config.fetch(:convox_app_name)
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                  logger.info "Creating app: #{app_name}..."
         | 
| 165 | 
            +
                  logger.info "=> Documentation: " \
         | 
| 166 | 
            +
                              "https://docs.convox.com/deployment/creating-an-application"
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                  run_convox_command! "apps create #{app_name} --wait"
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  retries = 0
         | 
| 171 | 
            +
                  loop do
         | 
| 172 | 
            +
                    break if convox_app_exists?
         | 
| 173 | 
            +
                    if retries > 5
         | 
| 174 | 
            +
                      raise "Something went wrong while creating the #{app_name} app! " \
         | 
| 175 | 
            +
                            "(Please wait a few moments and then restart the installation script.)"
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
                    logger.info "Waiting for #{app_name} to be ready..."
         | 
| 178 | 
            +
                    sleep 3
         | 
| 179 | 
            +
                    retries += 1
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  logger.info "=> #{app_name} app created!"
         | 
| 183 | 
            +
                end
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                def set_default_app_for_directory!
         | 
| 186 | 
            +
                  logger.info "Setting default app in ./.convox/app..."
         | 
| 187 | 
            +
                  FileUtils.mkdir_p File.expand_path("./.convox")
         | 
| 188 | 
            +
                  File.open(File.expand_path("./.convox/app"), "w") do |f|
         | 
| 189 | 
            +
                    f.puts config.fetch(:convox_app_name)
         | 
| 190 | 
            +
                  end
         | 
| 191 | 
            +
                end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                def convox_app_exists?
         | 
| 194 | 
            +
                  require_config(%i[convox_app_name])
         | 
| 195 | 
            +
                  app_name = config.fetch(:convox_app_name)
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  logger.debug "Looking for existing #{app_name} app..."
         | 
| 198 | 
            +
                  convox_output = `convox api get /apps`
         | 
| 199 | 
            +
                  raise "convox command failed!" unless $?.success?
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                  apps = JSON.parse(convox_output)
         | 
| 202 | 
            +
                  apps.each do |app|
         | 
| 203 | 
            +
                    if app["name"] == app_name
         | 
| 204 | 
            +
                      logger.debug "=> Found #{app_name} app."
         | 
| 205 | 
            +
                      return true
         | 
| 206 | 
            +
                    end
         | 
| 207 | 
            +
                  end
         | 
| 208 | 
            +
                  logger.debug "=> Did not find #{app_name} app."
         | 
| 209 | 
            +
                  false
         | 
| 210 | 
            +
                end
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                # Create the s3 bucket, and also apply a CORS configuration
         | 
| 213 | 
            +
                def create_s3_bucket!
         | 
| 214 | 
            +
                  require_config(%i[s3_bucket_name])
         | 
| 215 | 
            +
                  bucket_name = config.fetch(:s3_bucket_name)
         | 
| 216 | 
            +
                  if s3_bucket_exists?
         | 
| 217 | 
            +
                    logger.info "#{bucket_name} S3 bucket already exists!"
         | 
| 218 | 
            +
                  else
         | 
| 219 | 
            +
                    logger.info "Creating S3 bucket resource (#{bucket_name})..."
         | 
| 220 | 
            +
                    run_convox_command! "rack resources create s3 " \
         | 
| 221 | 
            +
                                        "--name \"#{bucket_name}\" " \
         | 
| 222 | 
            +
                                        "--wait"
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    retries = 0
         | 
| 225 | 
            +
                    loop do
         | 
| 226 | 
            +
                      return if s3_bucket_exists?
         | 
| 227 | 
            +
             | 
| 228 | 
            +
                      if retries > 10
         | 
| 229 | 
            +
                        raise "Something went wrong while creating the #{bucket_name} S3 bucket! " \
         | 
| 230 | 
            +
                              "(Please wait a few moments and then restart the installation script.)"
         | 
| 231 | 
            +
                      end
         | 
| 232 | 
            +
                      logger.debug "Waiting for S3 bucket to be ready..."
         | 
| 233 | 
            +
                      sleep 3
         | 
| 234 | 
            +
                      retries += 1
         | 
| 235 | 
            +
                    end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                    logger.debug "=> S3 bucket created!"
         | 
| 238 | 
            +
                  end
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                  set_s3_bucket_cors_policy
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                def s3_bucket_exists?
         | 
| 244 | 
            +
                  require_config(%i[s3_bucket_name])
         | 
| 245 | 
            +
                  bucket_name = config.fetch(:s3_bucket_name)
         | 
| 246 | 
            +
                  logger.debug "Looking up S3 bucket resource: #{bucket_name}"
         | 
| 247 | 
            +
                  `convox api get /resources/#{bucket_name} 2>/dev/null`
         | 
| 248 | 
            +
                  $?.success?
         | 
| 249 | 
            +
                end
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                def s3_bucket_details
         | 
| 252 | 
            +
                  require_config(%i[s3_bucket_name])
         | 
| 253 | 
            +
                  @s3_bucket_details ||= begin
         | 
| 254 | 
            +
                    bucket_name = config.fetch(:s3_bucket_name)
         | 
| 255 | 
            +
                    logger.debug "Fetching S3 bucket resource details for #{bucket_name}..."
         | 
| 256 | 
            +
             | 
| 257 | 
            +
                    response = `convox api get /resources/#{bucket_name}`
         | 
| 258 | 
            +
                    raise "convox command failed!" unless $?.success?
         | 
| 259 | 
            +
             | 
| 260 | 
            +
                    bucket_data = JSON.parse(response)
         | 
| 261 | 
            +
                    s3_url = bucket_data["url"]
         | 
| 262 | 
            +
                    matches = s3_url.match(
         | 
| 263 | 
            +
                      /^s3:\/\/(?<access_key_id>[^:]*):(?<secret_access_key>[^@]*)@(?<bucket_name>.*)$/
         | 
| 264 | 
            +
                    )
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                    match_keys = %i[access_key_id secret_access_key bucket_name]
         | 
| 267 | 
            +
                    unless matches && match_keys.all? { |k| matches[k].present? }
         | 
| 268 | 
            +
                      raise "#{s3_url} is an invalid S3 URL!"
         | 
| 269 | 
            +
                    end
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                    {
         | 
| 272 | 
            +
                      access_key_id: matches[:access_key_id],
         | 
| 273 | 
            +
                      secret_access_key: matches[:secret_access_key],
         | 
| 274 | 
            +
                      name: matches[:bucket_name],
         | 
| 275 | 
            +
                    }
         | 
| 276 | 
            +
                  end
         | 
| 277 | 
            +
                end
         | 
| 278 | 
            +
             | 
| 279 | 
            +
                def set_s3_bucket_cors_policy
         | 
| 280 | 
            +
                  require_config(%i[aws_access_key_id aws_secret_access_key])
         | 
| 281 | 
            +
                  access_key_id = config.fetch(:aws_access_key_id)
         | 
| 282 | 
            +
                  secret_access_key = config.fetch(:aws_secret_access_key)
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                  unless config.key? :s3_bucket_cors_policy
         | 
| 285 | 
            +
                    logger.debug "No CORS policy provided in config: s3_bucket_cors_policy"
         | 
| 286 | 
            +
                    return
         | 
| 287 | 
            +
                  end
         | 
| 288 | 
            +
                  cors_policy_string = config.fetch(:s3_bucket_cors_policy)
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                  bucket_name = s3_bucket_details[:name]
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                  logger.debug "Looking up existing CORS policy for #{bucket_name}"
         | 
| 293 | 
            +
                  existing_cors_policy_string =
         | 
| 294 | 
            +
                    `AWS_ACCESS_KEY_ID=#{access_key_id} \
         | 
| 295 | 
            +
                    AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
         | 
| 296 | 
            +
                    aws s3api get-bucket-cors --bucket #{bucket_name} 2>/dev/null`
         | 
| 297 | 
            +
                  if $?.success? && existing_cors_policy_string.present?
         | 
| 298 | 
            +
                    # Sort all the nested arrays so that the equality operator works
         | 
| 299 | 
            +
                    existing_cors_policy = JSON.parse(existing_cors_policy_string)
         | 
| 300 | 
            +
                    cors_policy_json = JSON.parse(cors_policy_string)
         | 
| 301 | 
            +
                    [existing_cors_policy, cors_policy_json].each do |policy_json|
         | 
| 302 | 
            +
                      if policy_json.is_a?(Hash) && policy_json["CORSRules"]
         | 
| 303 | 
            +
                        policy_json["CORSRules"].each do |rule|
         | 
| 304 | 
            +
                          rule["AllowedHeaders"].sort! if rule["AllowedHeaders"]
         | 
| 305 | 
            +
                          rule["AllowedMethods"].sort! if rule["AllowedMethods"]
         | 
| 306 | 
            +
                          rule["AllowedOrigins"].sort! if rule["AllowedOrigins"]
         | 
| 307 | 
            +
                        end
         | 
| 308 | 
            +
                      end
         | 
| 309 | 
            +
                    end
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                    if existing_cors_policy == cors_policy_json
         | 
| 312 | 
            +
                      logger.debug "=> CORS policy is already up to date for #{bucket_name}."
         | 
| 313 | 
            +
                      return
         | 
| 314 | 
            +
                    end
         | 
| 315 | 
            +
                  end
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                  begin
         | 
| 318 | 
            +
                    logger.info "Setting CORS policy for #{bucket_name}..."
         | 
| 319 | 
            +
             | 
| 320 | 
            +
                    File.open("cors-policy.json", "w") { |f| f.puts cors_policy_string }
         | 
| 321 | 
            +
             | 
| 322 | 
            +
                    `AWS_ACCESS_KEY_ID=#{access_key_id} \
         | 
| 323 | 
            +
                    AWS_SECRET_ACCESS_KEY=#{secret_access_key} \
         | 
| 324 | 
            +
                      aws s3api put-bucket-cors \
         | 
| 325 | 
            +
                      --bucket #{bucket_name} \
         | 
| 326 | 
            +
                      --cors-configuration "file://cors-policy.json"`
         | 
| 327 | 
            +
                    unless $?.success?
         | 
| 328 | 
            +
                      raise "Something went wrong while setting the S3 bucket CORS policy!"
         | 
| 329 | 
            +
                    end
         | 
| 330 | 
            +
                    logger.info "=> Successfully set CORS policy for #{bucket_name}."
         | 
| 331 | 
            +
                  ensure
         | 
| 332 | 
            +
                    FileUtils.rm_f "cors-policy.json"
         | 
| 333 | 
            +
                  end
         | 
| 334 | 
            +
                end
         | 
| 335 | 
            +
             | 
| 336 | 
            +
                def add_docker_registry!
         | 
| 337 | 
            +
                  require_config(%i[docker_registry_url docker_registry_username docker_registry_password])
         | 
| 338 | 
            +
             | 
| 339 | 
            +
                  registry_url = config.fetch(:docker_registry_url)
         | 
| 340 | 
            +
             | 
| 341 | 
            +
                  logger.debug "Looking up existing Docker registries..."
         | 
| 342 | 
            +
                  registries_response = `convox api get /registries`
         | 
| 343 | 
            +
                  unless $?.success?
         | 
| 344 | 
            +
                    raise "Something went wrong while fetching the list of registries!"
         | 
| 345 | 
            +
                  end
         | 
| 346 | 
            +
                  registries = JSON.parse(registries_response)
         | 
| 347 | 
            +
             | 
| 348 | 
            +
                  if registries.any? { |r| r["server"] == registry_url }
         | 
| 349 | 
            +
                    logger.debug "=> Docker Registry already exists: #{registry_url}"
         | 
| 350 | 
            +
                    return true
         | 
| 351 | 
            +
                  end
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                  logger.info "Adding Docker Registry: #{registry_url}..."
         | 
| 354 | 
            +
                  logger.info "=> Documentation: " \
         | 
| 355 | 
            +
                              "https://docs.convox.com/deployment/private-registries"
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                  `convox registries add "#{registry_url}" \
         | 
| 358 | 
            +
                    "#{config.fetch(:docker_registry_username)}" \
         | 
| 359 | 
            +
                    "#{config.fetch(:docker_registry_password)}"`
         | 
| 360 | 
            +
                  unless $?.success?
         | 
| 361 | 
            +
                    raise "Something went wrong while adding the #{registry_url} registry!"
         | 
| 362 | 
            +
                  end
         | 
| 363 | 
            +
                end
         | 
| 364 | 
            +
             | 
| 365 | 
            +
                def default_service_domain_name
         | 
| 366 | 
            +
                  require_config(%i[convox_app_name default_service])
         | 
| 367 | 
            +
             | 
| 368 | 
            +
                  @default_service_domain_name ||= begin
         | 
| 369 | 
            +
                    convox_domain = convox_rack_data["domain"]
         | 
| 370 | 
            +
                    elb_name_and_region = convox_domain[/([^\.]*\.[^\.]*)\..*/, 1]
         | 
| 371 | 
            +
                    unless elb_name_and_region.present?
         | 
| 372 | 
            +
                      raise "Something went wrong while parsing the ELB name and region! " \
         | 
| 373 | 
            +
                            "(#{elb_name_and_region})"
         | 
| 374 | 
            +
                    end
         | 
| 375 | 
            +
                    app = config.fetch(:convox_app_name)
         | 
| 376 | 
            +
                    service = config.fetch(:default_service)
         | 
| 377 | 
            +
             | 
| 378 | 
            +
                    "#{app}-#{service}.#{elb_name_and_region}.convox.site"
         | 
| 379 | 
            +
                  end
         | 
| 380 | 
            +
                end
         | 
| 381 | 
            +
             | 
| 382 | 
            +
                def run_convox_command!(cmd, env = {})
         | 
| 383 | 
            +
                  command = "convox #{cmd}"
         | 
| 384 | 
            +
                  system env, command
         | 
| 385 | 
            +
                  raise "Error running: #{command}" unless $?.success?
         | 
| 386 | 
            +
                end
         | 
| 387 | 
            +
             | 
| 388 | 
            +
                private
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                def load_auth_from_file
         | 
| 391 | 
            +
                  return {} unless File.exist?(AUTH_FILE)
         | 
| 392 | 
            +
             | 
| 393 | 
            +
                  JSON.parse(File.read(AUTH_FILE))
         | 
| 394 | 
            +
                end
         | 
| 395 | 
            +
             | 
| 396 | 
            +
                def require_config(required_keys)
         | 
| 397 | 
            +
                  required_keys.each do |k|
         | 
| 398 | 
            +
                    raise "#{k} is missing from the config!" unless config[k]
         | 
| 399 | 
            +
                  end
         | 
| 400 | 
            +
                end
         | 
| 401 | 
            +
              end
         | 
| 402 | 
            +
            end
         | 
    
        data/lib/convox.rb
    ADDED
    
    
| @@ -0,0 +1,186 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "highline"
         | 
| 4 | 
            +
            require "fileutils"
         | 
| 5 | 
            +
            require "json"
         | 
| 6 | 
            +
            require "securerandom"
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module ConvoxInstaller
         | 
| 9 | 
            +
              class Config
         | 
| 10 | 
            +
                attr_accessor :logger, :config, :prompts, :highline
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                CONFIG_FILE = File.expand_path("~/.convox/installer_config").freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                DEFAULT_PROMPTS = [
         | 
| 15 | 
            +
                  {
         | 
| 16 | 
            +
                    key: :stack_name,
         | 
| 17 | 
            +
                    title: "Convox Stack Name",
         | 
| 18 | 
            +
                    prompt: "Please enter a name for your Convox installation",
         | 
| 19 | 
            +
                    default: "convox",
         | 
| 20 | 
            +
                  },
         | 
| 21 | 
            +
                  {
         | 
| 22 | 
            +
                    key: :aws_region,
         | 
| 23 | 
            +
                    title: "AWS Region",
         | 
| 24 | 
            +
                    default: "us-east-1",
         | 
| 25 | 
            +
                  },
         | 
| 26 | 
            +
                  {
         | 
| 27 | 
            +
                    key: :instance_type,
         | 
| 28 | 
            +
                    title: "EC2 Instance Type",
         | 
| 29 | 
            +
                    default: "t3.medium",
         | 
| 30 | 
            +
                  },
         | 
| 31 | 
            +
                  {
         | 
| 32 | 
            +
                    section: "Admin AWS Credentials",
         | 
| 33 | 
            +
                  },
         | 
| 34 | 
            +
                  {
         | 
| 35 | 
            +
                    key: :aws_access_key_id,
         | 
| 36 | 
            +
                    title: "AWS Access Key ID",
         | 
| 37 | 
            +
                  },
         | 
| 38 | 
            +
                  {
         | 
| 39 | 
            +
                    key: :aws_secret_access_key,
         | 
| 40 | 
            +
                    title: "AWS Secret Access Key",
         | 
| 41 | 
            +
                  },
         | 
| 42 | 
            +
                ].freeze
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def initialize(options = {})
         | 
| 45 | 
            +
                  @logger = Logger.new(STDOUT)
         | 
| 46 | 
            +
                  logger.level = options[:log_level] || Logger::INFO
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  self.prompts = options[:prompts] || DEFAULT_PROMPTS
         | 
| 49 | 
            +
                  self.config = {}
         | 
| 50 | 
            +
                  load_config_from_file
         | 
| 51 | 
            +
                  load_config_from_env
         | 
| 52 | 
            +
                  self.config = config.merge((options[:config] || {}).symbolize_keys)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  self.highline = options[:highline] || HighLine.new
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def config_keys
         | 
| 58 | 
            +
                  prompts.map { |prompt| prompt[:key] }.compact.map(&:to_sym)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def prompt_for_config
         | 
| 62 | 
            +
                  loop do
         | 
| 63 | 
            +
                    prompts.each do |prompt|
         | 
| 64 | 
            +
                      if prompt[:section]
         | 
| 65 | 
            +
                        highline.say "\n#{prompt[:section]}"
         | 
| 66 | 
            +
                        highline.say "============================================\n\n"
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                      next unless prompt[:key]
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                      ask_prompt(prompt)
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    show_config_summary
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    @completed_prompt = true
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                    highline.say "Please double check all of these configuration details."
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                    agree = highline.agree(
         | 
| 80 | 
            +
                      "Would you like to start the Convox installation?" \
         | 
| 81 | 
            +
                      " (press 'n' to correct any settings)"
         | 
| 82 | 
            +
                    )
         | 
| 83 | 
            +
                    break if agree
         | 
| 84 | 
            +
                    highline.say "\n"
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  config
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def show_config_summary
         | 
| 91 | 
            +
                  highline.say "\n============================================"
         | 
| 92 | 
            +
                  highline.say "                 SUMMARY"
         | 
| 93 | 
            +
                  highline.say "============================================\n\n"
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  config_titles = prompts.map do |prompt|
         | 
| 96 | 
            +
                    prompt[:title] || prompt[:key]
         | 
| 97 | 
            +
                  end.compact
         | 
| 98 | 
            +
                  max = config_titles.map(&:length).max
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  prompts.each do |prompt|
         | 
| 101 | 
            +
                    next if !prompt[:key] || prompt[:hidden]
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    value = config[prompt[:key]]
         | 
| 104 | 
            +
                    title = prompt[:title] || prompt[:key]
         | 
| 105 | 
            +
                    padded_key = "#{title}:".ljust(max + 3)
         | 
| 106 | 
            +
                    highline.say "    #{padded_key} #{value}"
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                  highline.say "\nWe've saved your configuration to: #{CONFIG_FILE}"
         | 
| 109 | 
            +
                  highline.say "If anything goes wrong during the installation, " \
         | 
| 110 | 
            +
                               "you can restart the script to reload the config and continue.\n\n"
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                private
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def ask_prompt(prompt)
         | 
| 116 | 
            +
                  key = prompt[:key]
         | 
| 117 | 
            +
                  title = prompt[:title] || key
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  # If looping through the config again, ask for all
         | 
| 120 | 
            +
                  # the config with defaults.
         | 
| 121 | 
            +
                  if config[key] && !@completed_prompt
         | 
| 122 | 
            +
                    logger.debug "Found existing config for #{key} => #{config[key]}"
         | 
| 123 | 
            +
                    return
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  # Used when we want to force a default value and not prompt the user.
         | 
| 127 | 
            +
                  # (e.g. securely generated passwords)
         | 
| 128 | 
            +
                  if prompt[:value]
         | 
| 129 | 
            +
                    return if config[key]
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                    default = prompt[:value]
         | 
| 132 | 
            +
                    config[key] = default.is_a?(Proc) ? default.call : default
         | 
| 133 | 
            +
                    save_config_to_file
         | 
| 134 | 
            +
                    return
         | 
| 135 | 
            +
                  end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                  prompt_string = prompt[:prompt] || "Please enter your #{title}: "
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  config[key] = highline.ask(prompt_string) do |q|
         | 
| 140 | 
            +
                    if @completed_prompt
         | 
| 141 | 
            +
                      q.default = config[key]
         | 
| 142 | 
            +
                    elsif prompt[:default]
         | 
| 143 | 
            +
                      q.default = prompt[:default]
         | 
| 144 | 
            +
                    end
         | 
| 145 | 
            +
                    q.validate = /.+/
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  save_config_to_file
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                def load_config_from_file
         | 
| 152 | 
            +
                  return unless Config.config_file_exists?
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  logger.debug "Loading saved config from #{CONFIG_FILE}..."
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                  loaded_config = JSON.parse(Config.read_config_file)["config"].symbolize_keys
         | 
| 157 | 
            +
                  self.config = config.merge(loaded_config).slice(*config_keys)
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                def load_config_from_env
         | 
| 161 | 
            +
                  config_keys.each do |key|
         | 
| 162 | 
            +
                    env_key = key.to_s.upcase
         | 
| 163 | 
            +
                    value = ENV[env_key]
         | 
| 164 | 
            +
                    next unless value.present?
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                    logger.debug "Found value for #{key} in env var: #{env_key} => #{value}"
         | 
| 167 | 
            +
                    config[key] = value
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                def save_config_to_file
         | 
| 172 | 
            +
                  FileUtils.mkdir_p File.expand_path("~/.convox")
         | 
| 173 | 
            +
                  File.open(CONFIG_FILE, "w") do |f|
         | 
| 174 | 
            +
                    f.puts({config: config}.to_json)
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                def self.config_file_exists?
         | 
| 179 | 
            +
                  File.exist?(CONFIG_FILE)
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                def self.read_config_file
         | 
| 183 | 
            +
                  File.read(CONFIG_FILE)
         | 
| 184 | 
            +
                end
         | 
| 185 | 
            +
              end
         | 
| 186 | 
            +
            end
         | 
| @@ -0,0 +1,71 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "highline"
         | 
| 4 | 
            +
            require "os"
         | 
| 5 | 
            +
            require "logger"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module ConvoxInstaller
         | 
| 8 | 
            +
              class Requirements
         | 
| 9 | 
            +
                attr_accessor :ecr_label, :logger
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(options = {})
         | 
| 12 | 
            +
                  @ecr_label = options[:ecr_label]
         | 
| 13 | 
            +
                  @logger = Logger.new(STDOUT)
         | 
| 14 | 
            +
                  logger.level = options[:log_level] || Logger::INFO
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def ensure_requirements!
         | 
| 18 | 
            +
                  logger.debug "Checking for required commands..."
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  @missing_packages = []
         | 
| 21 | 
            +
                  unless has_command? "convox"
         | 
| 22 | 
            +
                    @missing_packages << {
         | 
| 23 | 
            +
                      name: "convox",
         | 
| 24 | 
            +
                      brew: "convox",
         | 
| 25 | 
            +
                      docs: "https://docs.convox.com/introduction/installation",
         | 
| 26 | 
            +
                    }
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  unless has_command? "aws"
         | 
| 30 | 
            +
                    @missing_packages << {
         | 
| 31 | 
            +
                      name: "aws",
         | 
| 32 | 
            +
                      brew: "awscli",
         | 
| 33 | 
            +
                      docs: "https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html",
         | 
| 34 | 
            +
                    }
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  if @missing_packages.any?
         | 
| 38 | 
            +
                    logger.error "This script requires the convox and AWS CLI tools."
         | 
| 39 | 
            +
                    if OS.mac?
         | 
| 40 | 
            +
                      logger.error "Please run: brew install " \
         | 
| 41 | 
            +
                                   "#{@missing_packages.map { |p| p[:brew] }.join(" ")}"
         | 
| 42 | 
            +
                    else
         | 
| 43 | 
            +
                      logger.error "Installation Instructions:"
         | 
| 44 | 
            +
                      @missing_packages.each do |package|
         | 
| 45 | 
            +
                        logger.error "* #{package[:name]}: #{package[:docs]}"
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                    quit!
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def has_command?(command)
         | 
| 53 | 
            +
                  path = find_command command
         | 
| 54 | 
            +
                  if path.present?
         | 
| 55 | 
            +
                    logger.debug "=> Found #{command}: #{path}"
         | 
| 56 | 
            +
                    return true
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                  logger.debug "=> Could not find #{command}!"
         | 
| 59 | 
            +
                  false
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                # Stubbed in tests
         | 
| 63 | 
            +
                def find_command(command)
         | 
| 64 | 
            +
                  `which #{command} 2>/dev/null`.chomp
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def quit!
         | 
| 68 | 
            +
                  exit 1
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
            end
         |