REST plugin concepts

Terminology

REST plugin

A REST plugin, also known as a REST-based plugin, is a plugin that contains procedures implemented using REST requests over HTTP. Note that a REST plugin could have procedures whose implementation does not use REST.

REST methods

REST methods act as proxies for a REST request in a specific language of implementation. For example a REST API endpoint /foobar/info that supports an operation called GET can be represented as a Perl function called getInfo adequately parametrized based on the interface of the REST request. The REST methods generated for Perl use LWP (Library for WWW in Perl) to both perform HTTP requests as well as handle HTTP responses.

REST methods occur in two ways: * Directly corresponding to the implementation of a REST Procedure. For example refer to getUser in the snippets below. * As dependencies that are in turn used by the REST Methods of a REST procedure. For example, refer to uploadReleaseAsset in the snippets below.

REST client

A REST client is a module that is a collection of REST Methods created for the purpose of encapsulation, as well as performance optimization, where a single object can be instantiated to handle REST calls, including authorization, for the life time of the request response cycle in a plugin invocation.

REST procedures

A REST procedure, also known as a REST backed procedure, is a procedure whose implementation involves making a REST request.

User Agent

The LWP::UserAgent is a Perl class implementing a web user agent. LWP::UserAgent objects are used to dispatch web requests. The term userAgent in the pluginspec is used to represent the name of this object.

Usage of REST in pluginspec.yaml

The different usages of REST in pluginspec.yaml are described below.

Plugin configuration

The REST key here represents a dictionary of fields required to create a plugin configuration. Salient fields are described below.

  • defaultEndpointValue represents the root-endpoint for making a HTTP request.

  • checkConnectionUri represents the path that needs to be appended to the root-endpoint in order to make a HTTP request to check connection.

  • authSchemes represent the various authorization schemes that can be used in order to complete the HTTP Request.

It is important to note that the details pertaining to a REST request mentioned in the configuration are implicitly used by all REST methods in the plugin, through the REST client.

# REST-backed plugin configuration
configuration:
    checkConnection: true
    restConfigInfo:
        endpointLabel: 'Github Endpoint'
        endpointDescription: Endpoint to connect to.
        checkConnectionUri: '/user'
        defaultEndpointValue: https://api.github.com
        headers:
            Accept: 'application/json'
        authSchemes:
            basic:
                userNameLabel: 'Username'
                passwordLabel: 'Password'
                description: 'Username and password to connect to ...'
            bearer:
                passwordLabel: 'Bearer token'
                description: 'Bearer token to connect toGitHub.'
            anonymous:
                checkConnectionUri: '/emojis'
    hasProxySupport: true
    hasDebugLevel: true

REST client

The REST key here represents a dictionary of fields required to create a REST client in Perl. Salient fields are described below.

  • The endpoints key represents a list of REST Methods. For each REST method:

    • The methodName represents the name of the Method.

    • The httpMethod represents the REST Verb (Operation).

    • The parameters key represents the list of parameters.

    • The in key represents if the parameter is a PATH or a QUERY parameter.

    • The url represents the path that would be appended to the root-endpoint in the Plugin configuration to make the HTTP request.

    restClient:
        userAgent: MyGitHubClient
        language: perl
        endpoints:
            - methodName: uploadReleaseAsset
              httpMethod: POST
              parameters:
                - name: name
                  in: query
                - name: repositoryOwner
                  in: path
                - name: releaseId
                  in: path
                - name: repositoryName
                  in: path
                url: /repos/{{repositoryOwner}}/{{repositoryName}}/releases/{{releaseId}}/assets

REST procedures

The REST key is used with two connotations in the creation of a REST procedure to:

  • Specify that a certain parameter in a procedure is mapped to a path parameter of its REST implementation. For example refer to getUser in the snippet below.

  • Specify a dictionary of salient fields required to create the REST Method.

    • url which will get appended to the root end-point in plugin configuration when making a HTTP request.

    • methodName which represents the name of the method.

    • httpMethod which represents the REST operation performed.

procedures:
-
    name: 'Get User'
    description: 'This procedure downloads the specified user data'
    hasConfig: true
    shell: 'ec-perl'
    parameters:
    -
        name: username
        documentation: Name of the GitHub user, e.g. `octocat`
        required: true
        type: entry
        label: Username
        restParamInfo:
            in: path
    restProcedureInfo:
        url: '/users/{{username}}'
        methodName: 'getUser'
        httpMethod: 'GET'

Usage of authorization schemes in pluginspec.yaml

Currently the following REST authorization schemes are supported as part of the plugin configuration.

  • basic: Uses a combination of username and password as below. The UI renders the username and password as two different fields.

    authSchemes:
         basic:
             userNameLabel:GitHubUser Name
             passwordLabel:GitHubPassword
             description: 'Username and password to connect to github'
             credentialLabel:GitHubCredentials
  • bearer: Passes a bearer token as part of the authorization header. The UI renders the token as a single field of credential type secret.

    authSchemes:
        bearer:
            passwordLabel: 'Github Token'
            description:GitHubPAT
            prefix: Bearer
  • oauth1: OAuth authorization is performed using a secret and consumer key based on the RSA-SHA1 signature method. This is typically with vendors, like Jira, that support this type of authorization.

    authSchemes:
        oauth1:
            tokenLabel: token
            signatureMethod: RSA-SHA1
  • anonymous: No authorization is performed. This is typically applicable when there is a need for read-only use cases, where a plugin contains procedures that do not need authorization.

    authSchemes:
        anonymous:
            # Since there is no authorization, the URI may be different from the default
            checkConnectionUri: ''

Authorization schemes beyond the above would require custom code to be written by a developer and are beyond the scope of this guide.

Modules generated for REST plugins

For a REST plugin whose name is CB-Git there are two modules that are generated.

CB-Git.pm

# Auto-generated method for the procedure Sample REST Procedure/Sample REST Procedure
# Add your code into this method and it will be called when step runs
sub sampleRESTProcedure {
    my ($pluginObject) = @_;

    my $context = $pluginObject->getContext();
    my $params = $context->getStepParameters();

    my $RestRESTClient = $pluginObject->getRestRESTClient;
    my %params = (
        username => $params->getParameter('username')->getValue,
    );
    my $response = $RestRESTClient->getUser(%params);
    logInfo("Got response from the server: ", $response);

    my $stepResult = $context->newStepResult;

    $stepResult->apply();
}
## === step ends ===

# Generated, once
sub getRestRESTClient {
    my ($self) = @_;

    my $context = $self->getContext();
    my $config = $context->getRuntimeParameters();
    require FlowPlugin::RestRESTClient;
    my $client = FlowPlugin::RestRESTClient->createFromConfig($config);
    return $client;
}

CB-GitRestClient.pm

## === REST client ends, checksum: 57bb11176e6b2635ec06ddf1a2b94d95 ===
sub augmentRequest {
    my ($self, $r, $p) = @_;
    # empty, for user to fill
    return $r;
}

# can be redefined
sub encodePayload {
    my ($self, $payload) = @_;

    return encode_json($payload);
}

sub processResponse {
    my ($self, $response) = @_;

    return undef;
}

# can be redefined
sub parseResponse {
    my ($self, $response) = @_;

    if ($response->content) {
        return decode_json($response->content);
    }
}

1;

Extending generated methods (code snippets)

Special getters

In all redefined methods you may use the following getters:

  • Returns the name of the generated method provided in the pluginspec.yaml:

    print $self->method
  • Returns the map with parameters passed to the method from the original call:

    $self->methodParameters
  • Gets the original method name and original request parameters:

    # Somewhere in the calling code
    $client->methodName(param1 => $param1);
    
    # Somewhere in the REST client
    if ($self->method eq 'methodName') {
        my $parameters = $self->methodParameters;
        print $parameters->{param1};
    }

Custom authorization

Pass signing secrets to the client during object creation.

sub augmentRequest {
    my ($self, $r, $params) = @_;
    my $secret = $self->{secret};
    my $signature = md5_hex($secret . $r->uri . $r->method . $r->content);
    $r->header('X-Signature', $signature);
    return $r;
}

File upload

sub augmentRequest {
    my ($self, $r, $params) = @_;
    # empty, for user to fill

    if ($self->method eq 'uploadFile') {
        open my $fh, $params->{fileName} or die "Cannot open file $params->{fileName}: $!";
        binmode $fh;
        my $bytes = join '' => $fh;
        close $fh;
        $r->content($bytes);
    }
    return $r;
}

Altering default payload structure

sub encodePayload {
    my ($self, $payload) = @_;

    if ($self->method eq 'createIssue') {
        $payload = {
        issue => {title => $payload->{title}}
    };
    }

    return encode_json($payload);
}

Error message processing

sub processResponse {
    my ($self, $response) = @_;

    if ($response->is_success) {
        return;
    }

    my $json = decode_json($response->content);
    my $message = $json->{error} || "No error message received";
    die "Failed to execute request: $message";
}

Wrapping response JSON with objects

sub parseResponse {
    my ($self, $response) = @_;

    if ($self->method eq 'createRepository') {
        my $rawData = decode_json($response->content);
        my $repository = GH::Client::Repository->new($rawData);
        return $repository;
    }

    if ($response->content) {
    return decode_json($response->content);
    }
}