18.7.2015 - Docker zum Zweiten

Teil 1 Teil 2 Teil 3 Teil 4

Mehr von Docker

Jetzt wollen wir verschiedene Docker-Container miteinander spielen lassen:

sudo docker -d --env-file env.txt -v /srv/ebooks --name ebook-store rgwch/owncloud-client:latest
sudo docker -d -p 4040:8080 --volumes-from ebook-store rgwch/calibre-server:latest

Wenn env.txt so aussieht:

     URL=https://my.server.com/owncloud/remote.php/webdav/ebooks
     USER=ichselber
     PASSWORD=ganz_geheim

Dann wird dieses Arrangement folgendes tun: Es erstellt eine lokale Calibre-library in einem docker-data-container, hält diese Library synchron mit dem owncloud-Verzeichnis “ebooks” auf dem Server my.server.com und bietet diese Library auf port 4040 an. Mit irgendeinem kompatiblen eBook-reader (oder einem Web-Browser) kann man dann die Bibliothek durchsuchen und Bücher zum Lesen herunterladen.

Natürlich könnte man dasselbe auch ohne Docker erreichen. Was hier besticht, ist aber die Einfachheit. So ein System aufzusetzen dauert keine 10 Minuten. Und es läuft auf jedem Computer, auf dem Docker installiert ist.

Automated Builds

Wenn man ein wenig mit Docker herumspielt, stellt man bald fest, dass Docker images ein wenig unhandlich sind. Zwar tut Docker sein Bestes, um nur die Layers umherzuschieben, die wirklich unterschiedlich sind, aber vor allem der Upload eines selbstgemachten Images auf hub.docker.com ist doch eine arge Geduldsprobe. Und wenn man eine Kleinigkeit ändert, muss man erneut hochladen.

Man kann sich behelfen, indem man statt Docker Images nur die Dockerfiles umherschiebt und die Images auf jedem Computer separat mit docker build erstellt. Das kann aber je nach Image dann auch wieder ein recht zeitraubender Prozess sein. Aber es gibt einen einfacheren Weg, zumindest für OpenSource Dockerfiles:

  • github und docker accounts erstellen, wenn noch nicht vorhanden
  • Dockerfile und eventuell für den build benötigte Zusatzdateien in ein öffentliches Github Repository stellen
  • Auf hub.docker.com Add Repository und dort automated build auswählen. Dann GitHub wählen und das vorhin erstellte Repository aufsuchen.
  • Auf GitHub das Repository aufsuchen, dort auf Settings gehen, webhooks&services anklicken, unter services nach “Docker” suchen und aktivieren.

Von jetzt an wird jedes Mal, wenn ein Push ins GitHub Repository erfolgt, ein neues Docker-Image erstellt und automatisch auf Dockerhub bereitgestellt. Das Readme.md des Github Repositories wird dann zur Description des Docker Images.

13.7.2015 - Git Error 411

Git meldet beim Push folgendes:

$> git push origin master
Counting objects: 117, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (103/103), done.
error: RPC failed; result=22, HTTP code = 411
fatal: The remote end hung up unexpectedly

Tja, was nun?

Wenn man Zugriff auf den Server hat, kann man das Server log anschauen:

2015-07-13 15:21:44: (request.c.1125) POST-request, but content-length missing -> 411 

Tja, was nun?

Google hilft, aber nur mit den exakt richtigen Suchbegriffen…

Kurz: Folgendes muss man beim git repository eingeben:

`git config http.postBuffer 524288000`

Dann klappt’s auch mit dem Pushen.

5.7.2015 - Maven - Plugin! Plug in!

Teil 1 Teil 2 Teil 3 Teil 4

Plug in!

Irgendwann will man etwas, was Maven nicht kann. Dann macht man sich entweder auf die Suche nach einem Plugin mit den gewünschten Fähigkeiten, oder man schreibt selber eins. Das ist verblüffenderweise gar nicht so schwierig.

Anstatt irgendein Pseudoprojekt zu erstellen, beziehe ich mich im Folgenden auf ein real existierendes Projekt: Webelexis, und ein real existierendes Plugin: mimosa-maven-plugin, das ich für dieses Projekt gemacht habe.

Die Idee ist folgende: Das Teilprojekt webelexis-client ist eine JavaScript-Applikation, das Teilprojekt webelexis-server ist ein Java-Programm. Das Gesamtprojekt besteht dann aus dem Server, welcher das client-Programm in seinem Ubterverzeichnis ‘web’ hält, und mit diesem zusammen in ein einziges jar gepackt wird, welches der Anwender dann einfach mit java -jar webelexis-servr-x.y.z.jar starten kann und sofort eine voll funktionsfähige Web-Anwendung hat. Um das zu erreichen sind folgende Arbeitsschritte notwendig:

Clientseitig:

  • Jade-Dateien nach HTML compilieren
  • JavaScript Dateien minimieren und zusammenfassen
  • CSS Dateien minimieren und zusammenfassen
  • alles andere unverändert ins Zielverzeichnis kopieren
  • Unit-Tests laufen lassen

Diese ganzen clientseitigen Arbeiten habe ich mimosa übertragen. Ein einfaches mimosa build -m baut die minimierte Version der Website im Verzeichnis webelexis-client/dist

Serverseitig

  • Java Dateien compilieren
  • Ressourcen ins Zielverzeichnis kopieren
  • Tests laufen lassen
  • Alles in ein .jar packen.

Diese Aufgaben erledigt, wen wundert’s, Maven.

Bisher:

Der manuelle Ablauf ist so, dass man zuerst mimosa build im client startet, dann maven compile im server, dann den Inhalt von “webelexis-client/dist” nach “webelexis-server/target/classes/web” kopiert und dann maven package aufruft, um alles zusammenzupacken.

Neu:

Okay, man könnte sich einfach ein Shell-Script schreiben, was das alles erledigt. Oder meinetwegen einen Ant-Task. Oder man könnte ein existierendes Maven-Plugin, zum Beispiel exec-maven-plugin einsetzen. Aber wir wollen ja etwas lernen. Also bauen wir unser eigenes Plugin für diesen Job. Das Ziel ist: Ich gebe nur maven package ein, und dann werden client und server gebaut, getestet, zusammenkopiert und in ein jar abgefüllt.

Aufbau:

Das Plugin besteht aus folgenden Teilen:

pom.xml (wer hätte das gedacht?) Ich zitiere hier nur die relevanten Stellen. Die ganze Datei kann im Quelltext mit oben im ersten Absatz stehendem Link heruntergeladen werden.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>rgwch</groupId>
    <artifactId>mimosa-maven-plugin</artifactId>
    <version>1.0.1</version>
    <packaging>maven-plugin</packaging>
    <dependencies>
        <dependency>
            <groupId>org.apache.maven</groupId>
            <artifactId>maven-plugin-api</artifactId>
            <version>3.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.maven.plugin-tools</groupId>
            <artifactId>maven-plugin-annotations</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-io</artifactId>
            <version>1.3.2</version>
        </dependency>

    </dependencies>
</project>

Das Einzige, was anders ist, als bei anderen Maven-Projekten, ist <packaging>

Und dann im Wesentlichen der Klasse ch.rgw.mmp.MimosaMavenPlugin.java:

package ch.rgw.mmp;

import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.IOException;

@Mojo(name = "mimosa")
public class MimosaMavenPlugin extends AbstractMojo {

    @Parameter private File source;
    @Parameter private File intermediate;
    @Parameter private File dest;
    @Parameter private String mimosaOptions;

    public void execute() throws MojoExecutionException, MojoFailureException {
        if (!dest.exists() && !dest.mkdirs()) {
        throw new MojoFailureException(null, "could not create directory " + dest.getAbsolutePath(), "");
        }
        getLog().info("launching mimosa from " + source.getAbsolutePath());
        try {
        Process mimosa=Runtime.getRuntime().exec("mimosa build", null,source);
        StreamBuffer errorStream=new StreamBuffer(mimosa.getErrorStream(),"ERROR");
        StreamBuffer outputStream=new StreamBuffer(mimosa.getInputStream(),"OUTPUT");
        errorStream.start();
        outputStream.start();
        getLog().info(Integer.toString(mimosa.waitFor()));
        FileUtils.copyDirectory(intermediate,dest);
        } catch (IOException e) {
        e.printStackTrace();
        throw new MojoFailureException(e.getMessage());
        } catch (InterruptedException e) {
        e.printStackTrace();
        throw new MojoFailureException(e.getMessage());
        }
    }
}

Das ist alles. Die Klasse StreamBuffer liste ich hier jetzt nicht auf, die erledigt nur das Handling des outputs von Mimosa. Die einzige Besonderheit dieser Klasse sind die Annotationen @Mojo und @Parameter, sowie der Aufruf von getLog(). Alles Andere ist standard Java.

@Mojo, das mich immer an diese kanarische Sauce erinnert, ist das Kommando mit dem dieses Plugin involviert wird. Hier definieren wir, dass es auf den Namen “mimosa” hören soll.

@Parameter sind die Werte, die das aufrufende Projekt dem Plugin mitgeben kann.

Einsatz

Der Aufruf unseres Plugins in der pom.xml von webelexis-server sieht so aus:

<plugin>
    <groupId>rgwch</groupId>
    <artifactId>mimosa-maven-plugin</artifactId>
    <version>1.0.1</version>
    <configuration>
        <source>../webelexis-client</source>
        <intermediate>../webelexis-client/dist</intermediate>
        <dest>target/classes/web</dest>
        <mimosaOptions>-m</mimosaOptions>
    </configuration>
    <executions>
        <execution>
            <id>1</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>mimosa</goal></goals>
        </execution>
    </executions>
</plugin>

Hier wird deklariert, welche Parameter das Plugin bekommt, nämlich das Basisverzeichnis des Client-Projekts, dessen Ausgabeverzeichnis, sowie das endgültige Zielverzeichnis im Server-Projekt. Dann wird unter <executions> festgelegt, wann es ausgeführt wird, und welches Kommando ausgeführt wird: Wir möchten, dass in der Phase “prepare-package” das Kommando “mimosa” unseres Plugins ausgeführt wird. Danach soll der Inhalt des mimosa-Ausgabeverzeichnisses ins Unterverzeichnis “web” des server-Ausgabeverzeichnisses kopiert werden.

Das ist eigentlich alles. Wenn wir jetzt zunächst im Verzeichnis mimosa-maven-plugin den Befehl mvn install ausführen, können wir anschliessend im Server-Verzeichnis mvn package aufrufen und erhalten am Ende ein .jar, welches den verarbeiteten Server- und Client- Code enthält.

Publish or perish

Um ein Plugin auch anderen zur Verfügung zu stellen, muss man ein wenig mehr Aufwand betreiben. Bisher haben wir es ja nur mittels mvn installim lokalen Repository installiert.

Wir könnten es z.B. nach maven central schicken, oder an einen anderen vom Internet aus erreichbaren Ort. Im Prinzip spielt das keine Rolle. Ich zeige hier das Vorgehen für Bintray. Bintray ist ein Hoster für Binärdateien von OpenSource Projekten. Die Firma ist sehr liberal. Ausser einem kostenlosen Konto gibt es keine Voraussetzungen. Mit einem solchen kostenlosen Konto hat man automatisch ein Maven-Repository (und zusätzlich auch Repositories für rpm, debian, docker und Anderes).

Nach dem Erstellen eines Kontos geht es los:

Package erstellen:

  • Maven -> Add new Package

  • Name: mimosa-maven-plugin (Der Name ist nicht ganz zufällig gewählt, sondern wird von Apache so vorgeschlagen. Auf keinen Fall sollte man sein Plugin etwa “maven-mimosa-plugin” nennen, da alle Namen beginnend mit “Maven” für Apache reserviert sind. Selbstverständlich beachten wir dieses Anliegen.)

  • Licenses: Hier muss man mindestens eine OpenSource Lizenz eingeben. Das ist Bedingung für das kostenlose Hosten auf Bintray

  • Version Control: Hier muss man angeben, wo der Quellcode zu finden ist. In unserem Fall ist das http://gitlab.com/rgwch/mimosa-maven-plugin.

Alle anderen Felder sind nicht zwingend.

Mit “Create Package” wird die Package dann erstellt.

Version erstellen

Das eben erstellte mimosa-maven-plugin package anwählen und dort “new version” eingeben. Idealerweise gibt man hier dasselbe ein, was man auch in der pom.xml eingetragen hat (das wird zar nicht überprüft, erscheint aber vernünftig).

Dateien hochladen

Die eben erstellte Version anklicken und “upload file” wählen. Wichtig ist nun das Feld Target repository path. Hier muss der maven-konforme Pfad (also so wie im lokalen maven repository) eingegeben werden. In unserem Fall ist das: rgwch/mimosa-maven-plugin/1.0.1

Dann muss man mindestens zwei Dateien hochladen:

  • mimosa-maven-plugin-1.0.1.jar
  • mimosa-maven-plugin-1.0.1.pom

Das zweite ist die pom.xml, die vor dem Hochladen so umbenannt werden muss.

Nachdem wir für beide Dateien “publish” freigegeben haben, sind sie vom Internet aus erreichbar. Allerdings wird Maven sie nicht finden. Wie sollte es auch? “irgendwo im Internet” ist keine ausreichend genaue Beschreibung.

pom.xml anpassen

Folgender Eintrag in der pom.xml von webelexis-server löst dieses Problem:

<pluginRepositories>
    <pluginRepository>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>bintray-rgwch-maven</id>
        <name>bintray-plugins</name>
        <url>http://dl.bintray.com/rgwch/maven</url>
    </pluginRepository>
    <pluginRepository>
        <releases>
            <updatePolicy>never</updatePolicy>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>central</id>
        <name>Central Repository</name>
        <url>http://repo.maven.apache.org/maven2</url>
    </pluginRepository>
</pluginRepositories>

Damit erklären wir Maven, dass es Plugins nicht nur in repo.maven.apache.org, sondern auch in dl.bintray.com/rgwch/maven suchen soll.

Jetzt kann jeder nodejs- und maven-Besitzer Webelexis sehr einfach selber bauen:

git clone https://github.com/rgwch/webelexis.git
sudo npm install -g mimosa
cd webelexis/webelexis-server
mvn package

Das ist alles. Das mimosa-maven-plugin wird im Rahmen von mvn package automatisch von bintray heruntergeladen, installiert und ausgeführt.

RTFM

Ohne Dokumentation werden die Anwender unseres neuen Plugins viel zuviele Rückfragen stellen. Und am liebsten wollen sie die Dokumentation so, wie sie sie gewohnt sind. Zum Beispiel in derselben Form, wie Libraries und Plugins in maven central dokumentiert sind.

Nicht leichter als das.

mvn site 

erledigt das und erstellt ein Verzeichnins “site” in mimosa-maven-plugin/target, das man unverändert auf einen Webserver kopieren kann. Das Resultat sieht dann so aus.

4.7.2015 - Maven - Konfiguration

Teil 1 Teil 2 Teil 3 Teil 4

Convention over Configuration, but…

Wie wir in den letzten beiden Posts, Einstieg und Pom gesehen haben, startet Maven mit “vernünftigen” Annahmen, die dazu führen, dass ein einfaches Projekt, welches ein Layout hat, das den Maven-Konventionen entspricht, erfolgreich gebaut werden kann. Und zwar ohne irgendwelche zusätzlich Konfiguration, die über das simple Basis-Pom hinausgeht.

Leider sind im “echten Leben” Projekte nur selten einfach genug, um allein per Konvention gebaut werden zu können. Dann sind Eingriffe in der pom.xml notwendig. Die ultimative Unterstützung dabei ist die offizielle POM-Referenz von Apache.

Ich zeige hier nur einige häufig benötigte Dinge.

Dependencies

Abhängigkeiten müssen in der Pom deklariert werden. Dies ist genauer im letzten Post beschrieben. Man kann entweder manuell <dependency> tags einfügen, oder man überlässt seiner IDE das Management. Zumindest Idea hat ziemlich ausgedehnten Maven Support.

Plugins

Fast alles, was Maven tut, tut es mit Plugins. Das heisst, es tut eigentlich nichts, als in jeder “lifecycle-phase” die dazu passenden Plugins aufzurufen und zu parametrisieren. Die Parameter entnimmt es der pom.xml. Dass man davon im Grundzustand nichts sieht, das liegt -wieder einmal- am “convention over configuration”. Wann immer ein Plugin in der pom.xml nicht konfiguriert wird, dann erhält es “vernünftige” Defaults.

Betrachten wir als Beispiel das “maven-compiler-plugin”. Dieses gehört zu den Plugins, die standardmässig, also ohne weitere Konfiguration, von Maven geladen und ausgeführt werden. Seine Aufgabe ist das Kompilieren von Java-Quellcode aus src/main/java nach target/classes. Wenn man genau das will und mit den Standardeinstellungen des Compilers zufrieden ist, muss man überhaupt nichts konfigurieren.

Wenn man etwas anderes will, dann muss man das Plugin in der Sektion <build> der pom.xml aufnehmen und konfigurieren.

<project>
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.3</version>
        <configuration>
          <source>1.8</source>
          <target>1.8</target>
          <debug>false</debug>
          <optimize>true</optimize>
        </configuration>
      </plugin>
    </plugins>
  </build>
  ...
</project>

In diesem Beispiel wollen wir, dass der Compiler optimierten java 8 - Bytecode ohne Debug-Informationen erstellt. Alle anderen Einstellungen bleiben auf default.

Diese Modifikation ist relativ häufig nötig, weil der Maven-Compiler standardmässig unoptimierten Java-5 Bytecode mit debug-Informationen erstellt.

Ebenfalls häufig wird man das maven-jar-plugin rekonfigurieren wollen:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>2.6</version>
    <configuration>
        <archive>
            <addMavenDescriptor>true</addMavenDescriptor>
            <compress>true</compress>
            <index>true</index>
            <manifest>
                <addClasspath>true</addClasspath>
                <mainClass>ch.webelexis.Runner</mainClass>
            </manifest>
            <manifestEntries>
                <mode>production</mode>
                <url>${project.url}</url>
                <timestamp>${maven.build.timestamp}</timestamp>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

Standardmässig erstellt der jar.packager kein Attribut “mainClass” in Manifest, so dass das resultierende jar nicht startfähig ist. Mit obiger Modifikation klappt es. Der Abschnitt <manifestEntries< dient nur zur Illustration, wie man beliebige weitere Attribute ins Manifest einbringen kann.

Das nächste Beispiel ist ein Plugin, das nicht zum Standard-Lieferumfang von Maven gehört. Es wird darum beim ersten Start des Projekts heruntergeladen.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.4</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Dieses Plugin löst folgendes Problem: Wenn man ein Projekt erstellt, das mehrere Abhängigkeiten hat, dann läuft das auf dem eigenen Computer wunderbar. Wenn man nun daraus ein .jar erstellt, und dieses jar weitergibt, dann wird es auf den meisten Zielcomputern nicht laufen. Weil es Abhänigkeiten zu libraries hat, die im lokalen maven repository gespeichert sind. Nun kann man entweder all diese Libraries mühsam zusammensuchen und mit ausliefern, sowie beim Prorgammstart den passenden classpath aufbauen, oder man lässt ein Plugin diese Arbeit erledigen. Das “shade” plugin kann einiges, aber in der hier gezeigten Basiskonfiguration tut es “nur” folgendes: Es holt und entpackt alle Abhängigkeiten und verpackt sie neu im generierten jar unseres Projekts. Dieses jar heisst im Apache-Speak dann “Uber-Jar” und hat keinerlei externe Abhängigkeiten mehr (Ausser Java natürlich). Gebräuchlicher als “Uber-Jar” ist der Begriff “Fat-Jar” für solche jars.

Properties

Manchmal will man selber irgendwelche Eigenschaften definieren. Das Sammelbecken für alles Mögliche ist der Abschnitt <properties>

<properties>
    <vertx-version>3.0.0</vertx-version>
    <maven.build.timestamp.format>E, dd MM yyyy HH:mm:ss z</maven.build.timestamp.format>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <hallo>Grüezi</hallo>
</properties>

Hier legen wir einige Variablen fest, auf die wir später zugreifen können:

<dependencies>
...
  <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-core</artifactId>
      <version>${vertx-version}</version>
  </dependency>
  <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
      <version>${vertx-version}</version>
  </dependency>
  <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-mongo-client</artifactId>
      <version>${vertx-version}</version>
  </dependency>
<dependencies>

Wir haben eine Variable vertx.version definiert, damit wir bei Versionsänderungen nur eine einzige Stelle in der pom.xml ändern müssen.

oder:

<build>
  <plugins>
    ...
    <plugin>
      ...
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
            <manifestEntries>
                <mode>production</mode>
                <url>${project.url}</url>
                <timestamp>${maven.build.timestamp}</timestamp>
            </manifestEntries>
        </archive>
      <configuration>
    </plugin>
  </plugins>
</build>

Hier verwenden wir die property maven.build.timestamp.format, um im Manifest des erstellten jars ein Attribut “timestamp” im gewünschten Format zu erstellen.

Die dritte oben gezeigte Property ist eine Maven Einstellung, die dazu führt, dass Maven den Quellcode systemunabhängig als UTF-8 intepretiert. Die vierte Property ist selbstdefiniert und erfüllt hier ausser “weil ich es kann” keinen speziellen Zweck.