diff --git a/lib/dogapi/facade.rb b/lib/dogapi/facade.rb index c5e412a7..94222270 100644 --- a/lib/dogapi/facade.rb +++ b/lib/dogapi/facade.rb @@ -59,6 +59,7 @@ class Client # rubocop:disable Metrics/ClassLength attr_accessor :v2 # Support for API version 2. + # rubocop:disable Metrics/MethodLength, Metrics/LineLength def initialize(api_key, application_key=nil, host=nil, device=nil, silent=true, timeout=nil, endpoint=nil) if api_key @@ -94,12 +95,16 @@ def initialize(api_key, application_key=nil, host=nil, device=nil, silent=true, @legacy_event_svc = Dogapi::EventService.new(@datadog_host) @hosts_svc = Dogapi::V1::HostsService.new(@api_key, @application_key, silent, timeout, @datadog_host) @integration_svc = Dogapi::V1::IntegrationService.new(@api_key, @application_key, silent, timeout, @datadog_host) + @aws_integration_svc = Dogapi::V1::AwsIntegrationService.new(@api_key, @application_key, silent, timeout, @datadog_host) + @aws_logs_svc = Dogapi::V1::AwsLogsService.new(@api_key, @application_key, silent, timeout, @datadog_host) @usage_svc = Dogapi::V1::UsageService.new(@api_key, @application_key, silent, timeout, @datadog_host) - + @azure_integration_svc = Dogapi::V1::AzureIntegrationService.new(@api_key, @application_key, silent, timeout, @datadog_host) + @gcp_integration_svc = Dogapi::V1::GcpIntegrationService.new(@api_key, @application_key, silent, timeout, @datadog_host) # Support for Dashboard List API v2. @v2 = Dogapi::ClientV2.new(@api_key, @application_key, true, true, @datadog_host) end + # rubocop:enable Metrics/MethodLength, Metrics/LineLength # # METRICS @@ -682,6 +687,108 @@ def delete_integration(source_type_name) @integration_svc.delete_integration(source_type_name) end + # + # AWS INTEGRATION + # + def aws_integration_list + @aws_integration_svc.aws_integration_list + end + + def aws_integration_create(config) + @aws_integration_svc.aws_integration_create(config) + end + + def aws_integration_delete(config) + @aws_integration_svc.aws_integration_delete(config) + end + + def aws_integration_list_namespaces + @aws_integration_svc.aws_integration_list_namespaces + end + + def aws_integration_generate_external_id(config) + @aws_integration_svc.aws_integration_generate_external_id(config) + end + + def aws_integration_update(config, new_config) + @aws_integration_svc.aws_integration_update(config, new_config) + end + + # + # AWS Logs Integration + # + + def aws_logs_add_lambda(config) + @aws_logs_svc.aws_logs_add_lambda(config) + end + + def aws_logs_list_services + @aws_logs_svc.aws_logs_list_services + end + + def aws_logs_save_services(config) + @aws_logs_svc.aws_logs_save_services(config) + end + + def aws_logs_integrations_list + @aws_logs_svc.aws_logs_integrations_list + end + + def aws_logs_integration_delete(config) + @aws_logs_svc.aws_logs_integration_delete(config) + end + + def aws_logs_check_lambda(config) + @aws_logs_svc.aws_logs_check_lambda(config) + end + + def aws_logs_check_services(config) + @aws_logs_svc.aws_logs_check_services(config) + end + + # + # AZURE INTEGRATION + # + + def azure_integration_list + @azure_integration_svc.azure_integration_list + end + + def azure_integration_create(config) + @azure_integration_svc.azure_integration_create(config) + end + + def azure_integration_delete(config) + @azure_integration_svc.azure_integration_delete(config) + end + + def azure_integration_update_host_filters(config) + @azure_integration_svc.azure_integration_update_host_filters(config) + end + + def azure_integration_update(config) + @azure_integration_svc.azure_integration_update(config) + end + + # + # GCP INTEGRATION + # + def gcp_integration_list + @gcp_integration_svc.gcp_integration_list + end + + def gcp_integration_delete(config) + @gcp_integration_svc.gcp_integration_delete(config) + end + + def gcp_integration_create(config) + @gcp_integration_svc.gcp_integration_create(config) + end + + def gcp_integration_update(config) + @gcp_integration_svc.gcp_integration_update(config) + end + # # USAGE # diff --git a/lib/dogapi/v1.rb b/lib/dogapi/v1.rb index 50fdcbf5..2c9be7a6 100644 --- a/lib/dogapi/v1.rb +++ b/lib/dogapi/v1.rb @@ -17,3 +17,7 @@ require 'dogapi/v1/hosts' require 'dogapi/v1/integration' require 'dogapi/v1/usage' +require 'dogapi/v1/aws_integration' +require 'dogapi/v1/aws_logs' +require 'dogapi/v1/azure_integration' +require 'dogapi/v1/gcp_integration' diff --git a/lib/dogapi/v1/aws_integration.rb b/lib/dogapi/v1/aws_integration.rb new file mode 100644 index 00000000..23451eba --- /dev/null +++ b/lib/dogapi/v1/aws_integration.rb @@ -0,0 +1,113 @@ +require 'dogapi' + +module Dogapi + class V1 # for namespacing + + # AwsIntegrationService for user interaction with AWS configs. + class AwsIntegrationService < Dogapi::APIService + + API_VERSION = 'v1' + + # Retrieve AWS integration information + def aws_integration_list + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/aws", nil, nil, false) + end + + # Create an AWS integration + # :config => Hash: integration config. + # config = { + # :account_id => '', + # :host_tags => ['api:example'], + # :role_name => '' + # } + # + # Access Key/Secret Access Key based accounts (GovCloud and China only) + # + # config = { + # :access_key_id => '', + # :host_tags => ['api:example'], + # :secret_access_key => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_integration_create(config) + def aws_integration_create(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/aws", nil, config, true) + end + + # Delete an integration + # :config => Hash: integration config. + # config = { + # :account_id => '', + # :role_name => '' + # } + # Access Key/Secret Access Key based accounts (GovCloud and China only) + # + # config = { + # :access_key_id => '', + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_integration_delete(config) + def aws_integration_delete(config) + request(Net::HTTP::Delete, "/api/#{API_VERSION}/integration/aws", nil, config, true) + end + + # List available AWS namespaces + def aws_integration_list_namespaces + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/aws/available_namespace_rules", nil, nil, false) + end + + # Generate new AWS external ID for a specific integrated account + # :config => Hash: integration config. + # config = { + # :account_id => '', + # :role_name => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_integration_generate_external_id(config) + def aws_integration_generate_external_id(config) + request(Net::HTTP::Put, "/api/#{API_VERSION}/integration/aws/generate_new_external_id", nil, config, true) + end + + # Update integrated AWS account. + # :config => Hash: integration config. + # config = { + # "account_id": '', + # "role_name": '' + # } + # + # new_config = { + # "account_id": '', + # "host_tags": ['tag:example1,tag:example2'], + # "filter_tags": ['datadog:true'] + # } + # + # Access Key/Secret Access Key based accounts (GovCloud and China only) + # + # config = { + # "access_key_id": '', + # "secret_access_key": '' + # } + # + # new_config = { + # "access_key_id": '', + # "host_tags": ['new:tags'], + # "filter_tags": ['datadog:true'] + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + + # puts dog.aws_integration_update(config, new_config) + def aws_integration_update(config, new_config) + request(Net::HTTP::Put, "/api/#{API_VERSION}/integration/aws", config, new_config, true) + end + + end + + end +end diff --git a/lib/dogapi/v1/aws_logs.rb b/lib/dogapi/v1/aws_logs.rb new file mode 100644 index 00000000..cf05ca41 --- /dev/null +++ b/lib/dogapi/v1/aws_logs.rb @@ -0,0 +1,103 @@ +require 'dogapi' + +module Dogapi + class V1 # for namespacing + + # AwsLogsService for user interaction with AWS configs. + class AwsLogsService < Dogapi::APIService + + API_VERSION = 'v1' + + # Get the list of current AWS services for which Datadog offers automatic log collection. + # Use returned service IDs with the services parameter for the Enable + # an AWS service log collection API endpoint. + def aws_logs_list_services + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/aws/logs/services", nil, nil, false) + end + + # Create an AWS integration + # :config => Hash: integration config. + # config = { + # :account_id => '', + # :lambda_arn => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_logs_add_lambda(config) + def aws_logs_add_lambda(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/aws/logs", nil, config, true) + end + + # List all Datadog-AWS Logs integrations configured in your Datadog account. + def aws_logs_integrations_list + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/aws/logs", nil, nil, false) + end + + # Enable automatic log collection for a list of services. + # This should be run after running 'aws_logs_add_lambda' to save the config. + # config = { + # :account_id => '', + # :services => ['s3', 'elb', 'elbv2', 'cloudfront', 'redshift', 'lambda'] + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_logs_save_services(config) + def aws_logs_save_services(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/aws/logs/services", nil, config, true) + end + + # Delete an AWS Logs integration + # :config => Hash: integration config. + # config = { + # :account_id => '', + # :lambda_arn => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.aws_logs_integration_delete(config) + def aws_logs_integration_delete(config) + request(Net::HTTP::Delete, "/api/#{API_VERSION}/integration/aws/logs", nil, config, true) + end + + # Check function to see if a lambda_arn exists within an account. + # This sends a job on our side if it does not exist, then immediately returns + # the status of that job. Subsequent requests will always repeat the above, so this endpoint + # can be polled intermittently instead of blocking. + + # Returns a status of 'created' when it's checking if the Lambda exists in the account. + # Returns a status of 'waiting' while checking. + # Returns a status of 'checked and ok' if the Lambda exists. + # Returns a status of 'error' if the Lambda does not exist. + + # contents of config should be + # >>> :account_id => '' + # >>> :lambda_arn => '' + + def aws_logs_check_lambda(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/aws/logs/check_async", nil, config, true) + end + + # Test if permissions are present to add log-forwarding triggers for the + # given services + AWS account. Input is the same as for save_services. + # Done async, so can be repeatedly polled in a non-blocking fashion until + # the async request completes + + # Returns a status of 'created' when it's checking if the permissions exists in the AWS account. + # Returns a status of 'waiting' while checking. + # Returns a status of 'checked and ok' if the Lambda exists. + # Returns a status of 'error' if the Lambda does not exist. + + # contents of config should be + # :account_id => '' + # :services => ['s3', 'elb', 'elbv2', 'cloudfront', 'redshift', 'lambda'] + def aws_logs_check_services(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/aws/logs/services_async", nil, config, true) + end + + end + + end +end diff --git a/lib/dogapi/v1/azure_integration.rb b/lib/dogapi/v1/azure_integration.rb new file mode 100644 index 00000000..732e138e --- /dev/null +++ b/lib/dogapi/v1/azure_integration.rb @@ -0,0 +1,81 @@ +require 'dogapi' + +module Dogapi + class V1 # for namespacing + + # AzureIntegrationService for user interaction with Azure configs. + class AzureIntegrationService < Dogapi::APIService + + API_VERSION = 'v1' + + # Retrieve Azure integration information + def azure_integration_list + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/azure", nil, nil, false) + end + + # Delete an Azure integration + # :config => Hash: integration config. + # config = { + # :tenant_name => '', + # :client_id => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.azure_integration_delete(config) + def azure_integration_delete(config) + request(Net::HTTP::Delete, "/api/#{API_VERSION}/integration/azure", nil, config, true) + end + + # Create an Azure integration + # :config => Hash: integration config. + # config = { + # :tenant_name => '', + # :host_filters => 'new:filter', + # :client_id => '', + # :client_secret => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.azure_integration_create(config) + def azure_integration_create(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/azure", nil, config, true) + end + + # Update an Azure integrations host filters + # :config => Hash: integration config. + # config = { + # :tenant_name => '', + # :host_filters => 'new:filter', + # :client_id => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.azure_integration_update_host_filters(config) + def azure_integration_update_host_filters(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/azure/host_filters", nil, config, true) + end + + # Update a configured Azure account. + # :config => Hash: integration config. + # config = { + # :tenant_name => '', + # :new_tenant_name => '', + # :host_filters => ':,:', + # :client_id => '', + # :new_client_id => '' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.azure_integration_update(config) + def azure_integration_update(config) + request(Net::HTTP::Put, "/api/#{API_VERSION}/integration/azure", nil, config, true) + end + + end + + end +end diff --git a/lib/dogapi/v1/gcp_integration.rb b/lib/dogapi/v1/gcp_integration.rb new file mode 100644 index 00000000..ae7b7615 --- /dev/null +++ b/lib/dogapi/v1/gcp_integration.rb @@ -0,0 +1,72 @@ +require 'dogapi' + +module Dogapi + class V1 # for namespacing + + # GcpIntegrationService for user interaction with gcp configs. + class GcpIntegrationService < Dogapi::APIService + + API_VERSION = 'v1' + + # Retrieve gcp integration information + def gcp_integration_list + request(Net::HTTP::Get, "/api/#{API_VERSION}/integration/gcp", nil, nil, false) + end + + # Delete an gcp integration + # :config => Hash: integration config. + # config = { + # :project_id => 'datadog-sandbox', + # :client_email => 'email@example.com' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.gcp_integration_delete(config) + def gcp_integration_delete(config) + request(Net::HTTP::Delete, "/api/#{API_VERSION}/integration/gcp", nil, config, true) + end + + # Create an gcp integration + # :config => Hash: integration config. + # config = { + # :type => 'service_account', + # :project_id => '', + # :private_key_id => '', + # :private_key => '', + # :client_email => '', + # :client_id => '', + # :auth_uri => '', + # :token_uri => '', + # :auth_provider_x509_cert_url => '', + # :client_x509_cert_url => '', + # :host_filters => ':,:,' + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.gcp_integration_create(config) + def gcp_integration_create(config) + request(Net::HTTP::Post, "/api/#{API_VERSION}/integration/gcp", nil, config, true) + end + + # Update a configured gcp account. + # :config => Hash: integration config. + # config = { + # :project_id => '', + # :client_email => '', + # :host_filters => ':,:,' + # :automute => true # takes a boolean and toggles GCE automuting + # } + # + # dog = Dogapi::Client.new(api_key, app_key) + # + # puts dog.gcp_integration_update(config) + def gcp_integration_update(config) + request(Net::HTTP::Put, "/api/#{API_VERSION}/integration/gcp", nil, config, true) + end + + end + + end +end diff --git a/spec/integration/aws_integration_spec.rb b/spec/integration/aws_integration_spec.rb new file mode 100644 index 00000000..b271035a --- /dev/null +++ b/spec/integration/aws_integration_spec.rb @@ -0,0 +1,51 @@ +require_relative '../spec_helper' + +describe Dogapi::Client do + CONFIG = { + account_id: '123456789101', + role_name: 'DatadogApiTestRole' + }.freeze + + UPDATE_CONFIG = { + account_id: '123456789102', + filter_tags: ['datadog:true'], + host_tags: ['api:test'], + role_name: 'DatadogApiTestRole' + }.freeze + + describe '#aws_integration_create' do + it_behaves_like 'an api method', + :aws_integration_create, [CONFIG], + :post, '/integration/aws', CONFIG + end + + describe '#aws_integration_list' do + it_behaves_like 'an api method', + :aws_integration_list, nil, + :get, '/integration/aws' + end + + describe '#aws_integration_list_namespaces' do + it_behaves_like 'an api method', + :aws_integration_list_namespaces, nil, + :get, '/integration/aws/available_namespace_rules' + end + + describe '#aws_integration_generate_external_id' do + it_behaves_like 'an api method', + :aws_integration_generate_external_id, [CONFIG], + :put, '/integration/aws/generate_new_external_id', CONFIG + end + + describe '#aws_integration_update' do + it_behaves_like 'an api method with params and body', + :aws_integration_update, [CONFIG, UPDATE_CONFIG], + :put, '/integration/aws', CONFIG, UPDATE_CONFIG + end + + describe '#aws_integration_delete' do + it_behaves_like 'an api method', + :aws_integration_delete, [CONFIG], + :delete, '/integration/aws', CONFIG + end +end diff --git a/spec/integration/aws_logs_spec.rb b/spec/integration/aws_logs_spec.rb new file mode 100644 index 00000000..168bf75a --- /dev/null +++ b/spec/integration/aws_logs_spec.rb @@ -0,0 +1,55 @@ +require_relative '../spec_helper' + +describe Dogapi::Client do + CONFIG = { + account_id: '123456789101', + lambda_arn: 'arn:aws:lambda:us-east-1:123456789101:function:LogsCollectionAPITest' + }.freeze + + SERVICES_CONFIG = { + account_id: '601427279990', + services: %w([s3] [elb] [elbv2] [cloudfront] [redshift] [lambda]) + }.freeze + + describe '#aws_logs_add_lambda' do + it_behaves_like 'an api method', + :aws_logs_add_lambda, [CONFIG], + :post, '/integration/aws/logs', CONFIG + end + + describe '#aws_logs_save_services' do + it_behaves_like 'an api method', + :aws_logs_save_services, [SERVICES_CONFIG], + :post, '/integration/aws/logs/services', SERVICES_CONFIG + end + + describe '#aws_logs_check_services' do + it_behaves_like 'an api method', + :aws_logs_check_services, [SERVICES_CONFIG], + :post, '/integration/aws/logs/services_async', SERVICES_CONFIG + end + + describe '#aws_logs_check_lambda' do + it_behaves_like 'an api method', + :aws_logs_check_lambda, [CONFIG], + :post, '/integration/aws/logs/check_async', CONFIG + end + + describe '#aws_logs_list_services' do + it_behaves_like 'an api method', + :aws_logs_list_services, nil, + :get, '/integration/aws/logs/services' + end + + describe '#aws_logs_integrations_list' do + it_behaves_like 'an api method', + :aws_logs_integrations_list, nil, + :get, '/integration/aws/logs' + end + + describe '#aws_logs_integration_delete' do + it_behaves_like 'an api method', + :aws_logs_integration_delete, [CONFIG], + :delete, '/integration/aws/logs', CONFIG + end +end diff --git a/spec/integration/azure_integration_spec.rb b/spec/integration/azure_integration_spec.rb new file mode 100644 index 00000000..eecb5c9c --- /dev/null +++ b/spec/integration/azure_integration_spec.rb @@ -0,0 +1,59 @@ +require_relative '../spec_helper' + +describe Dogapi::Client do + CONFIG = { + tenant_name: 'testc44-1234-5678-9101-cc00736ftest', + client_id: 'testc7f6-1234-5678-9101-3fcbf464test' + }.freeze + + CREATE_CONFIG = { + tenant_name: 'testc44-1234-5678-9101-cc00736ftest', + host_filters: 'api:test', + client_id: 'testc7f6-1234-5678-9101-3fcbf464test', + client_secret: 'testingx./Sw*g/Y33t..R1cH+hScMDt' + }.freeze + + UPDATE_HF_CONFIG = { + tenant_name: 'testc44-1234-5678-9101-cc00736ftest', + host_filters: 'api:test1,api:test2', + client_id: 'testc7f6-1234-5678-9101-3fcbf464test' + }.freeze + + UPDATE_CONFIG = { + tenant_name: 'testc44-1234-5678-9101-cc00736ftest', + new_tenant_name: '1234abcd-1234-5678-9101-abcd1234abcd', + host_filters: 'api:test3', + client_id: 'testc7f6-1234-5678-9101-3fcbf464test', + new_client_id: 'abcd1234-5678-1234-5678-1234abcd5678' + }.freeze + + describe '#azure_integration_list' do + it_behaves_like 'an api method', + :azure_integration_list, nil, + :get, '/integration/azure' + end + + describe '#azure_integration_create' do + it_behaves_like 'an api method', + :azure_integration_create, [CREATE_CONFIG], + :post, '/integration/azure', CREATE_CONFIG + end + + describe '#azure_integration_update_host_filters' do + it_behaves_like 'an api method', + :azure_integration_update_host_filters, [UPDATE_HF_CONFIG], + :post, '/integration/azure/host_filters', UPDATE_HF_CONFIG + end + + describe '#azure_integration_update' do + it_behaves_like 'an api method', + :azure_integration_update, [UPDATE_CONFIG], + :put, '/integration/azure', UPDATE_CONFIG + end + + describe '#azure_integration_delete' do + it_behaves_like 'an api method', + :azure_integration_delete, [CONFIG], + :delete, '/integration/azure', CONFIG + end +end diff --git a/spec/integration/gcp_integration_spec.rb b/spec/integration/gcp_integration_spec.rb new file mode 100644 index 00000000..303ae2df --- /dev/null +++ b/spec/integration/gcp_integration_spec.rb @@ -0,0 +1,53 @@ +require_relative '../spec_helper' + +describe Dogapi::Client do + CONFIG = { + project_id: 'datadog-apitest', + client_email: 'email@example.com' + }.freeze + + CREATE_CONFIG = { + type: 'service_account', + project_id: 'datadog-apitest', + private_key_id: '123456789abcdefghi123456789abcdefghijklm', + private_key: 'fake_key', + client_email: 'email@example.com', + client_id: '123456712345671234567', + auth_uri: 'fake_uri', + token_uri: 'fake_uri', + auth_provider_x509_cert_url: 'fake_url', + client_x509_cert_url: 'fake_url', + host_filters: 'api:test' + }.freeze + + UPDATE_CONFIG = { + project_id: 'datadog-apitest', + client_email: 'email@example.com', + host_filters: 'api:test1,api:test2', + automute: false + }.freeze + + describe '#gcp_integration_list' do + it_behaves_like 'an api method', + :gcp_integration_list, nil, + :get, '/integration/gcp' + end + + describe '#gcp_integration_create' do + it_behaves_like 'an api method', + :gcp_integration_create, [CREATE_CONFIG], + :post, '/integration/gcp', CREATE_CONFIG + end + + describe '#gcp_integration_update' do + it_behaves_like 'an api method', + :gcp_integration_update, [UPDATE_CONFIG], + :put, '/integration/gcp', UPDATE_CONFIG + end + + describe '#gcp_integration_delete' do + it_behaves_like 'an api method', + :gcp_integration_delete, [CONFIG], + :delete, '/integration/gcp', CONFIG + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1e4c47b3..5a798185 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -73,6 +73,23 @@ module SpecDog end end + shared_examples 'an api method with params and body' do |command, args, request, endpoint, params, body| + it 'queries the api with params and body' do + url = api_url + endpoint + stub_request(request, /#{url}/).to_return(body: '{}').then.to_raise(StandardError) + expect(dog.send(command, *args)).to eq ['200', {}] + params.each { |k, v| params[k] = v.join(',') if v.is_a? Array } + params = params + + body = MultiJson.dump(body) if body + + expect(WebMock).to have_requested(request, url).with( + query: params, + body: body + ) + end + end + shared_examples 'an api method with optional params' do |command, args, request, endpoint, opt_params| include_examples 'an api method', command, args, request, endpoint it 'queries the api with optional params' do