X

Break New Ground

Making Quick-and-Dirty REST calls to the OCI API in Ruby

Oracle Cloud Infrastructure (OCI) offers Software Development Kits (SDKs) for many popular programming languages, including Java, TypeScript, Go, .NET, Python and Ruby.  Check out the OCI SDK documentation for more information on the SDKs.  SDKs are really nice, with the OCI Ruby SDK being no exception!  They typically follow the specific idioms and common best practices for each language, allowing developers to interact with OCI in a familiar manner.  There are times when I don't want all of the structure, "definition" and overhead.  There are times when I need to make a "fast-and-dirty" call to the OCI API, the equivalent of calling curl in my language of choice.  To be clear, this is not meant to deprecate or eliminate the usage of the Ruby SDK, as it's very useful and a great tool to have in our toolkit.  This article is about exploring an alternative, lightweight, less refined way of interacting with the OCI API - talking to it directly.  While there are times for this, it'll likely be the exception rather than the norm (with the SDK being our primary go-to when interacting with OCI in Ruby).

Now that we understand that we're just looking for a way to directly interact with the OCI API with as few layers as possible, let's explore how to do this in just a few lines of Ruby code.

Calling the OCI API Directly

The OCI documentation contains a great page that can start off our journey of how to make "lightweight" API calls to OCI using Ruby.  One example on this page is for Ruby... we'll take this example and iterate off of it a bit.

Before we dive into this, let's make sure that we have some prerequisites sorted out...

Prerequisites

There are a few things that you'll need to follow along:

  • OCI tenancy (if you don't have one, click here to sign-up for free)
  • OCI user account with an API key configured
  • OCI config file
  • Ruby
  • OCI Ruby SDK

OCI Tenancy

If you don't have one yet... click here to sign-up for an Oracle free tier account now!

OCI User Account

Follow the directions in the OCI documentation if you've not set up an API key for your user account.

OCI Config File

If you don't have the OCI CLI installed, check out the installation docs.  While the OCI CLI is not required, it is handy to have it available, particularly for the testing of our OCI config file.  Here's a quick summary of what I used in my OCI config file to get going:

[DEFAULT]
user=YOUR_USER_OCID
fingerprint=YOUR_API_KEY_FINGERPRINT
key_file=FULL_PATH_TO_YOUR_API_PRIVATE_KEY
tenancy=YOUR_TENANCY_OCID
region=YOUR_REGION_HERE

The above config is not exhaustive, so if you're having trouble or want more information, check out the docs. If you're wondering where to get your tenancy OCID, your fingerprint, etc. take a look at this page which will point you to where/how to get this info.

To make sure that things are working, you can try the following OCI CLI command:

$ oci iam user get --user-id 

NOTE: If you're using a non-standard OCI config file name, you'll need to add the --config-file <PATH TO YOUR OCI CONFIG FILE> parameter to the above command.

If you have the OCI CLI installed and your config file is working, you should see some output similar to the following:

{
  "data": {
    "capabilities": {
      "can-use-api-keys": true,
      "can-use-auth-tokens": true,
      "can-use-console-password": true,
      "can-use-customer-secret-keys": true,
      "can-use-o-auth2-client-credentials": true,
      "can-use-smtp-credentials": true
    },
    "compartment-id": "",
    "defined-tags": {},
    "description": "",
    "email": "",
    "external-identifier": null,
    "freeform-tags": {},
    "id": "",
    "identity-provider-id": null,
    "inactive-status": null,
    "is-mfa-activated": false,
    "lifecycle-state": "ACTIVE",
    "name": "",
    "time-created": "2021-02-03T16:59:43.768000+00:00"
  },
  "etag": ""
}

If you get an error, revisit your OCI config file. Once things are working, then continue onward!

Ruby

I'm using rbenv to manage my Ruby installations and chose to use Ruby 2.6.1 for this exercise.  You don't have to use rbenv (you might prefer rvm or other solutions).  In my scenario, I already had rbenv installed and had installed the 2.6.1 Ruby runtime (which is why I was able to tell it just to use it).  See the rbenv docs for how to install rbenv, plus install Ruby versions, etc. as needed.  Here's what I did to set things up on my MacOS system:

$ rbenv local 2.6.1
 
$ rbenv version
2.6.1 (set by )
 
$ ruby -v
ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin17]

You can use a different version and it'll likely not cause issues... this is what I used (so if needed, consider trying this one).

OCI Ruby SDK

While we're going to be sending requests to the OCI API, we'll grab a few handy classes from the Ruby SDK, so let's install it:

$ gem install oci
 
....

Running It

Let's take the example from the OCI docs and modify it just a bit so that we have the following:

# Copyright (c) 2016, 2020, Oracle and/or its affiliates.  All rights reserved.
# This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or
# Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
 
require 'oci'
 
config = OCI::ConfigFileLoader.load_config(config_file_location: 'my_config', profile_name: 'USER_TWO')
endpoint = OCI::Regions.get_service_endpoint(config.region, :IdentityClient)
 
uri = URI(endpoint + '/20160918/users/' + config.user)
request = Net::HTTP::Get.new(uri)
 
signer = OCI::Signer.new(config.user, config.fingerprint, config.tenancy, config.key_file, pass_phrase:my_private_key_pass_phrase)
signer.sign(:get, uri.to_s, request, nil)
 
result = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) {|http|
http.request(request)
}
 
puts result.body

Make sure that you set the config_file_location to the actual path and filename of your config file (example: instead of just 'my_config', replace it with something like '/home/me/.oci/config', but this should be tailored to what you use on your machine). Also, make sure that profile_name points to an actual profile name - by default, it'll be default (in which case you can just remove profile_name altogether, or set it to a value of 'DEFAULT'), however, any other non-default profile name can be specified here (again, tailor to what you need/use). See the OCI Ruby SDK docs for info on the default filename (and location) as well as the profile name.

NOTE: If your API private key doesn't have a passphrase, make sure to remove pass_phrase from the end of line 13!

Now to run it:

$ ruby ruby-oci-api-1.rb
{"capabilities":{"canUseConsolePassword":true,"canUseApiKeys":true,"canUseAuthTokens":true,"canUseSmtpCredentials":true,"canUseCustomerSecretKeys":true,"canUseOAuth2ClientCredentials":true,"canUseDbCredentials":true},"email":"","emailVerified":true,"identityProviderId":null,"externalIdentifier":null,"timeModified":"2021-02-18T18:36:01.395Z","isMfaActivated":false,"lastSuccessfulLoginTime":"2021-02-18T18:36:01.361Z","id":"","compartmentId":"","name":"","description":"","timeCreated":"2021-02-03T16:59:43.768Z","freeformTags":{},"definedTags":{},"lifecycleState":"ACTIVE"}

It's not pretty, but it worked! Terrific... now we can issue whatever kind of calls to the OCI API that we might want. Way to go!

Improving This a Bit

We really just franken-copied the code from the OCI docs, so we've not achieved anything really ground-breaking. What if we have several different API calls to make? Sure, we can wash-rinse-repeat what we have, but could this be refactored a bit to better support this multi-call interaction? Absolutely!

Consider the following:

# Copyright (c) 2016, 2020, 2021, Oracle and/or its affiliates.  All rights reserved.
# This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or
# Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
 
require 'oci'
 
config = OCI::ConfigFileLoader.load_config(config_file_location:'my_config', profile_name:'USER_TWO')
signer = OCI::Signer.config_file_auth_builder(config)
client = OCI::ApiClient.new(config, signer)
 
cfg_endpoint = OCI::Regions.get_service_endpoint(config.region, :IdentityClient)
method = :get
path = "/users/#{config.user}"
endpoint = "#{cfg_endpoint}/20160918"
body = nil
opts = {
    query_params: {},
    form_params: {},
    body: nil,
    operation_signing_strategy: nil
}
response = client.call_api(method, path, endpoint, opts) {|t| body = t }
 
# to see the response code
puts response.status
# to see the response headers
puts response.headers
# to see the response body
puts body

What've we done here? We've taken the original code, refactored it a bit and come up with another way to streamline things a bit. Here are a few call-outs to take note of:

  • There are a couple of different ways of authenticating to the OCI API - this is just one (using the standard method, using an API key). Take a look at the OCI Ruby SDK documentation for the different options available (such as Instance Principals authentication).
  • The service endpoints might differ, depending on the service you're wanting to interact with. Cross-reference the regions.rb file in the SDK with the OCI API documentation as needed to find the right endpoint.

Taking it a step further...

Lines 11-22 could easily be put into a class and/or method which could be called repeatedly (though unless you're dealing with the same endpoint, you'd likely need to alter line 12 too). Here's an incomplete idea (think pseudo-code) of what I'm thinking:

class OciApi
  def initialize(cfg_file, profile_name)
    # read config file, profile, etc. here...
    #   set config/signer variables, etc.
  end
   
  def make_request(endpoint_type, path, type, body, opts)
    # make the call!
    # return the response!
  end
end

This way, all we'd have to do is something like the following (again, incomplete code here to convey the idea):

api = OciApi.new(my_file, 'DEFAULT')
for u in compartments do |user_ids|
  resp = api.make_request(:IdentityClient, "/users/#{u}", :get, nil, {})
  puts "ERROR" if resp.status != 200
end

The above is a trivial example but shows it might be possible to invoke just a single method for each API request. Not bad, right?! Here's a working version of this:

# Copyright (c) 2016, 2020, Oracle and/or its affiliates.  All rights reserved.
# This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or
# Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
 
module OCI
  class Response
    attr_accessor :body
     
    def initialize(status, headers, data)
      @body = nil
       
      super
    end
  end
end
 
class OciApi
  require 'oci'
   
  def initialize(cfg_file = nil, profile_name = 'DEFAULT')
    @config = OCI::ConfigFileLoader.load_config(config_file_location: cfg_file, profile_name: profile_name)
    signer = OCI::Signer.config_file_auth_builder(@config)
    @client = OCI::ApiClient.new(@config, signer)
  end
     
  def make_request(endpoint_type, path, method = :get, body = nil, opts = {})
    endpoint = OCI::Regions.get_service_endpoint(@config.region, endpoint_type)
    body = nil
    response = @client.call_api(method, path, endpoint, opts) {|t| body = t }
    response.body = body
    response
  end
end
 
users = ['']
 
api = OciApi.new('my_config', 'USER_TWO')
for user_id in users do
  resp = api.make_request(:IdentityClient, "/20160918/users/#{user_id}", :get)
  puts "ERROR" if resp.status != 200
  puts resp.status
  puts resp.body
end

We monkey-patched in a body attribute to the Response class (which is in the OCI module), which is both readable and writable... while this isn't necessarily the best solution, it works for this trivial example. We're able to capture the body and place it in this attribute (variable), which allows us to read it later.

Running this last iteration, we get:

$ ruby ruby-oci-api-3.rb
200
{"capabilities":{"canUseConsolePassword":true,"canUseApiKeys":true,"canUseAuthTokens":true,"canUseSmtpCredentials":true,"canUseCustomerSecretKeys":true,"canUseOAuth2ClientCredentials":true,"canUseDbCredentials":true},"email":"","emailVerified":true,"identityProviderId":null,"externalIdentifier":null,"timeModified":"2021-02-18T18:36:01.395Z","isMfaActivated":false,"lastSuccessfulLoginTime":"2021-02-18T18:36:01.361Z","id":"","compartmentId":"","name":"","description":"","timeCreated":"2021-02-03T16:59:43.768Z","freeformTags":{},"definedTags":{},"lifecycleState":"ACTIVE"}

Conclusion

We've covered a bit of ground. When using Ruby, it's possible to leverage the power and all of the object-oriented goodness that the OCI Ruby SDK offers, yet also possible to make lightweight calls directly to the OCI API. If we got to the point where we didn't want all of the extra overhead of the Ruby SDK, we could explore using only a minimal set of classes (rather than the whole SDK). This is another exercise for another time... suffice it to say, you've got options when you need to interact with the OCI API in Ruby. Have fun interacting with the OCI API in Ruby!

Image by Roger Mosley from Pixabay

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.