This is documentation for Apprenda 7 and 8.
Documentation for older versions are also available.

Platform Add-On Creation Tutorial

This tutorial will walk through the process of creating a sample Apprenda Platform Add-On that provisions databases on an existing off-Platform MongoDB instance.

Download MongoDB Add-On Sample

MongoDB Add-On sample

Prerequisites

In order to follow this tutorial, you must have the following:

  • Apprenda SDK for the Platform version you are running installed on the machine you are using. Download the Apprenda SDK
  • running and configured off-Platform MongoDB instance. If you already have one set up, you may need to substitute your existing configuration values with values are used here.

To set up MongoDB to work with this tutorial,

  1. download the latest version of MongoDB and extract it to a folder
  2. open a command prompt and navigate to the folder where you extracted the MongoDB binaries
  3. start MongoDB. Use the command below and specify the authentication option, the path to the folder where it should store data files, and the port you wish to use:
.\mongod.exe --auth --dbpath D:\MongoDB-DATA --port 27277
  1. open a second command prompt and navigate to the MongoDB folder
  2. start the MongoDB console:
.\mongo.exe --port 27277
  1. create the initial admin user. This is the user the Add-on will use to provision new databases. When MongoDB is run in "auth" mode, nothing can happen until this first admin user is created. (NOTE: MongoDB passwords cannot contain the '@' character, since that character is used as a separator character in the connection string.)
db.createUser(
  {
    user: "ApprendaAdmin",
    pwd: "password",
    roles: [ { role: "root", db: "admin" } ]
  })

Apprenda Add-On Archive Structure

To create an add-on on the Platform, you must have an Add-On Archive that contains the files of the Add-On. At a minimum, an Apprenda Add-On is a ZIP archive that contains the following files: 

  • the SaaSGrid.API.dll (available in the Apprenda SDK)
  • a DLL created by you containing an Add-On implementation
  • an AddonManifest.xml file that configures and defines properties of the Add-On
  • an icon that represents the Add-On

An Add-On implementation is a class that inherits from the AddonBase class defined in SaaSGrid.API.dll. This is included in the Apprenda SDK. The Add-On Manifest and the icon must be in the root of the archive, but there are no other restrictions on the folder structure.

Creating the Add-On

To create a new Add-On,

  1. open Visual Studio and create a new C# class library project
  2. add a reference to SaaSGrid.API.dll
  3. create a new class and inherit from Apprenda.SaaSGrid.Addons.AddonBase

There are several supporting objects in the AddonBase class that you can use to create your Add-On. You must implement the Test, Provision, and Deprovision methods of the AddonBase class in order for your Add-On to work.

First, let's examine the AddonBase class and its supporting objects:

AddonBase

public abstract class AddonBase
{
	public const string AddOnBaseClassFullName = "Apprenda.SaaSGrid.Addons.AddonBase";

	/// <summary>
	/// Provisions an instance of this add-on.
	/// </summary>
	/// <param name="request">A request object encapsulating all the parameters available to provision an add-on.</param>
	/// <returns>A <see cref="ProvisionAddOnResult"/> object containing the results of the operation as well as the data needed to connect to / use the newly provisioned instance.</returns>
	public abstract ProvisionAddOnResult Provision(AddonProvisionRequest request);
	
	/// <summary>
	/// De-provisions an instance of this add-on.
	/// </summary>
	/// <param name="request">A request object encapsulating all the parameters available to deprovision an existing add-on instance.</param>
	/// <returns>An <see cref="OperationResult"/> object containing the results of the operation.</returns>
	public abstract OperationResult Deprovision(AddonDeprovisionRequest request);
	
	/// <summary>
	/// Tests the add-on.
	/// </summary>
	/// <param name="request">A request object encapsulating all the parameters available to test the Addon.</param>
	/// <returns>An <see cref="OperationResult"/> object containing the results of the test.</returns>
	public abstract OperationResult Test(AddonTestRequest request);	
}

OperationResult

public class OperationResult
{
    /// <summary>
    /// Gets or sets a value indicating whether or not the operation was successful.
    /// </summary>
    public bool IsSuccess { get; set; }

    /// <summary>
    /// Gets or sets a message to display to the user after the operation is complete.  This can be a success or failure message.
    /// </summary>
    public string EndUserMessage { get; set; }
}

ProvisionAddOnResult

public class ProvisionAddOnResult : OperationResult
{
    /// <summary>
    /// Gets the connection data returned by the provision add-on operation.
    /// </summary>
    public string ConnectionData { get; set; }
}

AddonProvisionRequest

 public class AddonProvisionRequest
{
    /// <summary>
    /// The manifest for the add-on to provision.
    /// </summary>
    public AddonManifest Manifest { get; set; }
 
    /// <summary>
    /// Optional parameters given by the developer who requested the provisioned instance.
    /// </summary>
    public string DeveloperOptions { get; set; }
}

AddonDeprovisionRequest

 public class AddonDeprovisionRequest
{
    /// >summary>
    /// The manifest for the add-on to deprovision.
    /// >/summary>
    public AddonManifest Manifest { get; set; }
 
    /// >summary>
    /// The optional parameters given by the developer who requested the provisioned instance. 
    /// This will only be available if they were used during a provisioning request, otherwise
    /// this property will be null.
    /// >/summary>
    public string DeveloperOptions { get; set; }
 
    /// >summary>
    /// A string indicating how to connect to / use the instance of the add-on. This is the value 
    /// that was returned from the original call to Provision.
    /// >/summary>
    public string ConnectionData { get; set; }
}

​AddonTestRequest

 public class AddonTestRequest
{
    /// >summary>
    /// The manifest for the add-on to provision.
    /// >/summary>
    public AddonManifest Manifest { get; set; }
 
    /// >summary>
    /// Optional parameters given by the developer who requested the provisioned instance.
    /// >/summary>
    public string DeveloperOptions { get; set; }
}

Test

This is the method that is invoked when the Platform Operator chooses the "Test" menu option for an Add-On from the System Operations Center (SOC). The Platform Operator would use the "Test" option to make sure that the Add-On is configured properly before enabling it and making it available to Development Teams. The implementation of the Test method should be robust enough to exercise most or all of the Add-On's features or at a minimum the necessary validation so that when a provision and deprovision requests are made they will succeed as expected or return useful errors that developers can correct and try again.  

The AddonTestRequest object contains two properties that can be useful for testing Add-Ons.

  • Manifest contains all the configuration data for this Add-On. See more about the Add-On Manifest
  • DeveloperOptions is an arbitrary string that provides a means of passing in additional information during the provisioning process. If your Add-On requires additional information, include appropriate instructions in your Add-On Manifest so that the Tenant knows what information to provide

The Test method returns an OperationResult object (shown above). This object provides a way for your Add-On to pass feedback, positive or negative, to the Tenant, as well as a global success flag. When the operation is successful, the Tenant message will be shown as an info message in the report card returned upon the operation's completion.  When the operation fails, an error message will be returned instead. 

Any unhandled exceptions in the Test or any Add-On method will result in a failure. The full exception is logged in the SOC Event Log for Platform Operators to troubleshoot. Tenants, however, will only see "An unknown error has occurred" text so it is important to handle all known possible outcomes so that useful information is always displayed to the Tenant.

Below is the Test method for the MongoDB Add-On (download the complete source sample to see the full implementation):

public override OperationResult Test(AddonTestRequest request)
{
    AddonManifest manifest = request.Manifest;
    var results = new OperationResult();

    try
    {
        var server = GetServer(manifest);

        if (server == null)
        {
            results.EndUserMessage = "Unable to connect to the server using the information from the add-on manifest.";
            return results;
        }

        // Create a DB and add a collection to make sure the MongoDB instance is configured correctly.
        var database = server.GetDatabase("test");
        var collection = database.GetCollection<TestObject>("testObjects");
        var insertResult = collection.Insert(new TestObject { Value = "test" });
        database.Drop();

        if (insertResult.Ok)
        {
            results.IsSuccess = true;
        }
        else
        {
            results.EndUserMessage = insertResult.ErrorMessage;
        }
    }
    catch (MongoException mongoException)
    {
        results.EndUserMessage = mongoException.Message;
    }

    return results;
}

Provision

This method is invoked when a Development Team provisions a new instance of your Add-On. The implementation of this method will vary with each Add-On that is created, but the structure is roughly the same as the Test method. The Add-On configuration is passed in a AddonProvisionRequest object (shown above)that contains two properties whereany additional information that is needed from the Tenant will be passed.

  • Manifest
  • DeveloperOptions

The result of the operation is returned in a ProvisionAddOnResult object. This object, shown above, inherits from OperationResult. The only addition is the ConnectionData property, which you should set to a string that the Development Team will use to connect to and/or communicate with the Add-On instance that was just provisioned. In the MongoDB Add-On example, this is the connection string to the database that was created.  

The MongoDB Add-On enforces security at the database instance level. Every database that is provisioned has a user created specifically for that database. The user credentials are passed in using the DeveloperOptions property as shown below:

AddonManifest manifest = request.Manifest;
string developerOptions = request.DeveloperOptions;
 
string username = null;
string password = null;
var result = new ProvisionAddOnResult();

foreach (var pair in developerOptions.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
{
    var innerPair = pair.Split("=".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);

    if (innerPair.Length < 2)
    {
        continue;
    }

    switch (innerPair[0])
    {
        case "username":
            username = innerPair[1];
            break;
        case "password":
            password = innerPair[1];
            break;
    }
}

if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
    result.EndUserMessage = "The developerOptions parameter must contain the username and password to assign to this developer's MongoDB instance. The correct format is: username=<username>,password=<password>";
    return result;
}

Once you have parsed the credentials, you can create the database and add the user to it. The user will only have access to this one database, so it is not possible for consumers of this Add-On instance to maliciously alter or delete data in any other databases created by the Add-On.

Next, you can insert some data into the database to force MongoDB to actually create it. Finally, set the ConnectionData property to the connection string to this instance and return the result. Just like in the Test method, any error messages that you wish to communicate back to the Tenant are passed back in the EndUserMessage property.

try
{
    var server = GetServer(manifest);
    var databaseName = GetDatabaseName(manifest);

    if (server.DatabaseExists(databaseName))
    {
        result.EndUserMessage = string.Format("A MongoDB instance with the name '{0}' already exists. Use a different instance alias.", databaseName);
        return result;
    }

    var database = server.GetDatabase(databaseName);
    database.AddUser(new MongoCredentials(username, password));

    // MongoDB does not actually create the DB until you put data into it.
    // So we store the time the DB was created to force creation.
    var collection = database.GetCollection<ProvisioningData>("__provisioningData");
    var insertResult = collection.Insert(new ProvisioningData());

    if (insertResult.Ok)
    {
        // Set the connection string that the app will use.
        // This connection string includes the username and password given for this instance.
        result.ConnectionData = string.Format("mongodb://{0}:{1}@{2}/{3}", username, password, manifest.ProvisioningLocation, databaseName);
        result.IsSuccess = true;
    }
    else
    {
        result.EndUserMessage = string.Format("There was an error provisioning the database:{0}{1}", Environment.NewLine, insertResult.ErrorMessage);
    }
}
catch (MongoException mongoException)
{
    result.EndUserMessage = mongoException.Message;
}

return result;

Deprovision

​This is the simplest of the three methods, since it only needs to tear down a given instance of the Add-On. Like the other two methods, the configuration data is passed in using the Manifest property of the AddonDeprovisionRequest (shown above), the ConnectionData and the DeveloperOptions is also included as properties of the request object in case they are needed for deprovisioning of the Add-On. In this example that piece of data is not needed. Just like the other methods, the result of the operation is returned in an OperationResult object.

public override OperationResult Deprovision(AddonDeprovisionRequest request)
{
    AddonManifest manifest = request.Manifest	
    var result = new OperationResult();

    try
    {
        var server = GetServer(manifest);
        var database = server.GetDatabase(GetDatabaseName(manifest));
        database.Drop();
        result.IsSuccess = true;
    }
    catch (MongoException mongoException)
    {
        result.EndUserMessage = mongoException.Message;
    }

    return result;
}

The Add-On Manifest

Every Add-On must include a Manifest in the root of the Add-On archive that defines metadata as well as configuration data for the Add-On.

Add-On Manifest Schema Location

Required fields

  • Name
  • Version
  • Vendor
  • Author

Only the above fields are required in the Manifest,  but most Add-Ons will need some or all of the remaining fields. The Apprenda Platform Operator can edit all of the optional fields in the SOC after the Add-On has been uploaded.

If you do not wish to include the connection information for your Add-On in the Manifest, it can be omitted, but the Platform Operator will need to fill it in after the Add-On is uploaded to the Apprenda Platform. The Add-On Manifest also defines an arbitrary property bag that you can use to store any other pieces of information you need for your Add-On. You can choose to encrypt these values.

Below is a sample Add-On Manifest. The Deployment Notes info is visible to the Platform Operator and should assist in configuring the Add-On. The Developer Help section is visible to Development Teams, and should include any information they need to provision instances of the Add-On.

Please note: utf-8 encoding is required for all persistence scripts, application and add-on manifests, CLI installer input files, and Apprenda mock files.

Sample Add-On Manifest

<?xml version="1.0" encoding="utf-8" ?>
<addonManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns="http://schemas.apprenda.com/AddonManifest"
               xsi:schemaLocation="http://schemas.apprenda.com/AddonManifest http://apprenda.com/schemas/platform/7.0/AddonManifest.xsd"
               name="MongoDB"
               manifestVersion="1"
               version="2.2.3"
               description="Provides MongoDB databases scoped to a development team."
               deploymentNotes="This add-on assumes that you already have a MongoDB instace running, configured with authentication, and one admin user.  The admin username and password are needed to create client databases.  The location field should be the host and port of the MongoDB instance.  ex) hostname:12345"
               developerHelp="When deploying a new MongoDB database you need to specify the credentials you want to use in your app to connect to this database.  These credentials should be passed in using the developerOptions argument in the following format: username=yourusername,password=yourpassword"
               vendor="Apprenda"
               author="A Developer"
               provisioningUsername="ApprendaAdmin"
               provisioningPassword="password"
               provisioningLocation="apprendamongo1:27277">
  <properties>
    <property key="exampleProperty" value="true"/>
    <property key="exampleEncryptedProperty" value="sensitiveValue" isEncrypted="true"/>
  </properties>
</addonManifest>

The current Add-On configuration, meaning the values provided in the Manifest plus any changes made by the Platform Operator, is passed into all Add-On methods in the AddonManifest object. The most useful pieces of information are the connection properties and any values in the property bag that are needed for your Add-On. In addition to the properties defined in the schema, there are three more properties included in the AddonManifest object that are computed at runtime before calling the Add-On methods:

  • CallingDeveloperId: ​ the ID of the Development Team (Tenant) who invoked the Add-On.  For example, if Dev Team A provisioned an instance of the MongoDB Add-On, this property would be Dev Team A's  ID.
  • CallingDeveloperAliasthe alias of the Development Team who invoked the Add-On.
  • InstanceAliasthe instance alias given to the Add-On instance that is being operated on.

An Add-On manifest may contain a parameters tag that can be used to specify parameters that can (or must) be filled in when a Development Team provisions in instance. This tag is available when manifestVersion="2" is specified in the addonManifest tag:

Sample Add-On Manifest Using Parameters: manifestVersion="2"

<?xml version="1.0" encoding="utf-8" ?>
<addonManifest xmlns:xsd="http://www.w3.org/2001/XMLSchema"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xmlns="http://schemas.apprenda.com/AddonManifest"
               xsi:schemaLocation="http://schemas.apprenda.com/AddonManifest http://apprenda.com/schemas/platform/7.0/AddonManifest.xsd"
               name="Amazon S3"
               description="Amazon S3 is storage for the Internet. It is designed to make web-scale computing easier for developers."
               deploymentNotes="Updated to reflect changes in Amazon SDK, configured to work on .NET 4.5 runtime"
               developerHelp="contact cdutra@apprenda.com for assistance."
               vendor="Apprenda"
               author="A Developer"
               version="1.0.0"
               manifestVersion="2"
               provisioningUsername=""
               provisioningPassword=""
               provisioningLocation="">
  <properties>
    <property key="AWSClientKey" displayName="AWS Client Key" description="Required - please specify the client key of the user who has access to provision S3 Buckets" value=""/>
    <property key="AWSSecretKey" displayName="AWS Secret Key" description="Required - please specify the secret key of the user who has access to provision S3 Buckets" value=""/>
    <property key="requireDevCredentials" displayName="Require Developer Credentials" description="If set to true, each developer will need to provide their own Amazon credentials, otherwise the default creds will be used to provision S3 Buckets." value="true"  />
    <property key="BucketName" displayName="S3 Bucket Name" description="Default name of the bucket. Should be overridden during provisioning." value="" />
    <property key="BucketRegionName" displayName="S3 Bucket Region Name" description="The bucket region locality expressed using the name of the region. When set, this will determine where your data will reside in S3. Valid values: us-east-1, us-west-1, us-west-2, eu-west-1, ap-southeast-1, ap-southeast-2, ap-northeast-1, sa-east-1" value="us-east-1"/>
    <property key="CannedACL" displayName="Canned ACL" description="The canned ACL to apply to the bucket. Should be set my Platform Opertator as a default" value=""/>
    <property key="Grants" displayName="Grants" description="Gets additional access control lists for requests to this S3 bucket. See documentation for details" value=""/>
    <property key="UseClientRegion" displayName="Use Client Region"  description="This indicates whether to use the same region as the S3 client was established." value=""/>
    <property key="RegionEndpoint" displayName="S3 Client Region Endpoint" description="Used during setup of the S3 client for provisioning/deprovisioning" value=""/>
  </properties>
  <parameters allowUserDefinedParameters="true">
    <parameter key="RequiredParameter" displayName="Required Parameter" isRequired="true" isEncrypted="false" description="A parameter that is required" />
    <parameter key="EncryptedParameter" displayName="Encrypted Parameter" isRequired="false" isEncrypted="true" description="A parameter that is encrypted when stored in the DeployedAddOnParameters table" />
    <parameter key="OptionalParameter" displayName="Optional Parameter" isRequired="false" isEncrypted="false" description="A parameter that is optional" />
    <parameter key="DefaultValueParameter" displayName="Parameter w/ Default Value" isRequired="false" isEncrypted="false" defaultValue="default" description="A parameter that has a default value" />
  </parameters>
</addonManifest>

As needed, each parameter can be marked as required or encrypted. A defaultValue can also be set.

The parameters tag has an attribute, allowUserDefinedParameters, that you can set to "true" to allow Dev Teams to specify additional parameters/values when provisioning an instance.

Packaging an Add-On

Once your Add-On is written and built, it needs to be packaged. To Package, you place the AddonManifest.xml, icon.png, and the assembly(s) that contain your Add-On into a ZIP file.  It is now ready to be uploaded to the Apprenda Platform.

Next Steps

Please see this documentation for information on how Development Teams can provision and manage Add-On instances, as well as how guest applications can consume individual Add-On instances.