cloudsoft.io

Defining and Deploying

Intro

This walkthrough will set up a simple entity, add it to the catalog, and provision it.

For illustration purposes, we will write an integration with Github Gist, with an effector to create new gists.

Project Setup

Follow the instructions to create a new Java project using the archetype, and import it into your IDE. This example assumes you used the groupId com.acme and artifact id autobrick.

First ensure you can build this project at the command line, using mvn clean install.

Java Entity Classes

For this particular example, we will use a third party Gist library, so will need to add that as a dependency. Add the following to your pom.xml inside the <dependencies> section (see Maven for more details):

<dependency>
  <groupId>org.eclipse.mylyn.github</groupId>
  <artifactId>org.eclipse.egit.github.core</artifactId>
  <version>2.1.5</version>
</dependency>

Create a new Java interface, GistGenerator, to describe the entity’s interface (i.e. the configuration options, sensors, and effectors). The code below assumes you have created this in the package com.acme for src/main/java.

package com.acme;

import java.io.IOException;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.ImplementedBy;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.annotation.Effector;
import org.apache.brooklyn.core.annotation.EffectorParam;
import org.apache.brooklyn.core.config.ConfigKeys;

@ImplementedBy(GistGeneratorImpl.class)
public interface GistGenerator extends Entity {

    ConfigKey<String> OAUTH_KEY = ConfigKeys.newStringConfigKey("oauth.key", "OAuth key for creating a gist",
            "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

    @Effector(description="Create a Gist")
    String createGist(
            @EffectorParam(name="gistName", description="Gist Name", defaultValue="Demo Gist") String gistName,
            @EffectorParam(name="fileName", description="File Name", defaultValue="Hello.java") String fileName,
            @EffectorParam(name="gistContents", description="Gist Contents", defaultValue="System.out.println(\"Hello World\");") String gistContents,
            @EffectorParam(name="oauth.key", description="OAuth key for creating a gist", defaultValue="") String oauthKey) throws IOException;

    @Effector(description="Retrieve a Gist")
    public String getGist(
            @EffectorParam(name="id", description="Gist id") String id,
            @EffectorParam(name="oauth.key", description="OAuth key for creating a gist", defaultValue="") String oauthKey) throws IOException;
}

To describe each part of this:

  • The @ImplementedBy indicates the implementation class for this entity type - i.e. the class to instantiate when an entity of this type is created.
  • By extending Entity, we indicate that this interface is an Entity type. We could alternatively have extended one of the other sub-types of Entity.
  • The OAUTH_KEY is a configuration key - it is configuration that can be set on the entity when it is being instantiated.
  • The @Effector annotation indicates that the given method is an effector, so should be presented and tracked as such. Execution of the effector is intercepted, to track it as a task and show its execution in the Activity view.
  • The @EffectorParam annotations give metadata about the effector’s parameters. This metadata, such as the parameter description, is available to those using the client CLI, REST API and web-console.

Note there is an alternative way of defining effectors - adding them to the entity dynamically, discussed in the section Dynamically Added Effectors.

Next lets add the implementation. Create a new Java class named GistGeneratorImpl.

package com.acme;

import java.io.IOException;
import java.util.Collections;

import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.util.text.Strings;
import org.eclipse.egit.github.core.Gist;
import org.eclipse.egit.github.core.GistFile;
import org.eclipse.egit.github.core.service.GistService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Iterables;

public class GistGeneratorImpl extends AbstractEntity implements GistGenerator {

    private static final Logger LOG = LoggerFactory.getLogger(GistGeneratorImpl.class);

    @Override
    public String createGist(String gistName, String fileName, String gistContents, String oathToken) throws IOException {
        if (Strings.isBlank(oathToken)) oathToken = config().get(OAUTH_KEY);

        GistFile file = new GistFile();
        file.setContent(gistContents);
        Gist gist = new Gist();
        gist.setDescription(gistName);
        gist.setFiles(Collections.singletonMap(fileName, file));
        gist.setPublic(true);
        
        GistService service = new GistService();
        service.getClient().setOAuth2Token(oathToken);
        LOG.info("Creating Gist: " +  gistName);
        Gist result = service.createGist(gist);
        return result.getId();
    }
    
    @Override
    public String getGist(String id, String oathToken) throws IOException {
        if (Strings.isBlank(oathToken)) oathToken = config().get(OAUTH_KEY);

        GistService service = new GistService();
        service.getClient().setOAuth2Token(oathToken);
        Gist gist = service.getGist(id);
        return Iterables.getOnlyElement(gist.getFiles().values()).getContent();
    }
}

To describe each part of this:

  • Extends AbstractEntity - all entity implementations should extend this, or one of its sub-types.
  • Implements GistGenerator: this is the Entity type definition, so must be implemented. Users of the entity will only refer to the interface; they will never be given an instance of the concrete class - instead a dynamic proxy is used (to allow remoting).
  • org.slf4j.Logger is the logger used throughout Cloudsoft AMP.
  • Implements the createGist effector - we do not need to re-declare all the annotations.
  • If no oath.key parameter was passed in, then use the configuration set on the entity.
  • Use the third party library to create the gist.

Configuring GitHub

First, create a github.com account, if you do not already have one.

Before running the blueprint, we’ll need to generate an access token that has permissions to create a gist programmatically.

First create a new access token that our blueprint will use to create a gist:

Create a new access key.

Next, grant the token rights to create gists:

Grant access.

Testing

The archetype project comes with example unit tests that demonstrate how to test entities, both within Java and also using YAML-based blueprints.

We will create a similar Java-based test for this blueprint. Create a new Java class named GistGeneratorTest in the package com.acme, inside src/test/java.

You will need to substitute the github access token you generated in the previous section for the placeholder text xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.

package com.acme;

import static org.testng.Assert.assertEquals;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.core.test.AMPAppUnitTestSupport;
import org.testng.annotations.Test;

public class GistGeneratorTest extends AMPAppUnitTestSupport {

    @Test
    public void testEntity() throws Exception {
        String oathKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        GistGenerator entity = app.createAndManageChild(EntitySpec.create(GistGenerator.class));
        String id = entity.createGist("myGistName", "myFileName", "myGistContents", oathKey);
        
        String contents = entity.getGist(id, oathKey);
        assertEquals(contents, "myGistContents");
    }
}

Similarly, we can write a test that uses the GistGenerator from a YAML blueprint. Create a new Java class named GistGeneratorYamlTest in the package com.acme, inside src/test/java.

Again you will need to substitute the github access token you generated in the previous section for the placeholder text xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. See the section on externalised configuration for how to store these credentials more securely.

package com.acme;

import static org.testng.Assert.assertEquals;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.camp.brooklyn.AbstractYamlTest;
import org.apache.brooklyn.core.entity.Entities;
import org.testng.annotations.Test;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;

public class GistGeneratorYamlTest extends AbstractYamlTest {

    private String contents;

    @Test
    public void testEntity() throws Exception {
        String oathKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        
        String yaml = Joiner.on("\n").join(
            "name: my test",
            "services:",
            "- type: com.acme.GistGenerator",
            "  brooklyn.config:",
            "    oauth.key: "+oathKey);
        
        Entity app = createAndStartApplication(yaml);
        waitForApplicationTasks(app);

        Entities.dumpInfo(app);

        GistGenerator entity = (GistGenerator) Iterables.getOnlyElement(app.getChildren());
        String id = entity.createGist("myGistName", "myFileName", "myGistContents", null);
        
        contents = entity.getGist(id, null);
        assertEquals(contents, "myGistContents");
    }
}

Building the OSGi Bundle

Next we will build this example as an OSGi bundle so that it can be added to the Cloudsoft AMP server at runtime, and so multiple versions of the
blueprint can be managed.

The mvn clean install will automatically do this, creating a jar inside the target/ sub-directory of the project. This works by using the Maven Bundle Plugin which we get automatically by declaring the pom.xml’s parent as brooklyn-downstream-parent.

Adding to the catalog

Similar to the sample.bom entity that ships with the archetype, we will define a .bom file to add our GistGenerator to the catalog. Substitute the URL below for your own newly built artifact (which will be in the target sub-directory after running mvn clean install).

brooklyn.catalog:
  libraries:
  - http://search.maven.org/remotecontent?filepath=com/google/code/gson/gson/2.2.2/gson-2.2.2.jar
  - http://repo1.maven.org/maven2/org/apache/servicemix/bundles/org.apache.servicemix.bundles.egit.github.core/2.1.5_1/org.apache.servicemix.bundles.egit.github.core-2.1.5_1.jar
  - http://developers.cloudsoftcorp.com/brooklyn/guide/blueprints/java/gist_generator/autobrick-0.1.0-SNAPSHOT.jar
  id: example.GistGenerator
  version: "0.1.0-SNAPSHOT"
  itemType: template
  description: For programmatically generating GitHub Gists
  displayName: Gist Generator
  iconUrl: classpath:///sample-icon.png
  item:
    services:
    - type: com.acme.GistGenerator

See Handling Bundle Dependencies for a description of the brooklyn.libraries used above, and for other alternative approaches.

The command below will use the br CLI to add this to the catalog of a running AMP instance. Substitute the credentials, URL and port for those of your server.

$ br login https://127.0.0.1:8443 admin pa55w0rd
$ br catalog add gist_generator.bom

Using the blueprint

The YAML blueprint below shows an example usage of this blueprint:

name: my sample
services:
- type: example.GistGenerator
  brooklyn.config:
    oauth.key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Note the type name matches the id defined in the .bom file.

You can now call the effector by any of the standard means - web console, REST API, or Client CLI.