diff --git a/CHANGELOG.md b/CHANGELOG.md index de2b58ec32..4e848d135e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # New Relic Ruby Agent Release Notes # + ## v8.11.0 + + * **Added support for New Relic REST API v2 when using `newrelic deployments` command** + + Previously, the `newrelic deployments` command only supported the older version of the deployments api, which does not currently support newer license keys. Now you can use the New Relic REST API v2 to record deployments by providing your user api key to the agent configuration using `api_key`. When this configuration option is present, the `newrelic deployments` command will automatically use the New Relic REST API v2 deployment endpoint. [PR#1461](https://github.com/newrelic/newrelic-ruby-agent/pull/1461) + + Thank you to @Arkham for bringing this to our attention! + + + + ## v8.10.1 diff --git a/lib/new_relic/agent/configuration/default_source.rb b/lib/new_relic/agent/configuration/default_source.rb index b141ffaa13..4c359c6b0f 100644 --- a/lib/new_relic/agent/configuration/default_source.rb +++ b/lib/new_relic/agent/configuration/default_source.rb @@ -210,12 +210,16 @@ def self.host end def self.api_host + # only used for deployment task proc do - if String(NewRelic::Agent.config[:license_key]).start_with?('eu') - 'rpm.eu.newrelic.com' + api_version = if NewRelic::Agent.config[:api_key].nil? || NewRelic::Agent.config[:api_key].empty? + "rpm" else - 'rpm.newrelic.com' + "api" end + api_region = "eu." if String(NewRelic::Agent.config[:license_key]).start_with?('eu') + + "#{api_version}.#{api_region}newrelic.com" end end @@ -329,6 +333,13 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil) :allowed_from_server => false, :description => 'Your New Relic [license key](/docs/apis/intro-apis/new-relic-api-keys/#ingest-license-key).' }, + :api_key => { + :default => '', + :public => true, + :type => String, + :allowed_from_server => false, + :description => 'Your New Relic API key. Required when using the New Relic REST API v2 to record deployments using the `newrelic deployments` command.' + }, :agent_enabled => { :default => DefaultSource.agent_enabled, :documentation_default => true, diff --git a/lib/new_relic/cli/commands/deployments.rb b/lib/new_relic/cli/commands/deployments.rb index d7223c2f36..2192de0b7d 100644 --- a/lib/new_relic/cli/commands/deployments.rb +++ b/lib/new_relic/cli/commands/deployments.rb @@ -40,6 +40,7 @@ def initialize(command_line_args) load_yaml_from_env(control.env) @appname ||= NewRelic::Agent.config[:app_name][0] || control.env || 'development' @license_key ||= NewRelic::Agent.config[:license_key] + @api_key ||= NewRelic::Agent.config[:api_key] setup_logging(control.env) end @@ -64,29 +65,28 @@ def setup_logging(env) def run begin @description = nil if @description && @description.strip.empty? - create_params = {} - { - :application_id => @appname, - :host => NewRelic::Agent::Hostname.get, - :description => @description, - :user => @user, - :revision => @revision, - :changelog => @changelog - }.each do |k, v| - create_params["deployment[#{k}]"] = v unless v.nil? || v == '' - end - http = ::NewRelic::Agent::NewRelicService.new(nil, control.api_server).http_connection - - uri = "/deployments.xml" if @license_key.nil? || @license_key.empty? - raise "license_key was not set in newrelic.yml for #{control.env}" + raise "license_key not set in newrelic.yml for #{control.env}. api_key also required to use New Relic REST API v2" + end + + if !api_v1? && (@revision.nil? || @revision.empty?) + raise "revision required when using New Relic REST API v2 with api_key. Pass in revision using: -r, --revision=REV" end - request = Net::HTTP::Post.new(uri, {'x-license-key' => @license_key}) - request.content_type = "application/octet-stream" - request.set_form_data(create_params) + request = if api_v1? + uri = "/deployments.xml" + create_request(uri, {'x-license-key' => @license_key}, "application/octet-stream").tap do |req| + set_params_v1(req) + end + else + uri = "/v2/applications/#{application_id}/deployments.json" + create_request(uri, {"Api-Key" => @api_key}, "application/json").tap do |req| + set_params_v2(req) + end + end + http = ::NewRelic::Agent::NewRelicService.new(nil, control.api_server).http_connection response = http.request(request) if response.is_a?(Net::HTTPSuccess) @@ -108,14 +108,66 @@ def run end end + def api_v1? + @api_key.nil? || @api_key.empty? + end + private + def create_request(uri, headers, content_type) + Net::HTTP::Post.new(uri, headers).tap do |req| + req.content_type = content_type + end + end + + def application_id + return @application_id if @application_id + + # Need to connect to collector to acquire application id from the connect response + # but set monitor_mode false because we don't want to actually report anything + begin + NewRelic::Agent.manual_start(monitor_mode: false) + NewRelic::Agent.agent.connect_to_server + @application_id = NewRelic::Agent.config[:primary_application_id] + ensure + NewRelic::Agent.shutdown + end + end + + def set_params_v1(request) + params = { + :application_id => @appname, + :host => NewRelic::Agent::Hostname.get, + :description => @description, + :user => @user, + :revision => @revision, + :changelog => @changelog + }.each_with_object({}) do |(k, v), h| + h["deployment[#{k}]"] = v unless v.nil? || v == '' + end + request.set_form_data(params) + end + + def set_params_v2(request) + request.body = { + "deployment" => { + :description => @description, + :user => @user, + :revision => @revision, + :changelog => @changelog + } + }.to_json + end + def options OptionParser.new(%Q(Usage: #{$0} #{self.class.command} [OPTIONS] ["description"] ), 40) do |o| o.separator("OPTIONS:") o.on("-a", "--appname=NAME", String, "Set the application name.", - "Default is app_name setting in newrelic.yml") { |e| @appname = e } + "Default is app_name setting in newrelic.yml. Available only when using API v1.") { |e| @appname = e } + o.on("-i", "--appid=ID", String, + "Set the application ID", + "If not provided, will connect to the New Relic collector to get it") { |i| @application_id = i } o.on("-e", "--environment=name", String, "Override the (RAILS|RUBY|RACK)_ENV setting", "currently: #{control.env}") { |e| @environment = e } @@ -123,7 +175,7 @@ def options "Specify the user deploying, for information only", "Default: #{@user || ''}") { |u| @user = u } o.on("-r", "--revision=REV", String, - "Specify the revision being deployed") { |r| @revision = r } + "Specify the revision being deployed. Required when using New Relic REST API v2") { |r| @revision = r } o.on("-l", "--license-key=KEY", String, "Specify the license key of the account for the app being deployed") { |l| @license_key = l } o.on("-c", "--changes", diff --git a/test/multiverse/suites/rack/rack_builder_test.rb b/test/multiverse/suites/rack/rack_builder_test.rb index 57e25aea0c..b6bf890982 100644 --- a/test/multiverse/suites/rack/rack_builder_test.rb +++ b/test/multiverse/suites/rack/rack_builder_test.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 # This file is distributed under New Relic's license terms. # See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. # frozen_string_literal: true diff --git a/test/new_relic/cli/commands/deployments_test.rb b/test/new_relic/cli/commands/deployments_test.rb index dd5d905fd3..5d5c63316b 100644 --- a/test/new_relic/cli/commands/deployments_test.rb +++ b/test/new_relic/cli/commands/deployments_test.rb @@ -24,6 +24,7 @@ def setup def teardown super + mocha_teardown return unless @deployment puts @deployment.errors puts @deployment.messages @@ -61,6 +62,23 @@ def test_interactive @deployment = nil end + def test_interactive_v2 + mock_the_connection + with_config(:api_key => 'fake_api_key') do + @deployment = NewRelic::Cli::Deployments.new(:appname => 'APP', + :revision => 3838, + :application_id => "appid", + :user => 'Bill', + :description => "Some lengthy description") + assert_nil @deployment.exit_status + assert_nil @deployment.errors + assert_equal '3838', @deployment.revision + @deployment.run + refute @deployment.api_v1?, "Using v1 when v2 should be used" + @deployment = nil + end + end + def test_command_line_run mock_the_connection # @mock_response.expects(:body).returns("deployment") @@ -77,6 +95,19 @@ def test_command_line_run @deployment = nil end + def test_command_line_run_v2 + mock_the_connection + with_config(:api_key => 'fake_api_key') do + @deployment = NewRelic::Cli::Deployments.new(%w[-a APP -r 3838 --user=Bill --appid=appid1234] << "Some lengthy description") + assert_nil @deployment.exit_status + assert_nil @deployment.errors + assert_equal '3838', @deployment.revision + @deployment.run + refute @deployment.api_v1?, "Using v1 when v2 should be used" + @deployment = nil + end + end + def test_error_if_no_license_key with_config(:license_key => '') do assert_raises NewRelic::Cli::Command::CommandFailure do @@ -87,6 +118,16 @@ def test_error_if_no_license_key @deployment = nil end + def test_error_if_no_revision_with_api_key + with_config(:api_key => 'fake_api_key') do + assert_raises NewRelic::Cli::Command::CommandFailure do + deployment = NewRelic::Cli::Deployments.new(%w[-a APP --user=Bill] << "Some lengthy description") + deployment.run + end + end + @deployment = nil + end + def test_error_if_failed_yaml NewRelic::Agent::Configuration::YamlSource.any_instance.stubs(:failed?).returns(true) @@ -124,8 +165,30 @@ def test_with_unspecified_license_key @deployment = nil end + def test_gets_appid_from_connect_when_not_provided_with_v2 + mock_the_connection + mock_the_collector + + with_config(:api_key => 'fake_api_key') do + @deployment = NewRelic::Cli::Deployments.new(%w[-a APP -r 3838 --user=Bill] << "Some lengthy description") + assert_nil @deployment.exit_status + assert_nil @deployment.errors + assert_equal '3838', @deployment.revision + @deployment.run + @deployment = nil + end + end + private + def mock_the_collector + NewRelic::Agent.expects(:manual_start) + agent_mock = mock() + NewRelic::Agent.expects(:agent).returns(agent_mock) + agent_mock.expects(:connect_to_server) + NewRelic::Agent.expects(:shutdown) + end + def mock_the_connection mock_connection = mock() @mock_response = mock()