Skip to content

Instantly share code, notes, and snippets.

@bimargulies
Created August 2, 2015 14:07
Show Gist options
  • Save bimargulies/a125bc534dd5c2dc8596 to your computer and use it in GitHub Desktop.
Save bimargulies/a125bc534dd5c2dc8596 to your computer and use it in GitHub Desktop.

A little journey into Karaf

Recently, I took some existing OSGi bundles that had never been used with Karaf, and use them to build a web service in Karaf. These notes record some of the pitfalls that I encountered.

From Bundles to Features

The first step in Karaf deployment is to create features. At first blush, making features is trivial:

You set packaging to 'feature':

    <packaging>feature</packaging>

you enable the plugin:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.karaf.tooling</groupId>
                <artifactId>karaf-maven-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <startLevel>81</startLevel>
                    <aggregateFeatures>false</aggregateFeatures>
                </configuration>
            </plugin>
        </plugins>
    </build>

And you declare your bundles as Maven dependencies. Out pops a features.xml that defines one feature, named after your project. I ran into two subtleties:

Embedded Jar Files and Transitive Dependencies

My bundles were built with the Felix project's bundle plugin. The bundle plugin does not rewrite the pom to reflect embedding. So, if you are assembling an OSGi bundle out of several non-OSGi artifacts files, the resulting POM will still declare those artifacts as dependencies, even if they are now embedded in your bundle.

When Karaf sees a non-OSGi dependency, it tries to do you a favor by creating 'wrapped' bundles on the fly. So, now you have two copies of the code: one nicely embedded and isolated in your bundle, and another in a wrapped bundle added by Karaf.

You can avoid this situation in the first place by using 'shade' rather than the bundle plugin to combine things; you might also be able to avoid it by judicious use of 'scope' or 'optional' when building your bundle. If you can't do that, you need to tell Karaf to ignore the unwanted dependencies. Here's an example:

            <plugin>
                <groupId>org.apache.karaf.tooling</groupId>
                <artifactId>karaf-maven-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <startLevel>80</startLevel>
                    <aggregateFeatures>false</aggregateFeatures>
                    <excludedArtifactIds>
                        <excludedArtifactId>rbl-generic</excludedArtifactId>
                    </excludedArtifactIds>
                </configuration>
            </plugin>

Per-bundle Start Control and Other Customization

A feature.xml file has one element per bundle. These elements contain a few facts that you might want to adjust. Further, there's some metadata for each feature that you might want to tweak. On the other hand, you really want to be able to take advantage of Karaf's scan of the classpath to accumulate your feature contents. So you don't want to give up on the plugin altogether. In my case, I have some fragment bundles. Fragment bundles can't be started, so I wanted to mark that in feature descriptor.

To handle these issues, Karaf allows you to create a partial feature.xml; it fills in around what you supply. Here's an example. Note that I took advantage of filtering to avoid explicit versions in here. Karaf filled in the rest of the bundles.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="test-root-component">
    <feature name="test-root-component" description="Test root component feature" version="${project.version}">
        <bundle start="false">mvn:com.basistech.osgi/rosette-osgi-root-component/${root-component-version}/jar/fragment-bundle</bundle>
    </feature>
</features>

Assemblies

Once you have some features, you might want to proceed to create an assembly; a pre-configured Karaf installation that includes your features. I must confess: one aspect of assembly construction remains a matter of cargo-cult programming for me; the declaration of the dependent features. The documentation has a discussion of the use of 'scope' on feature dependencies, and I have, so far, failed to understand why one would ever want to set it to 'compile'. My recipe, which does attract a useful airplane, is to:

  • Declare the framework KAR with scope compile.
  • Declare the 'standard' features with scope runtime.
  • Declare 'cxf' with scope runtime.
  • Declare my features with scope runtime.
  • Declare all the features as 'bootFeatures'.

The result of all of this is that my services start up when Karaf boots. That's what I want, so I'm content.

In addition to declaring features, you can adjust the Karaf configuration files by adding resources to your project. Mine looks like:

        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/filtered-resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*</include>
                </includes>
            </resource>
        </resources>

Thus, files in src/main/resources/etc/ are delivered, as-is, to 'etc' in the Karaf distribution structure, and files in src/main/filtered-resources/etc are filtered in transit. You need to avoid filtering if you have any cases where you want to have runtime expansion (e.g. of ${karaf.home}) -- you want filtering if you want to fill in other information from pom properties.

My particular locus of customization was Maven. My assembly has to work in a disconnected environment, so I had to set up for that. The standard configuration is, in fact, quite friendly to this use case. The following are something of a 'belt and suspenders'; they allow me to control the contents of settings.xml, and they avoid touching files outside of the Karaf tree. Note that, with the assembly configuration above, Karaf won't, in fact, write anything to the local repository.

org.ops4j.pax.url.mvn.settings=${karaf.home}/etc/maven-settings.xml
org.ops4j.pax.url.mvn.localRepository=${karaf.data}/maven-repo

Testing

Pax-exam is the usual tool for automated tests of OSGi bundles in a Maven build. Pax-exam, helpfully enough, has Karaf support. There are, however, some subtleties.

Karaf takes some time to start up. If your test depends on Karaf being started, you need to wait for it. That's pretty simple: you just have to write your pax-exam junit test to inject `org.apache.karaf.features.BootFinished'.

The Maven customization I gave above will disconnect Karaf from your development environment. When testing, Karaf tends to find some extra things that it wants to download. So you need to customize your @Configuration to reconnect to your Maven build. This method configures my particular service, adds an extra feature, and re-re-configures Maven.

@Configuration
    public static Option[] configuration() throws Exception {
        String basedir = System.getProperty("basedir");
        File pipelineConfigFile = new File(basedir, "src/test/config/annotator-config.yaml");
        File mavenSettingsFile = new File(basedir, "src/test/karaf-config/maven-settings.xml");
        MavenArtifactUrlReference karafUrl = maven().groupId("com.basistech.ws").artifactId("rosapi-assembly").versionAsInProject().type("tar.gz");
        return new Option[] {
                /* The coordinates of the features.xml, and then the name of the feature from there we want installed. */
            features(maven("com.basistech.ws", "test-root-component").classifier("features").type("xml").versionAsInProject(), "test-root-component"),
            karafDistributionConfiguration().frameworkUrl(karafUrl).karafVersion("4.0.0").name("Apache Karaf")
                    .unpackDirectory(new File("target/pax")),
            keepRuntimeFolder(),
            logLevel(LogLevelOption.LogLevel.ERROR),
                // The plugin runs Karaf from target/pax/UUID directory.
            new KarafDistributionConfigurationFileExtendOption("etc/system.properties", "bt.as.configPathname", pipelineConfigFile.getAbsolutePath()),
                // Replace the 'suitable for deployment' maven config with a developer config so that it can find the feature
                // called out above. Depending on reading the local repo to get an item from earlier in the build may not be wise.
            new KarafDistributionConfigurationFilePutOption("etc/org.ops4j.pax.url.mvn.cfg", "org.ops4j.pax.url.mvn.localRepository", System.getProperty("user.home") + "/.m2/repository"),
            new KarafDistributionConfigurationFilePutOption("etc/org.ops4j.pax.url.mvn.cfg", "org.ops4j.pax.url.mvn.defaultLocalRepoAsRemote", "true"),
            new KarafDistributionConfigurationFileReplacementOption("etc/maven-settings.xml", mavenSettingsFile)
        };
    }

If you use a pax-exam test, you are running your test inside Karaf. What if, like me, you need to test from outside Karaf? In my case, the point of the whole business is to launch a restful web service and to test that it can, in fact, be reached from outside Karaf.

For this purpose, pax-exam provides the exam-maven-plugin. This plugin provides goals suitable for use in pre-integration-test and post-integration-test. The first starts the container (in this case, Karaf), and the second stops it.

To specify the configuration, you create a class consisting only of a pax-exam @Configuration method, and configure it into the plugin. The plugin forks a JVM with your Maven 'test' classpath and then uses the configuration class to launch a container.

             <plugin>
                <groupId>org.ops4j.pax.exam</groupId>
                <artifactId>exam-maven-plugin</artifactId>
                <version>4.5.0</version>
                <configuration>
                    <configClass>com.basistech.anvils.it.KarafConfiguration</configClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>start-container</goal>
                            <goal>stop-container</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

Once again, however, you have the problem of waiting for Karaf to start up. And the problem is harder; you don't have accessed to the BootFinished service. I coded a retry loop in a @BeforeClass method to wait for my service to start. You could probably think of something more elaborate. If you get this wrong, your tests will fail and leave very little evidence of their problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment