lib/envirobly/api.rb



require "json"
require "net/http"
require "socket"
require "uri"

class Envirobly::Api
  HOST = ENV["ENVIROBLY_API_HOST"].presence || "on.envirobly.com"
  USER_AGENT = "Envirobly CLI v#{Envirobly::VERSION}"
  CONTENT_TYPE = "application/json"

  def initialize(access_token: Envirobly::AccessToken.new, exit_on_error: true)
    @exit_on_error = exit_on_error
    @access_token = access_token
  end

  def validate_shape(params)
    post_as_json(api_v1_shape_validations_url, params:, headers: authorization_headers).tap do |response|
      unless response.success?
        $stderr.puts "Validation request responded with #{response.code}. Aborting."
        exit 1
      end
    end
  end

  def create_deployment(params)
    post_as_json(api_v1_deployments_url, params:, headers: authorization_headers)
  end

  def create_service_shell_connection(params)
    post_as_json(api_v1_service_shell_connections_url, params:, headers: authorization_headers)
  end

  def list_accounts
    get_as_json api_v1_accounts_url, headers: authorization_headers
  end

  def list_regions
    get_as_json api_v1_regions_url, headers: authorization_headers
  end

  def list_instance_types(region)
    get_as_json api_v1_instance_types_url(region), headers: authorization_headers
  end

  MAX_RETRIES = 30
  SHORT_RETRY_INTERVAL = 2.seconds
  LONG_RETRY_INTERVAL = 6.seconds
  def get_deployment_with_delay_and_retry(url, tries = 1)
    sleep SHORT_RETRY_INTERVAL * tries
    response = get_as_json URI(url)

    if response.success?
      response
    elsif MAX_RETRIES <= tries
      $stderr.puts "Max retries exhausted while waiting for deployment credentials. Aborting."
      exit 1
    else
      if tries > 3
        sleep LONG_RETRY_INTERVAL
      else
        sleep SHORT_RETRY_INTERVAL
      end

      get_deployment_with_delay_and_retry(url, tries + 1)
    end
  end

  def get_as_json(url, headers: {})
    request(url, type: Net::HTTP::Get, headers:)
  end

  def post_as_json(url, params: {}, headers: {})
    request(url, type: Net::HTTP::Post, headers:) do |request|
      request.body = params.to_json
    end
  end

  def put_as_json(url, params: {}, headers: {})
    request(url, type: Net::HTTP::Put, headers:) do |request|
      request.body = params.to_json
    end
  end

  private
    def api_v1_shape_validations_url
      api_url_for "v1/shape_validations"
    end

    def api_v1_deployments_url
      api_url_for "v1/deployments"
    end

    def api_v1_accounts_url
      api_url_for "v1/accounts"
    end

    def api_v1_regions_url
      api_url_for "v1/regions"
    end

    def api_v1_instance_types_url(region)
      api_url_for "v1/instance_types", query: "region=#{region}"
    end

    def api_v1_service_shell_connections_url
      api_url_for "v1/service_shell_connections"
    end

    def api_url_for(path, query: nil)
      URI::HTTPS.build(host: HOST, path: "/api/#{path}", query:)
    end

    def request(url, type:, headers: {})
      uri = URI(url)
      http = Net::HTTP.new uri.host, uri.port
      http.use_ssl = true
      http.open_timeout = 10
      http.read_timeout = 10

      headers = default_headers.merge headers
      request = type.new(uri, headers)
      request.content_type = CONTENT_TYPE

      yield request if block_given?

      http.request(request).tap do |response|
        def response.object
          @json_parsed_body ||= JSON.parse(body)
        rescue
          @json_parsed_body = { error_message: body }
        end

        def response.success?
          (200..299).include?(code.to_i)
        end

        if @exit_on_error && !response.success? && response.object["error_message"].present?
          puts response.object["error_message"] # TODO: Replace with shell.say_error
          exit 1
        end
      end
    end

    def default_headers
      { "User-Agent" => USER_AGENT, "X-Cli-Host" => Socket.gethostname }
    end

    def authorization_headers
      { "Authorization" => @access_token.as_http_bearer }
    end
end