The main provisioning logic is concentrated in the tenants
service and its tenant
APS type.
Implement the required components in the following files:
scripts/rest-utils.php
defines reusable utilities to operate REST 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.
In this document:
In this section:
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 a REST 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.
Define the rest_request_basic
utility:
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;
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)
));
Send the request:
$response = curl_exec($connector);
Collect the returned data and error data if any:
$errors = curl_error($connector);
$returnCode = (int)curl_getinfo($connector, CURLINFO_HTTP_CODE);
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);
*/
Convert the data from the JSON encoded string (UTF-8) into a PHP variable:
if (!$errors) return json_decode($response);
else return false;
Similarly, define the rest_request
utility.
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;
}
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;
}
?>
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:
Complete the following steps to implement the design requests outlined above:
To use the REST utilities defined in the above section, add a requirement for the respective file:
require "rest-utils.php";
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 Counter {
/**
* @type("integer")
* @title("License Limit")
* @description("The total number of repositories allowed by the license")
*/
public $limit;
/**
* @type("integer")
* @title("License Usage")
* @description("The total number of created repositories")
*/
public $usage;
}
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 Counter
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(Counter)
* @title("License Limit and Usage")
* @description("The total of repositories allowed by the license")
*/
public $licenseCounter;
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.
When creating a new subscription, the platform must call the provision
operation. To implement the designed behavior,
define that operation as follows:
Declare provision
as a public function:
public function provision() {
// This space will be filled out by the further steps...
}
Determine the company (or personal) name that will be used as the prefix for all repositories of the tenant:
$companyName = $this->account->companyName;
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"));
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:
//$licenseApsResource = $apsc->getResource($resource->apsId);
$apsc2 = $apsc->impersonate($this);
$apsc2->linkResource($this, "license", $resource->apsId);
# Initialize the license counter:
$this->licenseCounter = (object)array("limit"=>$resource->limit, "usage"=>0);
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.
From the license, get SKU data - scopes:
$this->scopes = $this->license->scopes;
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;
Prepare data for the REST 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
);
Send the REST request to create a security token that will represent the tenant on the application side:
## Send REST 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;
}
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.
With the following custom operation getRepos
, the service will be able to provide a list of repositories created by
a particular tenant:
Declare the custom operation:
/**
* @verb(GET)
* @path("/repos")
* @return(object)
*/
public function getRepos() {
}
Prepare parameters for the REST request - verb, URL, and token:
$verb = 'GET';
$url = $this->baseURL.'user/repos';
$token = $this->token;
Send the REST request for all repositories:
$response = rest_request($url, $verb, $token, $this->userAgent);
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;
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:
Declare the custom operation:
/**
* @verb(GET)
* @path("/refreshdata")
* @return(object)
*/
public function refreshData() {
}
Use the previously defined getRepos
method to get all tenant’s repositories and count their total:
$this->licenseCounter->usage = count($this->getRepos());
Update the license counter in the APS database:
$apsc = \APS\Request::getController();
$apsc->updateResource($this);
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:
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.
Verify whether the total of the tenant’s repositories does not exceed the limit licensed for the tenant:
$this->licenseCounter->usage = count($this->getRepos());
if($this->licenseCounter->usage >= $this->licenseCounter->limit) return;
Compute the full repository name:
$repoName = $this->repoNamePrefix.'_'.makeRepoName($name->name_suffix);
Prepare parameters for the REST request:
$verb = 'POST';
$url = $this->baseURL.'user/repos';
$token = $this->token;
$payload = array('name' => $repoName);
Send the request:
$response = rest_request($url, $verb, $token, $this->userAgent, $payload);
Regardless of the result, update the license counter:
$this->licenseCounter->usage = 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.
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:
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.
Prepare parameters for the REST request:
$verb = 'DELETE';
$url = $this->baseURL.'repos/'.$this->app->login.'/'.$name;
$token = $this->token;
$agent = $this->userAgent;
Send the request:
$response = rest_request($url, $verb, $token, $agent);
Regardless of the result, update the license counter:
$this->licenseCounter->usage = count($this->getRepos());
$apsc = \APS\Request::getController();
$apsc->updateResource($this);
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:
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.
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"));
Find the respective license and update the license counter limit:
foreach($resources as $resource) {
if($this->license->aps->type == $resource->apsType) {
$this->licenseCounter->limit = $resource->limit;
break;
}
}
$apsc->updateResource($this);
On tenant removal, the service must remove all its repositories and then remove the token. Define the upprovision
function to meet those requirements:
Declare unprovision
as a public function:
public function unprovision() {
// This space will be filled out by the further steps...
}
Get all repositories:
$url = $this->baseURL.'user/repos';
$token = $this->token;
$repos = rest_request($url, 'GET', $token, $this->userAgent);
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);
}
}
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);
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:
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
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.