Umgebungsspezifische Konfigurationen in Magnolia CMS

Einführung

Wenn ich ein grösseres Magnolia Projekt mit verschiedensten Instanzen aufsetze (local development, development, staging, integration, production), stellt sich früher oder später die Frage,  wie ich verschiedene Konfigurationen kontrolliert auf die verschiedenen Instanzen installieren kann.

In diesem Blog Post zeige ich einen einfachen Lösungsansatz auf, der sich beliebig erweitern und auf projektspezifische Bedürfnisse abstimmen lässt.

Problemstellung

Magnolia bringt out-of-the-box bereits einen Mechanismus mit, der umgebungsspezifisches Einspielen von XML Bootstrap Files erlaubt. Dieser Mechanismus ist aber beschränkt auf die Initialisierung des JCR Repositories und erlaubt deshalb weder das Einspielen von Updates via XML Files, noch eine Interaktion mit Java Code.

Im Laufe eines Projektes kommt oft der Punkt, an dem ich ein Update / Delta Task nur auf einem oder mehreren bestimmten Systemen ausführen möchte. Dazu zählen zum Beispiel das Ändern des SMTP Server im Mail Modul, oder das Aktualisieren eines Subscribers. Zudem kann es auf der lokalen Entwicklungsumgebung sinnvoll sein, bei jedem Neustart des Systems einen Teil der Konfigurationen neu zu schreiben, um die Entwicklung auf einem sauberen Stand zu starten oder Änderungen von anderen Entwicklern im eigenen Repository zu sehen.

Lösungsansatz

Im magnolia.properties File wird ein neues Property magnolia.environment definiert, welches den Namen der Umgebung beinhalten wird.

magnolia.properties

...
magnolia.environment = <ENV_NAME>
...

Dieser Lösungsansatz erfordert lediglich, dass für jede zu unterscheidende Umgebung ein eigenes magnolia.properties File existiert, was aber in grösseren Projekten weitgehend der Fall sein sollte.

Das neue Property wird durch eine simple Java Hilfsklasse ausgelesen, und erlaubt auf einfache Weise das Unterscheiden der verschiedenen Umgebungen, insbesondere der lokalen Entwicklungsumgebung. 

EnvironmentUtil.java

import info.magnolia.init.MagnoliaConfigurationProperties;
import info.magnolia.objectfactory.Components;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EnvironmentUtil {

    public static final String ENVIRONMENT_PROPERTY = "magnolia.environment";
    public static final String ENVIRONMENT_ALL = "all";
    public static final String ENVIRONMENT_LOCAL_DEVELOPMENT = "local-development";
    public static final String ENVIRONMENT_LOCAL_DEVELOPMENT_PUBLISH = "local-development-publish";
    private static final String ENVIRONMENT;
    private static final Logger log = LoggerFactory.getLogger(EnvironmentUtil.class);

    static {
        String envValue;
        try {
            envValue = Components.getComponent(MagnoliaConfigurationProperties.class).getProperty(ENVIRONMENT_PROPERTY);
        } catch (Throwable e) {
            envValue = StringUtils.EMPTY;
        }

        ENVIRONMENT = StringUtils.isNotBlank(envValue) ? envValue : ENVIRONMENT_ALL;
        log.info("Current environment set to {}", ENVIRONMENT);
    }

    public static String getEnvironment() {
        return ENVIRONMENT;
    }

    /**
     * @param environmentNames list of possible environments (will be processed with OR logic)
     * @return if one if the given environments or {@link EnvironmentUtil#ENVIRONMENT_ALL} is active    
     */

    public static boolean isAnyEnvironment(String... environmentNames) {
        boolean isAnyEnvironment = false;
        for (String environmentName : environmentNames) {
            if (isEnvironment(environmentName)) {
                isAnyEnvironment = true;
                break;
            }
        }
        return isAnyEnvironment;
    }

    public static boolean isEnvironment(String environmentName) {
        if (ENVIRONMENT_ALL.equalsIgnoreCase(environmentName)) {
            return true;
        } else {
            return getEnvironment().equals(environmentName);
        }
    }

    public static boolean isLocalDevelopmentEnvironment() {
        return isAnyEnvironment(
                ENVIRONMENT_LOCAL_DEVELOPMENT,
                ENVIRONMENT_LOCAL_DEVELOPMENT_PUBLISH
        );
    }

}

Anwendungsfälle

Nachdem ich dieses Basis Setup gemacht habe, gibt es verschiedenste Einsatz-Möglichkeiten.

A) Umgebungsabhängiger Task

Das offensichtlichste Einsatzgebiet wurde oben bereits erwähnt. Ich möchte einen Task nur auf einer bestimmten Umgebung ausführen.

EnvironmentMatchingDelegateTask.java

import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.ConditionalDelegateTask;
import info.magnolia.module.delta.Task;
import info.magnolia.module.delta.TaskExecutionException;
import org.apache.commons.lang.StringUtils;

public class EnvironmentMatchingDelegateTask extends ConditionalDelegateTask {

    private final String[] environments;

    public EnvironmentMatchingDelegateTask(Task taskToDelegate, String... environments) {
        super("Execute Task " + taskToDelegate.getName() + " in environments: " + StringUtils.join(environments, ", "), "", taskToDelegate);
        this.environments = environments;
    }

    @Override
    protected boolean condition(InstallContext installContext) throws TaskExecutionException {
        return EnvironmentUtil.isAnyEnvironment(environments);
    }

}
A1) SMTP Server im Mail Module

Oft gibt es in grösseren Projekten „interne“ und „externe“ Instanzen, also interne Test-Systeme und Systeme beim Kunden vor Ort. Gewisse Konfigurationen sind beispielsweise für alle internen Systeme gültig, wie beispielsweise der SMTP-Server, der verwendet wird um Formulare zu testen.

MyModuleVersionHandle.java

import info.magnolia.module.InstallContext;
import info.magnolia.module.delta.SetPropertyTask;
import info.magnolia.repository.RepositoryConstants;

public class InstallNamicsMailGatewayTask extends SetPropertyTask {

    public InstallNamicsMailGateway() {
        super(RepositoryConstants.CONFIG, "/modules/mail/config/smtp", "smtpServer", "<namics-mail-gateway-domain-name>");
    }

    @Override
    public void execute(InstallContext installContext) {

        //these values come from the magnolia.properties files of the different server configs below /WEB-INF/config
        String[] namicsEnvironments = new String[]{
                "namics-dev-author",
                "namics-dev-publish",
                "namics-sta-author",
                "namics-sta-publish",
                "namics-int-author",
                "namics-int-publish1",
                "namics-int-publish2"
        };

        if (EnvironmentUtil.isAnyEnvironment(namicsEnvironments)) {
            super.execute(installContext);
        } else {
            log.info("Skipping Task for non-Namics environments.");
        }
    }

}
A2) Spezialfall „Lokale Entwicklungsumgebung“

Um einen Task nur in der lokalen Entwicklungsumgebung auszuführen, trage ich im entsprechenden magnolia.properties File magnolia.environment = local-development ein. Danach kann ich in meinem ModuleVersionHandler einen Startup Task registrieren, welcher mir den Imaging Workspace leert, damit ImageVariations stets aktuell sind.

magnolia.properties

...
magnolia.environment = local-development
...
MyModuleVersionHandler.java

...
    @Override
    protected final List<Task> getStartupTasks(InstallContext installContext) {
        List<Task> startupTasks = new LinkedList<Task>(super.getStartupTasks(installContext));

        startupTasks.add(new EnvironmentMatchingDelegateTask(new EmptyImagingWorkspaceTask(), EnvironmentUtil.ENVIRONMENT_LOCAL_DEVELOPMENT));

        return startupTasks;
    }
...

B) Verschiedene Service Implementierungen

Immer wieder greifen wir mit Magnolia auf Umsysteme über Services zu. In solchen Fällen kann es dazu kommen, dass die Entwicklung von CMS Komponenten parallel zur Entwicklung der Service Implementierungen stattfindet. Um in dieser Zeit unabhängig entwickeln zu können, kann ich für die lokale Entwicklungsumgebung einen Dummy Service verwenden, welcher Testdaten liefert. Vereinfacht könnte dies so aussehen.

    public MyService getService() {
        if (EnvironmentUtil.isLocalDevelopmentEnvironment()) {
            return new DummyService();
        } else {
            return Components.getComponent(MyService.class);
        }
    }

C) Importieren von umgebungsspezifischen XML Bootstraps

Eine etwas ausgefeiltere Möglichkeit wäre, einen XML Bootstrap Mechanismus zu bauen. Dazu würde ich einen Task schreiben (analog ModuleBootstrapTask), welcher alle Bootstrap Files unterhalb von /environment-bootstrap/[environment-name]/ sammelt und diese automatisch ins Repository importiert. Dies wäre eine sinnvolle Ergänzung zum von Magnolia mitgelieferten Bootstrap Mechanismus unterhalb von /WEB-INF/bootstrap und /mgnl-bootstrap.

Ausblick & Feedback

Die Möglichkeiten sind an dieser Stelle noch lange nicht ausgeschöpft. Weitere Ideen, Kommentare und Diskussion sind herzlich willkommen!

Bei Namics gehen wir immer mehr in die Richtung, Magnolia Konfigurationen via Java Code zu erzeugen (configuration by code). Die Möglichkeit, gewisse Aktionen nur in gewissen Umgebungen auszuführen, ist ein wichtiger Baustein dieser Strategie.

Wir werden in den kommenden Wochen noch etwas detaillierter zu den Themen Verbessertes Module Version Handling und „Configuration by Code“ berichten. Also, stay tuned!

Ein Gedanke zu “Umgebungsspezifische Konfigurationen in Magnolia CMS

  1. Pingback: Verbessertes Module Version Handling – Magnolia

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind markiert *

*

*

Du kannst folgende HTML-Tags benutzen: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>