Executing Karaf Commands in PAX-Exam Tests

I recently had to write an integration test with PAX-Exam that required to install bundles in Karaf during the test execution. Usually, when you need a bundle to be installed with such tests, you add them in your initial PAX configuration. But this time, I had to install a bundle during the test, which means it could not be installed first.

The solution to that was to inject a Karaf (console) session in my test, and execute a command (bundle:install) from it.
Since I had to search for quite a moment, I thought it would be useful to make a post about it. Is is widely inspired from some integration tests from Karaf. At the moment, I have not found a better way to do it, but at least it is working.

Here is the snippet.

package whatever;

import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
import static org.ops4j.pax.exam.CoreOptions.systemProperty;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.configureSecurity;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.security.auth.Subject;

import org.apache.karaf.features.BootFinished;
import org.apache.karaf.features.FeaturesService;
import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.api.console.SessionFactory;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.ProbeBuilder;
import org.ops4j.pax.exam.TestProbeBuilder;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerMethod;

@RunWith( RoboconfPaxRunner.class )
@ExamReactorStrategy( PerMethod.class )
public class DelayedInstallationOfTargetHandlerTest extends DmTest {

	@Inject
	protected Manager manager;

	@Inject
	protected FeaturesService featuresService;

	@Inject
	protected SessionFactory sessionFactory;

	// Wait for all the boot features to be installed.
	@Inject
	protected BootFinished bootFinished;

	private Session session;
	private final ExecutorService executor = Executors.newCachedThreadPool();
	private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
	private final PrintStream printStream = new PrintStream( this.byteArrayOutputStream );
	private final PrintStream errStream = new PrintStream( this.byteArrayOutputStream );


	@Before
	public void setUp() throws Exception {
		this.session = this.sessionFactory.create( System.in, this.printStream, this.errStream );
	}


	@Override
	@Configuration
	public Option[] config() throws Exception {

		// Usual PAX configuration from a super class
		List<Option> options = new ArrayList<> ();
		options.addAll( Arrays.asList( super.config()));

		// Disable the JMX server. Not sure it is really useful...
		options.add( configureSecurity().disableKarafMBeanServerBuilder());

		return options.toArray( new Option[ options.size()]);
	}


	@Test
	public void run() throws Exception {

		// Do whatever you want before...

		// Prepare the execution
		this.byteArrayOutputStream.flush();
		this.byteArrayOutputStream.reset();

		// What we want to execute...
		final Callable<String> commandCallable = new Callable<String> () {
			@Override
			public String call() throws Exception {

				try {
					DelayedInstallationOfTargetHandlerTest.this.session.execute( "bundle:install mvn:/..." );

				} catch( Exception e ) {
					e.printStackTrace( System.err );
				}

				DelayedInstallationOfTargetHandlerTest.this.printStream.flush();
				DelayedInstallationOfTargetHandlerTest.this.errStream.flush();
				return DelayedInstallationOfTargetHandlerTest.this.byteArrayOutputStream.toString();
			}
		};

		// We will use "bundle:install", which requires some privileges.
		// So, we must enclose our invocation in a privileged action.
		String response;
		FutureTask<String> commandFuture = new FutureTask<String>( new Callable<String>() {
			@Override
			public String call() {

				// FIXME: isn't there a better way? "admin"???
				// The question was asked on Karaf's mailing-list.
				Subject subject = new Subject();
				subject.getPrincipals().addAll( Arrays.asList( new RolePrincipal( "admin" )));
				try {
					return Subject.doAs( subject, new PrivilegedExceptionAction<String> () {
						@Override
						public String run() throws Exception {
							return commandCallable.call();
						}
					});

				} catch( PrivilegedActionException e ) {
					e.printStackTrace( System.err );
				}

				return null;
			}
		});

		// Execute our privileged action.
		try {
			this.executor.submit( commandFuture );

			// Give up to 30 seconds for the command to complete...
			response = commandFuture.get( 30L, TimeUnit.SECONDS );

		} catch( Exception e ) {
			e.printStackTrace( System.err );
			response = "SHELL COMMAND TIMED OUT: ";
		}

		System.err.println( response );

		// Do whatever you want after...
	}
}

As you can see, the main issue is that some actions require special privileges.
It is the case of bundle:install. So, we have to wrap our execution so that it works. When you miss this part and simply use the session, you will only have access to the basic commands, those that generally are read-only (e.g. bundle:info).

You can find the original class in Roboconf’s main repository. I also created a Gist here.


About this entry