Tenant

The main provisioning logic is concentrated in the tenants service and its tenant APS type.

../../../../_images/step-project2.png ../../../../_images/step-meta2.png ../../../../_images/step-provision-b.png ../../../../_images/step-presentation1.png ../../../../_images/step-deploy2.png ../../../../_images/step-provisioning2.png

Implement the required components in the following files:

  • scripts/rest-utils.php defines reusable utilities to operate HTTP requests sent through curl.

  • scripts/tenants.php defines the tenants services and the tenant APS type. The APS type definition here will be used by the APS PHP runtime only.

  • schemas/tenant.schema contains the tenant APS type definition for the APS controller.

../../../../_images/step-provision-app.png ../../../../_images/step-provision-lic.png ../../../../_images/step-provision-tenant-b.png

REST Utilities

Declaration

Although some of operations exposed by GitHub API work well with the authentication using a security token, the others require the basic authentication using a “login:password” pair.

In scripts/rest-utils.php, define the two main REST utilities differentiated by the authentication method starting with the top-level structure:

<?php
   if (!defined('APS_DEVELOPMENT_MODE')) define ('APS_DEVELOPMENT_MODE', 'on');

   ## Basic user:password authentication
   function rest_request_basic($url, $verb, $login, $passwd,
      $agent = 'test@aps.test', $payload = '') {

   }

   ## Authentication via a user token
   function rest_request($url, $verb, $token, $agent, $payload = '') {

   }
?>

The input parameters are:

  • $url is the URL to address an HTTP request to.

  • $verb is one of the HTTP methods: GET, POST, PUT, or DELETE.

  • $login, $passwd, and token are the login name, password, and security token to be used for the authentication on the cloud application side. These parameters make the two utilities different.

  • $agent is a header required by GitHub API that typically specifies a way for getting in contact with you when necessary. This can be an email address.

  • $payload is a JSON object to be sent by POST or PUT methods.

Using Basic Authentication

Define the rest_request_basic utility:

  1. In accordance with the GitHub API and best practice examples published in PHP Client URL Library, prepare the HTTP headers and the authentication parameters using the respective input data:

    ## $logger = \APS\LoggerRegistry::get(); // Used for troubleshooting
    
    $headers = array(
      'Content-type: application/json',
      'User-Agent: '.$agent
    );
    $userPwd = $login.":".$passwd;
    
  2. Initialize the cURL connector structure and set the required parameters in it:

    $connector = curl_init();
    curl_setopt_array($connector, array(
       CURLOPT_URL => $url,
       CURLOPT_RETURNTRANSFER => true,
       CURLOPT_CUSTOMREQUEST => $verb,
       CURLOPT_HTTPHEADER => $headers,
       CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
       CURLOPT_USERPWD => $userPwd,
       ## CURLOPT_VERBOSE=>true, // Use it for troubleshooting
       CURLOPT_POSTFIELDS => json_encode($payload)
    ));
    
  3. Send the request:

    $response = curl_exec($connector);
    
  4. Collect the returned data and error data if any:

    $errors = curl_error($connector);
    $returnCode = (int)curl_getinfo($connector, CURLINFO_HTTP_CODE);
    
  5. Close the cURL connector:

    curl_close($connector);
    
    ## $logger->info("Return code: ".$returnCode."\n" ); // Logs to /var/log/httpd/error_log
    /*  Use it for troubleshooting only
        print('Error Dump: ');
        var_dump($errors);
        print('Response Dump: ');
        var_dump($response);
    */
    
  6. Convert the data from the JSON encoded string (UTF-8) into a PHP variable:

    if (!$errors) return json_decode($response);
    else return false;
    

Using Authentication through Token

Similarly, define the rest_request utility.

Preparing Repo Names

Add one more utility that will remove special symbols from the names to be assigned to repositories:

function makeRepoName($string) {
    //Make alphanumeric (removes all other characters)
    $string = preg_replace("/[^a-zA-Z0-9_\s-]/", "", $string);

    //Clean up multiple dashes or whitespaces
    $string = preg_replace("/[\s-]+/", " ", $string);

    //Convert whitespaces and underscores to underscore
    $string = preg_replace("/[\s_]/", "_", $string);

    return $string;
}

File Contents

Finally, the file contents will look similar to the rest-utils.php sample:

<?php
if (!defined('APS_DEVELOPMENT_MODE')) define ('APS_DEVELOPMENT_MODE', 'on');

## Basic user:password authentication
function rest_request_basic($url, $verb, $login, $passwd,
                $agent = 'test@aps.test', $payload = '') {
    ## $logger = \APS\LoggerRegistry::get(); // Used for troubleshooting

    $headers = array(
        'Content-type: application/json',
        'User-Agent: '.$agent
    );
    $userPwd = $login.":".$passwd;
    $connector = curl_init();
    curl_setopt_array($connector, array(
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $verb,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
        CURLOPT_USERPWD => $userPwd,
        ## CURLOPT_VERBOSE=>true, // Use it for troubleshooting
        CURLOPT_POSTFIELDS => json_encode($payload)
    ));
    $response = curl_exec($connector);
    $errors = curl_error($connector);
    $returnCode = (int)curl_getinfo($connector, CURLINFO_HTTP_CODE);
    curl_close($connector);

    ## $logger->info("Return code: ".$returnCode."\n" ); // Logs to /var/log/httpd/error_log
        /*  Use it for troubleshooting only
            print('Error Dump: ');
            var_dump($errors);
            print('Response Dump: ');
            var_dump($response);
        */

    if (!$errors) return json_decode($response);
    else return false;
}

## Authentication via a user token
function rest_request($url, $verb, $token, $agent, $payload = '') {
    $headers = array(
        'Content-type: application/json',
        'User-Agent: '.$agent,
        'Authorization: Token '.$token
    );
    $connector = curl_init();
    curl_setopt_array($connector,array(
        CURLOPT_URL =>$url,
        CURLOPT_RETURNTRANSFER=>true,
        CURLOPT_CUSTOMREQUEST=>$verb,
        CURLOPT_HTTPHEADER=>$headers,
        CURLOPT_FAILONERROR=>true,
        //CURLOPT_VERBOSE=>true,
        CURLOPT_POSTFIELDS=>json_encode($payload)
    ));

    $response = curl_exec($connector);
    $httpcode = curl_getinfo($connector, CURLINFO_HTTP_CODE);
    $errors = curl_error($connector);
    curl_close($connector);

    if (!$errors) {
        if ($verb == 'DELETE') return $httpcode;
        else return json_decode($response);
    }
    else return false;
}

## The function makes the repo name more robust by removing special characters
function makeRepoName($string) {
    //Make alphanumeric (removes all other characters)
    $string = preg_replace("/[^a-zA-Z0-9_\s-]/", "", $string);

    //Clean up multiple dashes or whitespaces
    $string = preg_replace("/[\s-]+/", " ", $string);

    //Convert whitespaces and underscores to underscore
    $string = preg_replace("/[\s_]/", "_", $string);

    return $string;
}

?>

Service Definition

The scripts/tenants.php script must contain the definition of the tenants service and tenant APS type to be used in APS PHP runtime. This service must provide the following functionality to comply with the Sales Model:

  • When a new tenant is provisioned (the provision method is called), the service must generate a new security token by sending a relevant REST request to the cloud application. You must save the token for further operations.

  • With the tenant token, the service must be able to create and remove repositories on the cloud application.

  • The service must be able to identify all repositories created for a particular tenant and return a list of them when requested.

  • Since the cloud application service we have chosen does not provide any ways to limit the total number of repositories (however, we need to simulate a limited service in our demo APS application), the APS application service must be able to count the total number of created repositories and prevent the attempts to exceed the limit obtained through the subscription.

We defined the relationship with other APS types earlier. Now, let us define properties and operations.

In this section:

Properties

Complete the following steps to implement the design requests outlined above:

  1. To use the REST utilities defined in the above section, add a requirement for the respective file:

    require "rest-utils.php";
    
  2. Before the main class definition, define an auxiliary class Counter to instantiate later the $licenseCounter property from it. We will use the latter to store the limit on the total number of repositories and their current usage.

    class LicCounter {
      /**
       * @type("integer")
       * @title("License Limit")
       * @description("The total number of repositories allowed by the license")
       */
      public $licLimit;
    
      /**
       * @type("integer")
       * @title("License Usage")
       * @description("The total number of created repositories")
       */
      public $licUsage;
    }
    
  3. Declare the following tenant properties:

    • $baseURL is a copy of the base URL defined in the app APS resource.

    • $token is a token generated by the cloud application and stored here for a particular tenant.

    • $tokenId is generated along with the token and will be used to remove the token when necessary.

    • $scopes is a set of scopes assigned to a token.

    • $repoNamePrefix is a prefix for repository names to be generated from the customer name. This will be used to identify all repositories created for a particular tenant.

    • $userAgent is any email address that the cloud application requires in every REST request.

    • $licenseCounter is a structure instantiated from the LicCounter class defined earlier.

    /**
     * @type(string)
     * @title("BaseURL")
     * @required
     * @description("Base URL of the cloud application endpoint")
     */
    public $baseURL;
    
    ## The user must have no access to token. None, but application, can use it.
    /**
     * @type(string)
     * @title("Token")
     * @required
     * @description("Security token for the tenant - generated by the cloud application")
     */
    public $token;
    
    /**
     * @type(integer)
     * @title("TokenID")
     * @required
     * @description("Security token ID for the tenant - generated by the cloud application")
     */
    public $tokenId;
    
    /**
     * @type(string)
     * @title("SecurityScopes")
     * @description("Access to the application objects - depends on the acquired license.")
     */
    public $scopes;
    
    /**
     * @type(string)
     * @title("RepoNamePrefix")
     * @required
     * @description("The prefix to be added to every repository created by the tenant")
     */
    public $repoNamePrefix;
    
    /**
     * @type(string)
     * @title("UserAgent")
     * @required
     * @description("Sent in the User-Agent HTTP header as required by some operations of the application API")
     */
    public $userAgent;
    
    ## This is an optional custom counter created for demo purposes only - to show the total of created repos in UI
    /**
     * @type(LicCounter)
     * @title("License Limit and Usage")
     * @description("The total of repositories allowed by the license")
     */
    public $licenseCounter;
    

Operations

The tenants service must be able to provision/unprovision tenants and manage repositories. Besides, to track limits in the subscription, every tenant must subscribe to the Resource Limit Change event and therefore must contain a handler to process that event.

Note

The provision and unprovision operations are standard and therefore do not require any special APS annotations before their definition.

Provision

When creating a new subscription, the platform must call the provision operation. To implement the designed behavior, define that operation as follows:

  1. Declare provision as a public function:

    public function provision() {
       // This space will be filled out by the further steps...
    
    }
    
  2. Determine the company (or personal) name that will be used as the prefix for all repositories of the tenant:

    $companyName = $this->account->companyName;
    
  3. From the subscription, get a list of resources purchased by the subscriber:

    ## Interact with the APS controller for getting additional data
    $apsc = \APS\Request::getController();
    
    ## Get data from the subscription:
    $subscriptionId = $this->subscription->aps->id;
    $resources = json_decode($apsc->getIo()->sendRequest(\APS\Proto::GET,
       "/aps/2/resources/".$subscriptionId."/resources"));
    
  4. Link the tenant with the license acquired by the subscription and initiate the license counter:

    foreach($resources as $resource) {
        if($resource->apsType == "http://aps-standard.org/samples/github/license/1.0" &&
              $resource->limit > 0) {
            # Link the tenant with the license:
            $logger->info("Request for link: ".$this."/license with ".$resource->apsId);
            $apsc->linkResource($this, "license", $resource->apsId);
    
            # Initialize the license counter:
            $this->licenseCounter = new stdClass();
            $this->licenseCounter->licLimit = $resource->limit;
            $this->licenseCounter->licUsage = 0;
            $logger->info("License Counter: ".print_r($this->licenseCounter, true));
            break;
        }
    }
    

    Key:

    • The above code determines if a resource is a license by the apsType property and then it selects the one whose limit property is higher than 0.

    • The service impersonates its owner through the tenant resource to request creation of a link from the tenant to its license.

    • Finally, it sets the counter limit equal the license limit.

  5. From the license, get SKU data - scopes:

    $this->scopes = $this->license->scopes;
    
  6. From the application instance, get the provider’s credentials:

    ## Get the provider's URL, login name, and password
    $this->baseURL = $this->app->baseURL;
    $login = $this->app->login;
    $passwd = $this->app->passwd;
    
  7. Prepare data for the HTTP request to create a token:

    ## Use the customer's admin email as the User Agent for the application API
    $this->userAgent = $this->account->adminContact->email;
    
    ## HTTP method and URL:
    $verb = 'POST';
    $url = $this->baseURL . 'authorizations';
    
    ## Payload for the request
    $payload = array(
       'scopes' => $this->scopes,
       'note' => $companyName
    );
    
  8. Send the HTTP request to create a security token that will represent the tenant on the application side:

    ## Send HTTP request to a user token for the tenant
    $response = rest_request_basic($url, $verb, $login, $passwd, $this->userAgent, $payload);
    if($response) {
       $this->token = $response->token;
       $this->tokenId = $response->id;
    }
    
  9. Prepare the prefix for repository names:

    ## Repo names to be created later must begin with the following prefix
    $this->repoNamePrefix = makeRepoName($companyName);
    

    The provisioning logic will use this prefix to distinguish repositories created by different tenants.

Read Repositories

With the following custom operation getRepos, the service will be able to provide a list of repositories created by a particular tenant:

  1. Declare the custom operation:

    /**
     * @verb(GET)
     * @path("/repos")
     * @return(object)
     */
    public function getRepos() {
    
    }
    
  2. Prepare parameters for the HTTP request - verb, URL, and token:

    $verb = 'GET';
    $url = $this->baseURL.'user/repos';
    $token = $this->token;
    
  3. Send the HTTP request for all repositories:

    $response = rest_request($url, $verb, $token, $this->userAgent);
    
  4. From the response, select only the repositories created by this tenant (using the tenant’s repoNamePrefix property):

    $returnData = [];
    
    if($response) {
        foreach ($response as $repo) {
            if($repo->name )
                $name = $repo->name;
            if(strpos($name, $this->repoNamePrefix) === 0) {
                array_push($returnData,
                    array(
                        "id" => $repo->id,
                        "name" => $repo->name,
                        "url" => $repo->html_url
                 ));
            }
        }
    }
    
    return $returnData;
    

Refresh License Data

For the customer’s convenience, define the custom operation called on by pressing the Refresh License button in the custom UI by doing the following:

  1. Declare the custom operation:

    /**
     * @verb(GET)
     * @path("/refreshdata")
     * @return(object)
     */
    public function refreshData() {
    
    }
    
  2. Use the previously defined getRepos method to get all tenant’s repositories and count their total:

    $this->licenseCounter->licUsage = count($this->getRepos());
    
  3. Update the license counter in the APS database:

    $apsc = \APS\Request::getController();
    $apsc->updateResource($this);
    

Create Repository

With the custom operation createRepo, the service will be able to create a repository using the tenant’s token. The tenant’s unique prefix must prepend a repository name. Follow these steps to define the new operation:

  1. Declare the custom operation:

    /**
     * @verb(POST)
     * @path("/repos")
     * @param(string,body)
     * @return(object)
     */
    public function createRepo($name) {
    
    }
    

    The $name argument must contain a suffix of the repository name in the format {"name_suffix":<value>}. The custom method must prepend the tenant unique prefix to a random string to have a full repository name.

    All of the subsequent steps define the function internals.

  2. Verify whether the total of the tenant’s repositories does not exceed the limit licensed for the tenant:

    $this->licenseCounter->licUsage = count($this->getRepos());
    if($this->licenseCounter->licUsage >= $this->licenseCounter->licLimit) return;
    
  3. Compute the full repository name:

    $repoName = $this->repoNamePrefix.'_'.makeRepoName($name->name_suffix);
    
  4. Prepare parameters for the HTTP request:

    $verb = 'POST';
    $url = $this->baseURL.'user/repos';
    $token = $this->token;
    $payload = array('name' => $repoName);
    
  5. Send the request:

    $response = rest_request($url, $verb, $token, $this->userAgent, $payload);
    
  6. Regardless of the result, update the license counter:

    $this->licenseCounter->licUsage = count($this->getRepos());
    $apsc = \APS\Request::getController();
    $apsc->updateResource($this);
    
    return $response;
    

Note

It seems like there is no need to return the response here. If you want to omit the last string in the above code, do not forget to update respectively the operation declaration.

Remove Repository

With the custom operation removeRepo, the service will be able to remove a repository using the tenant’s token. Follow these steps to define the new operation:

  1. Declare the custom operation:

    ## Custom method to remove a Repository on the App side
    /**
    * @verb(DELETE)
    * @path("/repos/{name}")
    * @param(string,path)
    */
    public function removeRepo($name) {
    
    }
    

    The $name argument must be the full name of the repository to be removed.

    All the subsequent steps define the function internals.

  2. Prepare parameters for the HTTP request:

    $verb = 'DELETE';
    $url = $this->baseURL.'repos/'.$this->app->login.'/'.$name;
    $token = $this->token;
    $agent = $this->userAgent;
    
  3. Send the request:

    $response = rest_request($url, $verb, $token, $agent);
    
  4. Regardless of the result, update the license counter:

    $this->licenseCounter->licUsage = count($this->getRepos());
    $apsc = \APS\Request::getController();
    $apsc->updateResource($this);
    

Define Event Handler

In this application, the event handler onLimitChange will be used to process changes of the license limit in the subscription as a result of the license upsell. In the tenant APS type definition to be defined later, we will subscribe every tenant to the resource limit change event and declare the onLimitChange custom operation as the event handler. The platform will then call this handler every time the respective subscription is changed:

  1. Declare the handler:

    /**
     * @verb(POST)
     * @path("/onLimitChange")
     * @param("http://aps-standard.org/types/core/resource/1.0#Notification",body)
     */
    public function onLimitChange($notification) {
    
    }
    

    All the other steps define the function internals.

  2. Interact with the APS controller to get resources from the subscription:

    $apsc = \APS\Request::getController();
    
    ## Get data from the subscription:
    $subscriptionId = $this->subscription->aps->id;
    $resources = json_decode($apsc->getIo()->sendRequest(\APS\Proto::GET,
          /aps/2/resources/".$subscriptionId."/resources"));
    
  3. Find the respective license and update the license counter limit:

    foreach($resources as $resource) {
        if($this->license->aps->type == $resource->apsType) {
            $this->licenseCounter->licLimit = $resource->limit;
            break;
        }
    }
    $apsc->updateResource($this);
    

Unprovision

On tenant removal, the service must remove all its repositories and then remove the token. Define the upprovision function to meet those requirements:

  1. Declare unprovision as a public function:

    public function unprovision() {
       // This space will be filled out by the further steps...
    
    }
    
  2. Get all repositories:

    $url = $this->baseURL.'user/repos';
    $token = $this->token;
    
    $repos = rest_request($url, 'GET', $token, $this->userAgent);
    
  3. Find all repositories named with the prefix used by this tenant and remove those repositories:

    foreach ($repos as $repo) {
        $name = $repo->name;
        if(strpos($name, $this->repoNamePrefix) === 0) {
            $this->removeRepo($name);
        }
    }
    
  4. Remove the token using the basic authentication:

    $login = $this->app->login;
    $passwd = $this->app->passwd;
    $url = $this->baseURL.'authorizations/'.$this->tokenId;
    rest_request_basic($url, 'DELETE', $login, $passwd);
    

APS Type Definition

Unlike for other services, for the tenants service we will define the tenant APS type before building an APS package. This is caused by the decision to subscribe tenants to the event type mentioned earlier using the simplest declarative way. This also corresponds to the service declaration in the APP-meta.xml file.

To keep the APS type in sync with the tenants.php script, generate this APS type from that PHP code and then add the required event subscription as follows:

  1. Generate the tenant.schema file inside the schemas/ folder using the scripts/tenants.php contents:

    $ mkdir schemas
    $ php scripts/tenants.php '$schema' > schemas/tenant.schema
    
  2. Ensure the new tenant.schema file is created in the schemas/ folder and then update the onLimitChange operation declaration so that it looks as follows:

    "onLimitChange": {
       "verb": "POST",
       "path": "/onLimitChange",
       "eventSubscription": {
           "event" : "http://parallels.com/aps/events/pa/subscription/limits/changed",
           "source": {
               "type" : "http://parallels.com/aps/types/pa/subscription/1.0"
           }
       },
       "parameters": {
           "notification": {
               "type": "http://aps-standard.org/types/core/resource/1.0#Notification",
               "required": true,
               "kind": "body"
           }
       }
    }
    

The above declaration makes all APS resources instantiated from the tenant APS type be subscribed to the Resource Limit Change event. Also, it declares the onLimitChange custom operation as the event handler.