This is documentation for the current major version Apprenda 7.
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.

MongoDB Add-On sample

Prerequisites

In order to complete this tutorial, the following conditions must be met:

  • The Apprenda SDK matching the version of the Apprenda Platform you are running must be installed on the machine you are using.
  • You will also need a running and configured off-Platform MongoDB instance to follow this tutorial.  If you already have one set up, you may need to substitute your existing configuration values in place of what values are used here.  If you do not already have an active MongoDB instance in place, the following steps can be used to quickly 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.  Be sure to 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
     

  4. Open a second command prompt and navigate to the MongoDB folder.
  5. Start the MongoDB console:

    .\mongo.exe --port 27277
     

  6. Create the initial admin user.  This is the user that will be used by the Add-On created here 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.)

    use admin
    db.addUser("ApprendaAdmin", "password")
     

  7. Exit the console.

Creating an Apprenda Platform Add-On

Apprenda Add-On Archive Structure

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 the developer 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 simply a class that inherits from the AddonBase class that is defined in SaaSGrid.API.dll, which 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

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; }
}

To create a new Add-On, open Visual Studio and create a new C# class library project.  Add a reference to SaaSGrid.API.dll, and then create a new class and inherit from Apprenda.SaaSGrid.Addons.AddonBase.  You will need to implement the following methods:

Test

This is the method that is invoked when the Apprenda 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; therefore, 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 AddOns. first, the Manifest property contains all the configuration data for this Add-On (for more information about the Add-On Manifest, see below).  Second, the DeveloperOptions property is an arbitrary string that provides a means of passing in additional information during the provisioning process.  If your Add-On requires additional information, be sure to include appropriate instructions in your Add-On Manifest so that the Tenant knows what information to provide.

The Test method returns an OperationResult object, which is 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.  It should be noted that any unhandled exceptions within this or any Add-On method will result in a failure.  The full exception will be logged in the SOC Event Log, but the Tenant 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 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 is the method that is invoked when a Development Team provisions a new instance of your Add-On (for more information on provisioning Add-On instances, please see here).  The implementation of this method will vary drastically 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 that similarly to the AddonTestRequest objects contains two properties: The Manifest property, and the DeveloperOptions property where any additional information that is needed from the Tenant will be passed in, and 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 should be 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; that is, 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 are able to 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, the ConnectionData and the DeveloperOptions is also included as properties of the request object in case they are needed for deprovisioning of the addon however 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 archive that defines metadata as well as configuration data for the Add-On.

Add-On Manifest Schema Location

Only the following fields are required, but most Add-Ons will need some or all of the remaining fields.  It should be noted that the Apprenda Platform Operator can edit all of the optional fields in the SOC after the Add-On has been uploaded.  Required fields:

  • Name
  • Version
  • Vendor
  • Author

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, and these values can optionally be encrypted.  

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 provisionined an instance of the MongoDB Add-On, this property would be Dev Team A's  ID.
  • CallingDeveloperAlias:  the alias of the Development Team who invoked the Add-On.
  • InstanceAlias:  the 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 can be 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.  Packaging is quite simple; you just need to put 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.