June 23, 2015

Bamboo, maven-release-plugin, public/private key SSH access to SVN, password access to Nexus

Introduction

Bamboo is great for performing builds.  The maven-release-plugin is great at releasing new versions of your software. SVN is a great source code repository. SSL public/private keys are great for keeping everything secure, and Nexus is great for Maven artifacts.  What's not so great is trying to get all these things working together!  What we'll look at in this blog is how to configure Bamboo build steps for both the repository host and the Maven configuration. Also, we'll look at how to configure your project's pom. Let's start with the Bamboo repository host build step.

Bamboo repository host build step

First, you need to configure the Bamboo repository host build step so Bamboo can checkout your source code:

Figure 1: Repository host build step

Looking at figure 1, the repository host setup is pretty easy: 

(A)
The URL.  It will need to be in the form "svn+ssh" instead of "https". 

(B)
The username.  It's a good idea to create an account specifically for bamboo to use. 

(C)
The authentication type...no big deal. 

(D)
A variable reference to the fully-qualified location of the SSL key.

Next let's take a look at the Maven configuration build step.

Maven configuration build step

Figure 2: Maven configuration build step
Let's take a look at this configuration one field at a time.

(A)
The version of Maven to use for this build. I did this research using 3.0.5.  No guarantees other versions of Maven will work.

(B)
The Maven goal to perform along with some additional attributes.

--batch-mode release:clean release:prepare release:perform
This runs the release plugin in batch mode so there is no user interaction and all defaults are used.

-s /home/bamboo_builder/.m2/bamboo_builder-settings.xml
This configures Maven to use a specific settings file.  In most cases the same Maven settings will be used for all builds in your organization. But, if you have builds which require unique settings, this is how to configure which settings file to use.  The main purpose of this file is to contain the username and encrypted password to access Nexus  (Maven Encryption, 2015, June 21), which will look something like listing 1.

Listing 1: Nexus username & password in settings.xml

  
    nexus
    bamboo_builder
    {lAHiuyjiu4387y5jKLHE29kjlkahyr/=}
  

(C)
The version of Java to use.

(D)
The environment varibles for the build. This is most important field.  After much research, I found these values MUST BE CONFIGURED HERE (not in goals) otherwise the build will fail.

SVN_SSH="ssh -v -l bamboo_builder -i /home/bamboo_builder/.ssh/id_rsa"
This environment variable configures ssh to use the bamboo_builder user and the id_rsa private key when establishing an SSL connection with SVN.  Now, Maven itself has a number of different ways to configure a username and private key both within a settings.xml file and within the pom.xml for a project.  I tried them all and they all failed.  This was the only way I got it to work.

MAVEN_OPTS="-Dsettings.security=/home/bamboo_builder/.m2/bamboo_builder-security-settings.xml"
This configures Maven with a master password used to encrypt the other passwords in the settings.xml file (Maven Encryption, 2015, June 21).  Though not strictly necessary, it's good to have this file.  If you do have it, this is where you put the configuration for the build. Listing 2 shows what this looks like

Listing 2: Maven master password

    {jj498POHIU4HRUhj2rjpu2ajkshdfur/YI=}

(E)
If your project does not have unit tests, uncheck this box so the build won't fail.

Finally, your project's pom.xml needs a bit of special configuration so let's take a look at that next.

Project pom.xml

The project's pom.xml needs a couple configurations: The <scm> tag and the maven-release-plugin version.

First the <scm> tag needs to be configured so Maven knows where the project is located in SVN.  Listing 3 shows what this tag will look like. 

    NOTE: All 3 properties are needed...don't skip any!

Listing 3: The pom <scm> tag 

    scm:svn:svn+ssh://my.subversion.com/data1/svns_repo/path/to/my/project/trunk
    scm:svn:svn+ssh://my.subversion.com/data1/svns_repo/path/to/my/project/trunk
    scm:svn:svn+ssh://my.subversion.com/data1/svns_repo/path/to/my/project/trunk

Next the maven-release-plugin version needs to be configured. The default version of the maven-release-plugin for Maven 3.0.4 has some bugs in it, most notibly when trying to use this configuration to perform mult-module reactor builds.  So your project pom.xml must be configured to use a more recent version of the maven-release-plugin.  Listing 4 shows this.

Listing 4: The maven-release-plugin version

  
    
      
        org.apache.maven.plugins
        maven-release-plugin
        2.5.1
        
          false
        
      
    
  

Hopefully this fully documents everything that's needed.  Enjoy!

References
Maven Encryption. (2015, June 21). maven.apache.org. Retrieved June 23, 2015 from https://maven.apache.org/guides/mini/guide-encryption.html





June 10, 2015

Mocking properties of super classes in different packages for unit testing

Suppose I have the following reusable abstract class.
package org.ferris.io;
import org.apache.log4j.Logger;  
import javax.inject.Inject;
public abstract class AbstractPropertiesFile extends File {
  @Inject
  protected Logger log; 
  // . . .
}
Also suppose my application has a concrete implementation of this abstract class.
package org.ferris.application.preferences;
import  org.ferris.io.AbstractPropertiesFile;
public class PreferencesPropertiesFile extends AbstractPropertiesFile{
// . . .
} 
When unit testing PreferencesPropertiesFile, the challenge is to mock the protected Logger property of the super class.  It's a challenge because:
  1. The concrete class in in a different package than the abstract class
  2. The Logger property is being injected by CDI for you so there is no setter method.
Let's look at the first challenge.  A possible solution is to repackage my application and unit test to use the org.ferris.io package, but this means moving the PreferencesPropertiesFile to a package which doesn't make much sense for it to be in. Now let's look at the second challenge.  Adding a setter method would solve it, but do you really want to add a method just for unit testing purposes?  Neither of these solutions seem right.

A better solution is for the unit test to set the property of the super class using reflection. Though I'm sure many would argue against this solution, I'm OK with it because let's face it, every framework nowadays is using reflection or byte code manipulation, or proxying.  So a little more won't hurt.

Here is how you would setup the unit test.
@Mock
Logger logMock;
// . . .
@Test
public void someTest() {
    PreferencesPropertiesFile props = new PreferencesPropertiesFile ();
    {
        Field logField = AbstractPropertiesFile.class.getDeclaredField("log");
        logField.setAccessible(true);
        logField.set(props, logMock);
    }
    // . . .
} 
Enjoy!

April 22, 2015

Apache 2 ProxyPassReverse URL Replacement

It is very common to use Apache virtual hosts as a proxy to get to other application servers running in your environment. I have such a setup with Jira for my open source projects.  I have Jira for my open source projects at this URL: http://ferris-jira.ddns.net. This domain name gets resolved to my server, hits an Apache virtual host for that domain, then ProxyPasses the requests along to the Jira instance. The response from Jira gets ProxyPassReversed back through Apache and to the user's browser. This works fine, as long as links generated by the application are all relative.

Unfortunately, Jira generates some fully-qualified links which started with http://localhost:8080.  Once this fully-qualified link gets to the user's browser it obviously won't work.  To solve this problem I used a combination of different Apache directives to search through the response from Jira and replace any of the localhost URLs.  Let's take a look.

# Turn compression off in order for the Substitute to work.
RequestHeader unset Accept-Encoding
# http://httpd.apache.org/docs/2.4/mod/mod_proxy.html#examples
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
Substitute "s|http://localhost:8080|https://ferris-jira.ddns.net|n"
# In order for the substitute module to work we have to add it to the filter chain.
FilterDeclare Substitute
FilterProvider Substitute SUBSTITUTE "%{REQUEST_URI} =~ m#^/#"
FilterChain +Substitute

The first thing we need to do is use RequestHeader and turn off compression (#2). If the response from Jira is zipped, we can't do any search and replace so the response needs to be plain text.  Next are the typical ProxyPass (#4)  and ProxyPassReverse (#5). These get the request to Jira and the response from Jira. Next we define a Subsitute (#6) for what we want to replace. You'll recognize this syntax from the sed command. Finally we have a filter chain to get the Substitute to run on the response (#8-10). The filter matches on the URI of the request made to Apache (#9), and is typically configured to the application's context.  In the example above, I have Jira running as the root context / which is why ProxyPass and ProxyPassReverse have http://localhost:8080/ and the FilterProvider matches the regular expression m#^/#.  If Jira was running on the "jira" context, the configuration would be ProxyPass and ProxyPassReverse with http://localhost:8080/jira and FilterProvider with the regular expression m#^/jira#.

So that's it.  put this into your Apache <VirtualHost>, restart Apache and you'll be good to go.

References
ArtemGr. (Oct 15, 2013). Apache 2.4 reverse proxy with URL substitution. Retrieved April 10, 2015 from https://gist.github.com/ArtemGr/6993113

Enjoy!